Scanf yerine giriş dönüşümü için ne kullanabilirim?


125

Çok sık insanların başkalarını kullanmasını scanfve daha iyi alternatifler olduğunu söylemelerini caydırdığını gördüm . Ancak, tüm gördüğüm ya "kullanmayın scanf" ya da "burada doğru bir biçim dizesi" , ve asla "daha iyi alternatifler" herhangi bir örnek .

Örneğin, şu kod snippet'ini ele alalım:

scanf("%c", &c);

Bu, son dönüşümden sonra giriş akışında kalan boşlukları okur. Bunun için her zaman önerilen çözüm aşağıdakileri kullanmaktır:

scanf(" %c", &c);

veya kullanmamak scanf.

Beri scanfkötü, giriş formatlarını dönüştürmek için bazı ANSI C seçenekleri nelerdir scanfkullanmadan genellikle işleyebilir (tamsayılar gibi, kayan noktalı sayılar ve dizeleri) scanf?

Yanıtlar:


87

Girdiyi okumanın en yaygın yolları şunlardır:

  • fgetsgenellikle önerilen sabit bir boyutta kullanmak ve

  • kullanarak fgetc, yalnızca tek bir kitap okuyorsanız yararlı olabilir char.

Girişi dönüştürmek için kullanabileceğiniz çeşitli işlevler vardır:

  • strtoll, bir dizeyi tamsayıya dönüştürmek için

  • strtof/ d/ ld, dizeyi kayan noktalı sayıya dönüştürmek için

  • sscanf, aşağıda belirtilen düşüşlerin çoğuna sahip olmasına rağmen, sadece kullanmak kadar kötü değildir .scanf

  • Ayırıcı ile ayrılmış bir girişi düz ANSI C'de ayrıştırmanın iyi bir yolu yoktur. Ya POSIX'ten kullanın strtok_rya da strtokiş parçacığı için güvenli olmayan. Ayrıca olabilir rulo kendi kullanarak evreli varyantını strcspnve strspngibi strtok_rherhangi bir özel işletim sistemi desteği içermez.

  • Aşırı olabilir, ancak lexer'ları ve ayrıştırıcıları kullanabilirsiniz ( flexve bisonen yaygın örnekler).

  • Dönüşüm yok, sadece dizeyi kullanın


Sorumun tam olarak neden scanf kötü olduğuna girmediğim için ayrıntılı olarak anlatacağım:

  • Dönüşüm belirteçleri ile %[...]ve %c, scanfboşluk yemez. Bu sorunun , bu sorunun birçok kopyası tarafından kanıtlandığı gibi, yaygın olarak bilinmemektedir .

  • 'Un bağımsız değişkenlerine (özellikle dizelerle) &atıfta bulunurken tekli operatörün ne zaman kullanılacağı konusunda bazı karışıklıklar vardır scanf.

  • Dönüş değerini dikkate almamak çok kolay scanf. Bu, tanımlanmamış davranışın başlatılmamış bir değişkeni okumasına kolayca neden olabilir.

  • Arabellek taşmasını önlemek unutmayı çok kolaydır scanf. scanf("%s", str)daha kötü olmasa bile, o kadar kötü gets.

  • İle tamsayıları dönüştürürken taşmayı algılayamazsınız scanf. Aslında taşma , bu işlevlerde tanımlanmamış davranışa neden olur .



56

Neden scanfkötü?

Asıl sorun, scanfhiçbir zaman kullanıcı girdisi ile ilgilenmek istememiş olmasıdır. "Mükemmel" biçimlendirilmiş verilerle kullanılması amaçlanmıştır. "Mükemmel" kelimesinden alıntı yaptım çünkü bu tamamen doğru değil. Ancak, kullanıcı girişi kadar güvenilir olmayan verileri ayrıştırmak için tasarlanmamıştır. Doğası gereği, kullanıcı girdisi tahmin edilemez. Kullanıcılar talimatları yanlış anlar, yazım hataları yapar, yanlışlıkla girilmeden önce enter tuşuna basar vb. Kullanıcı girişi için kullanılmaması gereken bir fonksiyonun neden okunduğu makul bir şekilde sorulabilir stdin. Deneyimli bir * nix kullanıcısıysanız, açıklama sürpriz olmayacaktır, ancak Windows kullanıcılarının kafasını karıştırabilir. * Nix sistemlerinde, borulama ile çalışan programlar oluşturmak çok yaygındır,stdoutstdinikinci. Bu şekilde, çıktının ve girdinin öngörülebilir olduğundan emin olabilirsiniz. Bu koşullar altında, scanfaslında iyi çalışıyor. Ancak öngörülemeyen girdilerle çalışırken, her türlü sorunla karşılaşırsınız.

Öyleyse neden kullanıcı girişi için kullanımı kolay standart işlevler yok? Biri sadece burada tahmin edebilir, ancak eski hardcore C korsanlarının basitçe mevcut işlevlerin çok tıknaz olmalarına rağmen yeterince iyi olduğunu düşündüklerini varsayıyorum. Ayrıca, tipik terminal uygulamalarına baktığınızda çok nadiren kullanıcı girdisini okurlar stdin. Çoğu zaman tüm kullanıcı girişlerini komut satırı argümanları olarak iletirsiniz. Elbette, istisnalar vardır, ancak çoğu uygulama için kullanıcı girişi çok küçük bir şeydir.

Peki ne yapabilirsin?

Benim favorim ile fgetskombinasyon halinde sscanf. Bir keresinde bununla ilgili bir cevap yazdım, ancak tam kodu yeniden göndereceğim. İşte iyi (ama mükemmel değil) hata kontrolü ve ayrıştırma ile bir örnek. Hata ayıklama amaçları için yeterince iyi.

Not

Özellikle kullanıcıdan tek bir satıra iki farklı şey girmesini istemiyorum. Bunu sadece doğal bir şekilde birbirlerine ait oldukları zaman yapıyorum. Mesela beğen printf("Enter the price in the format <dollars>.<cent>: ")ve kullan sscanf(buffer "%d.%d", &dollar, &cent). Asla böyle bir şey yapmam printf("Enter height and base of the triangle: "). Aşağıda ana kullanım noktası, fgetsbir girişin diğerini etkilemediğinden emin olmak için girişlerin kapsüllenmesidir.

#define bsize 100

void error_function(const char *buffer, int no_conversions) {
        fprintf(stderr, "An error occurred. You entered:\n%s\n", buffer);
        fprintf(stderr, "%d successful conversions", no_conversions);
        exit(EXIT_FAILURE);
}

char c, buffer[bsize];
int x,y;
float f, g;
int r;

printf("Enter two integers: ");
fflush(stdout); // Make sure that the printf is executed before reading
if(! fgets(buffer, bsize, stdin)) error_function(buffer, 0);
if((r = sscanf(buffer, "%d%d", &x, &y)) != 2) error_function(buffer, r);

// Unless the input buffer was to small we can be sure that stdin is empty
// when we come here.
printf("Enter two floats: ");
fflush(stdout);
if(! fgets(buffer, bsize, stdin)) error_function(buffer, 0);
if((r = sscanf(buffer, "%d%d", &x, &y)) != 2) error_function(buffer, r);

// Reading single characters can be especially tricky if the input buffer
// is not emptied before. But since we're using fgets, we're safe.
printf("Enter a char: ");
fflush(stdout);
if(! fgets(buffer, bsize, stdin)) error_function(buffer, 0);
if((r = sscanf(buffer, "%c", &c)) != 1) error_function(buffer, r);

printf("You entered %d %d %f %c\n", x, y, f, c);

Bunlardan birçoğunu yaparsanız, her zaman temizleyen bir sarıcı oluşturmayı önerebilirim:

int printfflush (const char *format, ...)
{
   va_list arg;
   int done;
   va_start (arg, format);
   done = vfprintf (stdout, format, arg);
   fflush(stdout);
   va_end (arg);
   return done;
}```

Bunu yapmak, yuva girdisi ile karışıklık yaratabilecek yeni satırsonu olan ortak bir sorunu ortadan kaldıracaktır. Ama başka bir sorunu var, yani çizgi daha uzunsa bsize. Bunu ile kontrol edebilirsiniz if(buffer[strlen(buffer)-1] != '\n'). Eğer yeni satırı kaldırmak istiyorsanız bunu ile yapabilirsiniz buffer[strcspn(buffer, "\n")] = 0.

Genel olarak, kullanıcının farklı değişkenlere ayrıştırmanız gereken bazı garip formatta girdi girmesini beklememenizi tavsiye ederim. Değişkenleri atamak istiyorsanız heightve widthikisini aynı anda sormayın. Kullanıcının kendi aralarında enter tuşuna basmasına izin verin. Ayrıca, bu yaklaşım bir anlamda çok doğaldır. stdinEnter tuşuna basana kadar girişi asla almayacaksınız , o zaman neden her zaman tüm satırı okumazsınız? Tabii ki, arabellekten daha uzunsa bu yine de sorunlara yol açabilir. C kullanıcı girişinin hantal olduğunu belirtmeyi hatırladım mı? :)

Arabelleğe göre daha uzun satırlarla ilgili sorunları önlemek için, otomatik olarak uygun boyutta bir arabelleği ayıran bir işlev kullanabilirsiniz getline(). Dezavantajı freedaha sonra sonuca ihtiyacınız olacak .

Oyunun hızlandırılması

C'de kullanıcı girişi ile programlar oluşturma konusunda ciddiyseniz, gibi bir kütüphaneye bakmanızı tavsiye ederim ncurses. Çünkü o zaman muhtemelen bazı terminal grafiklerine sahip uygulamalar oluşturmak istersiniz. Ne yazık ki, bunu yaparsanız biraz taşınabilirlik kaybedersiniz, ancak kullanıcı girişini daha iyi kontrol etmenizi sağlar. Örneğin, kullanıcının enter tuşuna basmasını beklemek yerine bir tuşa basmayı anında okuyabilmenizi sağlar.


(r = sscanf("1 2 junk", "%d%d", &x, &y)) != 2Sondaki sayısal olmayan metni o kadar kötü algılamadığını unutmayın .
chux - Monica

1
@chux% f% f düzeltildi. İlkiyle ne demek istiyorsun?
Klutt

İle fgets()arasında "1 2 junk", if((r = sscanf(buffer, "%d%d", &x, &y)) != 2) {bu "önemsiz" olsa bile girişli şey yanlış bildirmez.
chux - Monica'yı eski durumuna getir

@chux Ah, şimdi anlıyorum. Bu kasıtlıydı.
Klutt

1
scanfmükemmel biçimlendirilmiş verilerle kullanılması amaçlanmıştır, ancak bu doğru değildir. @Chux tarafından belirtildiği gibi "önemsiz" ile ilgili sorunun yanı sıra, benzer bir biçimin "%d %d %d"bir, iki veya üç satırdan (veya aradaki boş satırlar varsa daha da fazla) girişi okumaktan mutluluk duyduğu gerçeği de yoktur. kuvvet (söz hakkından) gibi bir şey yaparak bir iki satırlık girişine yolu "%d\n%d %d"vb scanfbiçimlendirilmiş için uygun olabilir akışı girişi, ama bir şey satır bazlı tüm iyi de değil.
Steve Summit

18

scanfEğer zaman müthiş biliyorum girişinizi her zaman iyi yapılandırılmış ve iyi huylu olduğunu. Aksi takdirde...

IMO, işte en büyük sorunlar scanf:

  • Arabellek taşması riski - %sve %[dönüştürme belirteçleri için bir alan genişliği belirtmezseniz , bir arabellek taşması riskiyle karşı karşıya kalırsınız (bir arabellek tutulacak boyuttan daha fazla girdi okumaya çalışırsınız). Ne yazık ki, bunu bir argüman olarak belirtmek için iyi bir yol yoktur (olduğu gibi printf) - bunu dönüşüm belirtecinin bir parçası olarak sabit kodlamak veya bazı makro parlaklıkları yapmak zorundasınız.

  • Reddedilmesi gereken girdileri kabul eder - %dDönüşüm belirteciyle bir girdi okuyorsanız ve bunun gibi bir şey 12w4yazıyorsanız, bu girdiyi reddetmeyi beklersiniz scanf , ancak girmez - giriş akışında 12bırakarak başarıyla dönüştürür ve atar w4bir sonraki okumayı kirletmek.

Peki bunun yerine ne kullanmalısın?

Genellikle tüm etkileşimli girdileri metin olarak okumanızı öneririm fgets- bir seferde okunacak maksimum karakter sayısını belirlemenizi sağlar, böylece arabellek taşmasını kolayca önleyebilirsiniz:

char input[100];
if ( !fgets( input, sizeof input, stdin ) )
{
  // error reading from input stream, handle as appropriate
}
else
{
  // process input buffer
}

Bir tuhaflık, fgetseğer oda varsa, aradaki yeni satırı tamponda saklayacağıdır, böylece birinin beklediğinizden daha fazla girdi yazıp yazmadığını görmek için kolay bir kontrol yapabilirsiniz:

char *newline = strchr( input, '\n' );
if ( !newline )
{
  // input longer than we expected
}

Bununla nasıl başa çıkacağınız size bağlıdır - tüm girişi elden reddedebilir ve kalan girişleri aşağıdakilerle karıştırabilirsiniz getchar:

while ( getchar() != '\n' ) 
  ; // empty loop

Veya şu ana kadar aldığınız girdiyi işleyebilir ve tekrar okuyabilirsiniz. Bu, çözmeye çalıştığınız soruna bağlıdır.

To tokenize girişi kullanabilirsiniz, (bir veya daha fazla sınırlayıcı dayalı o kadar bölünmüş) strtok-, ama dikkat strtok'yani yapabilirsiniz (bunun girişini (o dize Terminatör ile sınırlayıcıları üzerine yazar) değiştirir ve onun durumunu korumak olamaz t Bir dizgiyi kısmen işaretleyin, sonra başka bir dizgeyi işaretlemeye başlayın, sonra orijinal dizede kaldığınız yerden devam edin). strtok_sTokenizatörün durumunu koruyan bir varyant var , ancak AFAIK uygulaması isteğe bağlıdır ( __STDC_LIB_EXT1__mevcut olup olmadığını görmek için tanımlanmış olup olmadığını kontrol etmeniz gerekir ).

Girişinizi tokenleştirdikten sonra, dizeleri sayılara dönüştürmeniz gerekiyorsa (yani, "1234"=> 1234) seçenekleriniz vardır. strtolve strtodtamsayıların ve gerçek sayıların dize temsillerini ilgili türlerine dönüştürecektir. Onlar da sen yakalamak için izin 12w4yukarıda bahsettiğim sorunu - argümanlarından biri ilk karakter için bir gösterici değil dizede dönüştürülür:

char *text = "12w4";
char *chk;
long val;
long tmp = strtol( text, &chk, 10 );
if ( !isspace( *chk ) && *chk != 0 )
  // input is not a valid integer string, reject the entire input
else
  val = tmp;

Bir alan genişliği ... - veya bir dönüşüm bastırma (örneğin %*[%\n], cevabın ilerleyen kısımlarında uzun çizgilerle uğraşmak için yararlıdır) belirtmezseniz .
Toby Speight

Alan genişliklerinin çalışma zamanı belirtimini almanın bir yolu vardır, ancak bu hoş değildir. Sonunda kodunuzda (belki de kullanarak snprintf()) biçim dizesini oluşturmak zorunda kalırsınız .
Toby Speight

5
isspace()Orada en yaygın hatayı yaptınız - olarak temsil edilen imzasız karakterleri kabul ediyor int, bu nedenle imzalı unsigned charplatformlarda UB'den kaçınmak için almanız gerekiyor char.
Toby Speight

9

Bu cevapta metin satırlarını okuduğunuzu ve yorumladığınızı varsayacağım . Belki bir şeyler yazıp RETURN'a vuran kullanıcıya soruyorsunuzdur. Ya da belki bir tür veri dosyasından yapılandırılmış metin satırlarını okuyorsunuzdur.

Metin satırlarını okuduğunuzdan, kodunuzu bir metin satırı okuyan bir kütüphane işlevi etrafında düzenlemek mantıklıdır. Standart işlevi, fgets()başkaları olmasına rağmen (dahil getline). Ve sonra bir sonraki adım o metin satırını bir şekilde yorumlamaktır.

fgetsBir metin satırını okumak için aramak için temel tarif :

char line[512];
printf("type something:\n");
fgets(line, 512, stdin);
printf("you typed: %s", line);

Bu sadece bir metin satırında okur ve tekrar basar. Yazıldığı gibi, birkaç dakika içinde ulaşacağımız birkaç sınırlama var. Ayrıca çok büyük bir özelliği var: İkinci argüman olarak geçtiğimiz 512 sayısı , okumak istediğimiz fgetsdizinin boyutu . Bu gerçeği - ne kadar okumaya izin verildiğini söyleyebileceğimiz - diziyi çok fazla okuyarak taşmayacağından emin olabileceğimiz anlamına gelir .linefgetsfgetsfgets

Şimdi bir metin satırını nasıl okuyacağımızı biliyoruz, ama ya gerçekten bir tam sayı, kayan nokta numarası veya tek bir karakter ya da tek bir kelime okumak istersek? (Ne Yani, eğer scanfbiz geliştirmeye çalışıyoruz çağrı gibi bir biçim belirteci kullanarak olmuştu %d, %f, %cveya %s?)

Bir metin satırını - dize - bunlardan herhangi biri gibi yeniden yorumlamak kolaydır. Bir dizgiyi tamsayıya dönüştürmek için bunu yapmanın en basit (kusurlu olsa da) yolu çağırmaktır atoi(). Kayan noktalı sayıya dönüştürmek için vardır atof(). (Ve bir dakika içinde göreceğimiz gibi daha iyi yollar da var.) İşte çok basit bir örnek:

printf("type an integer:\n");
fgets(line, 512, stdin);
int i = atoi(line);
printf("type a floating-point number:\n");
fgets(line, 512, stdin);
float f = atof(line);
printf("you typed %d and %f\n", i, f);

Tek bir karakteri (belki yazmak için kullanıcı isterse yya nevet olarak / yanıt yok), kelimenin tam anlamıyla sadece bu gibi hattın ilk karakteri, yakalayabilir:

printf("type a character:\n");
fgets(line, 512, stdin);
char c = line[0];
printf("you typed %c\n", c);

(Bu, elbette, kullanıcının çok karakterli bir yanıt yazma olasılığını yok sayar; yazılan fazladan karakterleri sessizce yok sayar.)

Eğer kullanıcı isterse Nihayet, kesinlikle bir dize yazmak için değil giriş hattını tedavi etmek istiyorsa, boşluk içeren

hello world!

dize "hello"ve başka bir şey ( scanfbiçimin %syapacağı şey) tarafından takip edildiğinden , bu durumda, biraz lif aldım, çizgiyi bu şekilde yeniden yorumlamak o kadar kolay değil, sonuçta cevap sorunun bir kısmı biraz beklemek zorunda kalacak.

Ama önce atladığım üç şeye geri dönmek istiyorum.

(1) Biz çağırıyoruz

fgets(line, 512, stdin);

diziye okumak lineve 512 dizinin boyutu olduğu için taşmaması linegerektiğini fgetsbilir. Ancak 512'nin doğru sayı olduğundan emin olmak için (özellikle, birinin boyutu değiştirmek için programı değiştirip düzeltmediğini kontrol etmek için), linebeyan edilen yere tekrar okumalısınız . Bu bir sıkıntı, bu yüzden boyutları senkronize tutmanın iki daha iyi yolu var. (A) boyut için bir ad oluşturmak üzere önişlemciyi kullanabilirsiniz:

#define MAXLINE 512
char line[MAXLINE];
fgets(line, MAXLINE, stdin);

Veya, (b) C'nin sizeofoperatörünü kullanın :

fgets(line, sizeof(line), stdin);

(2) İkinci sorun, hatayı kontrol etmememiz. Girişi okurken, her zaman hata olasılığını kontrol etmelisiniz. Herhangi bir nedenle fgetssorduğunuz metin satırını okuyamıyorsa, boş bir işaretçi döndürerek bunu gösterir. Yani böyle şeyler yapmalıydık

printf("type something:\n");
if(fgets(line, 512, stdin) == NULL) {
    printf("Well, never mind, then.\n");
    exit(1);
}

Son olarak, metin satırı okumak için, o sorun var fgetskarakterleri okur ve bulduğu kadar diziye doldurur \nhattını sonlandırır karakteri ve onu doldurur \nsizin de diziye karakter . Önceki örneğimizi biraz değiştirirseniz bunu görebilirsiniz:

printf("you typed: \"%s\"\n", line);

Bunu çalıştırır ve bana sorulduğunda "Steve" yazarsam,

you typed: "Steve
"

Yani "ikinci satır dize okumak ve dışarı aslında geri baskılı çünkü üzerinde "Steve\n".

Bazen bu ekstra yeni satırın önemi yoktur (aradığımız gibi atoiveya atofher ikisi de sayıdan sonra sayısal olmayan herhangi bir girişi yok saydığından), ancak bazen çok önemlidir. Çoğu zaman bu satırsonu kaldırmak isteyeceğiz. Bunu yapmanın birkaç yolu var, ki bir dakika içinde ulaşacağım. (Bunu çok şey söylediğimi biliyorum. Ama tüm bu şeylere geri döneceğim, söz veriyorum.)

Ben dedin ki": Bu noktada, düşünme olabilir scanf hiçbir iyiydi ve bu başka bir şekilde çok daha iyi olurdu Ama. fgetsSıkıntı gibi görünmeye başlıyor Çağrı. scanfOldu o kadar kolay bunu kullanmaya devam edemez!? "

Tabii, isterseniz kullanmaya devam edebilirsiniz scanf. (Ve gerçekten basit şeyler için, bazı açılardan daha basittir.) Ama, lütfen, 17 tuhaflığı ve foiblesinden biri nedeniyle başarısız olduğunda bana ağlama ya da girişiniz nedeniyle sonsuz bir döngüye girme. beklemiyorduk ya da daha karmaşık bir şey yapmak için nasıl kullanacağınızı anlayamadığınızda. Ve fgetsgerçek rahatsızlıklarına bir göz atalım :

  1. Her zaman dizi boyutunu belirtmeniz gerekir. Tabii ki, bu hiç bir sıkıntı değil - bu bir özellik, çünkü tampon taşması Gerçekten Kötü Bir Şey.

  2. Dönüş değerini kontrol etmelisiniz. Aslında, bu bir yıkama, çünkü doğru kullanmak scanfiçin, dönüş değerini de kontrol etmelisiniz.

  3. Arkalý \nsoymalýsýn. Bu, itiraf ediyorum, gerçek bir sıkıntı. Keşke size bu küçük sorun yoktu işaret edebilir Standart bir işlevi olsaydı. (Lütfen kimse gelmez gets.) Ama scanf's17 farklı rahatsızlıkla kıyaslandığında , bunu fgetsherhangi bir günün sıkıntısından alacağım .

Peki nasıl o yeni satır şerit? Üç yol:

(a) Açık yol:

char *p = strchr(line, '\n');
if(p != NULL) *p = '\0';

(b) Zor ve kompakt bir yol:

strtok(line, "\n");

Ne yazık ki bu her zaman işe yaramıyor.

(c) Başka bir kompakt ve hafif belirsiz yol:

line[strcspn(line, "\n")] = '\0';

Ve şimdi bu yoldan çekildiğine göre, atladığım başka bir şeye geri dönebiliriz: atoi()ve atof(). Bunlarla ilgili sorun, size başarı veya başarısızlığın başarılı olduğuna dair herhangi bir yararlı gösterge vermemeleri: sondaki sayısal olmayan girişi sessizce yok sayarlar ve hiç sayısal girdi yoksa sessizce 0 döndürürler. Başka avantajları da olan tercih edilen alternatifler strtolve şeklindedir strtod. strtolAyrıca size (diğer şeyler arasında) etkisini elde edebilirsiniz, yani 10 dışında bir üs kullanmasına izin verir %oveya %xbirliktescanf. Ancak bu işlevlerin doğru bir şekilde nasıl kullanılacağını göstermek kendi başına bir hikaye ve zaten oldukça parçalanmış bir anlatıya dönüşen şeyden çok fazla dikkat dağıtıcı olurdu, bu yüzden şimdi onlar hakkında daha fazla bir şey söylemeyeceğim.

Ana anlatıların geri kalanı, tek bir sayı veya karakterden daha karmaşık olan ayrıştırmaya çalıştığınız girdilerle ilgilidir. İki sayı veya boşlukla ayrılmış birden çok sözcük veya belirli çerçeveleme noktalama işaretleri içeren bir satırı okumak isterseniz ne olur? Bu, işlerin ilginçleştiği ve kullanarak bir şeyler yapmaya çalışıyorsanız, muhtemelen işlerin karmaşıklaştığı scanfve fgetstüm bu seçeneklerin tüm hikayesi olmasına rağmen, bir metin satırını temiz bir şekilde okuduğunuzdan çok daha fazla seçenek olduğu yerdir. Muhtemelen bir kitabı doldurabilir, bu yüzden burada sadece yüzeyi çizebiliriz.

  1. En sevdiğim teknik, satırı boşlukla ayrılmış "kelimelere" ayırmak, sonra her "kelimeyle" bir şeyler yapmaktır. Bunu yapmak için temel bir Standart işlev strtok(aynı zamanda sorunları da vardır ve ayrıca ayrı bir tartışmayı derecelendirir). Benim kendi tercihim, bu ders notlarında tarif ettiğim bir işlev olan her parçalanmış "kelime" ye işaretçiler dizisi oluşturmak için özel bir işlevdir . Her halükarda, "kelimeler" elde ettikten sonra, her birini, belki de daha önce incelediğimiz aynı atoi/ atof/ strtol/ strtodişlevleriyle daha fazla işleyebilirsiniz .

  2. Paradoksal olarak, buradan nasıl uzaklaşacağımızı anlamak için oldukça fazla zaman ve çaba harcıyor scanfolsak da, az önce okuduğumuz metin satırıyla başa çıkmanın bir başka güzel yolu da fgetsonu iletmektir sscanf. Bu şekilde, scanfçoğu dezavantaj olmadan avantajların çoğunu elde edersiniz .

  3. Giriş sözdiziminiz özellikle karmaşıksa, ayrıştırmak için bir "regexp" kitaplığı kullanmak uygun olabilir.

  4. Son olarak, size uygun olan özel çözümleme çözümlerini kullanabilirsiniz. char *Beklediğiniz karakterleri kontrol eden bir işaretçi ile bir kerede bir karakter üzerinde hareket edebilirsiniz . Yoksa sizin gibi işlevleri kullanarak belirli karakterler için arama yapabilirsiniz strchrveya strrchr, ya strspnya strcspnya strpbrk. Veya daha önce atladığımız strtolveya strtodişlevlerini kullanarak rakam karakter gruplarını ayrıştırabilir / dönüştürebilir ve atlayabilirsiniz .

Söylenebilecek çok daha fazla şey var, ama umarım bu giriş sizi başlatır.


Yazmaktan sizeof (line)ziyade yazmak için iyi bir neden var mı sizeof line? Birincisi linebir tür adı gibi görünüyor !
Toby Speight

@TobySpeight İyi bir sebep mi? Hayır, bundan şüpheliyim. Parantezler benim alışkanlığımdır, çünkü nesneler ya da gerekli adlar olup olmadığını hatırlamaktan rahatsız edilemem, ancak birçok programcı bunları mümkün olduğunca dışarıda bırakır. (Benim için kişisel tercih ve stil meselesi ve bu konuda oldukça küçük bir konu.)
Steve Summit

sscanfDönüşüm motoru olarak kullanmak , ancak girişi farklı bir araçla toplamak (ve muhtemelen masaj yapmak) için +1 . Ama belki de bu getlinebağlamda bahsetmeye değer .
dmckee --- eski moderatör kedi yavrusu

" fscanfGerçek rahatsızlıkları" hakkında konuştuğunuz zaman , yani fgets? Ve rahatsızlık # 3, özellikle scanfkarakter sayısı girişini (satırsonu sıyırmayı daha temiz hale getirecek) döndürmek yerine tampona işe yaramaz bir işaretçi döndürdüğü için gerçekten beni rahatsız ediyor .
Supercat

1
sizeofTarzınızın açıklaması için teşekkürler . Benim için, parenleri ne zaman zorladığınızı hatırlamak kolaydır: Bence (type)değeri olmayan bir oyuncu gibi düşünüyorum (çünkü sadece türle ilgileniyoruz). Başka bir şey: strtok(line, "\n")bunun her zaman işe yaramadığını söylüyorsunuz , ancak ne zaman işe yaramayacağı belli değil. Çizginin arabellekten daha uzun olduğu durumu düşündüğünüzü tahmin ediyorum, bu yüzden yeni satırımız yok ve strtok()null döndürüyor mu? Gerçek bir yazık fgets()daha yararlı bir değer döndürmez, bu nedenle yeni satırın orada olup olmadığını öğrenebiliriz.
Toby Speight

7

Scanf yerine girişi ayrıştırmak için ne kullanabilirim?

Yerine scanf(some_format, ...), düşünün fgets()ilesscanf(buffer, some_format_and %n, ...)

Kullanarak " %n", kod tüm formatın başarılı bir şekilde taranıp taranmadığını ve sonunda fazladan boşluk olmayan önemsiz olmadığını tespit edebilir .

// scanf("%d %f fred", &some_int, &some_float);
#define EXPECTED_LINE_MAX 100
char buffer[EXPECTED_LINE_MAX * 2];  // Suggest 2x, no real need to be stingy.

if (fgets(buffer, sizeof buffer, stdin)) {
  int n = 0;
  // add ------------->    " %n" 
  sscanf(buffer, "%d %f fred %n", &some_int, &some_float, &n);
  // Did scan complete, and to the end?
  if (n > 0 && buffer[n] == '\0') {
    // success, use `some_int, some_float`
  } else {
    ; // Report bad input and handle desired.
  }

6

Ayrıştırma gereksinimlerini şu şekilde belirtelim:

  • geçerli girdi kabul edilmelidir (ve başka bir forma dönüştürülmelidir)

  • geçersiz giriş reddedilmelidir

  • herhangi bir girdi reddedildiğinde, kullanıcıya neden reddedildiğini açıklayan (açık bir şekilde "programcı olmayan normal insanlar tarafından kolayca anlaşılabilen" bir dil) açıklayan bir mesaj vermek gerekir (böylece insanlar sorun)

İşleri çok basit tutmak için, tek bir basit ondalık tamsayıyı (kullanıcı tarafından girilen) ve başka bir şeyi ayrıştırmayı düşünelim. Kullanıcının girişinin reddedilmesinin olası nedenleri:

  • giriş kabul edilemez karakterler içeriyordu
  • giriş, kabul edilen minimum değerden daha düşük bir sayıyı temsil eder
  • giriş, kabul edilen maksimum değerden daha yüksek bir sayıyı temsil eder
  • girdi, sıfır olmayan kesirli kısmı olan bir sayıyı temsil eder

Ayrıca "giriş kabul edilemez karakterler içeriyor" ifadesini doğru tanımlayalım; ve şunu söyle:

  • baştaki boşluk ve sondaki boşluk dikkate alınmaz (örneğin "
    5", "5" olarak değerlendirilir)
  • sıfır veya bir ondalık basamağa izin verilir (örn. "1234." ve "1234.000", "1234" ile aynı şekilde işlenir)
  • en az bir basamak olmalıdır (örn. "." reddedilir)
  • birden fazla ondalık basamağa izin verilmez (örn. "1.2.3" reddedilir)
  • rakamlar arasında olmayan virgüller reddedilir (ör. ", 1234" reddedilir)
  • ondalık noktadan sonraki virgüller reddedilir (örneğin "1234.000.000" reddedilir)
  • başka bir virgülden sonraki virgüller reddedilir (örneğin "1, 234" reddedilir)
  • diğer tüm virgüller yoksayılır (ör. "1,234", "1234" olarak değerlendirilir)
  • boşluk olmayan ilk karakter olmayan eksi işareti reddedildi
  • boşluk olmayan ilk karakter olmayan pozitif bir işaret reddedilir

Buradan aşağıdaki hata mesajlarının gerekli olduğunu belirleyebiliriz:

  • "Giriş başlangıcında bilinmeyen karakter"
  • "Giriş sonunda bilinmeyen karakter"
  • "Girdinin ortasında bilinmeyen karakter"
  • "Sayı çok düşük (minimum ....)"
  • "Sayı çok yüksek (maksimum ....)"
  • "Sayı bir tamsayı değil"
  • "Çok fazla ondalık nokta"
  • "Ondalık basamak yok"
  • "Sayının başında bozuk virgül"
  • "Sayının sonunda bozuk virgül"
  • "Sayının ortasında kötü virgül"
  • "Ondalık noktadan sonra virgül bozuk"

Bu noktadan itibaren, bir dizgiyi tamsayıya dönüştürmek için uygun bir fonksiyonun çok farklı hata tiplerini ayırt etmesi gerektiğini görebiliriz; ve " scanf()" veya " atoi()" veya " strtoll()" gibi bir şey tamamen ve tamamen değersizdir, çünkü girdide neyin yanlış olduğuna dair herhangi bir gösterge vermezler (ve neyin geçerli / neyin geçersiz olduğuna dair tamamen alakasız ve uygunsuz bir tanım kullanırlar) giriş").

Bunun yerine, işe yaramayan bir şey yazmaya başlayalım:

char *convertStringToInteger(int *outValue, char *string, int minValue, int maxValue) {
    return "Code not implemented yet!";
}

int main(int argc, char *argv[]) {
    char *errorString;
    int value;

    if(argc < 2) {
        printf("ERROR: No command line argument.\n");
        return EXIT_FAILURE;
    }
    errorString = convertStringToInteger(&value, argv[1], -10, 2000);
    if(errorString != NULL) {
        printf("ERROR: %s\n", errorString);
        return EXIT_FAILURE;
    }
    printf("SUCCESS: Your number is %d\n", value);
    return EXIT_SUCCESS;
}

Belirtilen gereklilikleri karşılamak için; bu convertStringToInteger()işlev büyük olasılıkla tek başına birkaç yüz satır kod olacak.

Şimdi, bu sadece "tek bir basit ondalık tamsayıyı ayrıştırma" idi. Karmaşık bir şeyi ayrıştırmak isteyip istemediğinizi düşünün; "ad, sokak adresi, telefon numarası, e-posta adresi" yapılarının bir listesi gibi; veya bir programlama dili gibi. Bu durumlarda, sakatlanmış bir şaka olmayan bir ayrıştırma oluşturmak için binlerce satır kod yazmanız gerekebilir.

Başka bir deyişle...

Scanf yerine girişi ayrıştırmak için ne kullanabilirim?

Gereksinimlerinize uyacak şekilde kendiniz (potansiyel olarak binlerce satır) kod yazın.


5

Burada flexbasit bir girişi taramak için kullanılan bir örnek, bu durumda ABD ( n,nnn.dd) veya Avrupa ( n.nnn,dd) formatlarında olabilecek ASCII kayan nokta sayıları dosyası . Bu sadece çok daha büyük bir programdan kopyalanır, bu yüzden çözülmemiş bazı referanslar olabilir:

/* This scanner reads a file of numbers, expecting one number per line.  It  */
/* allows for the use of European-style comma as decimal point.              */

%{
  #include <stdlib.h>
  #include <stdio.h>
  #include <string.h>
  #ifdef WINDOWS
    #include <io.h>
  #endif
  #include "Point.h"

  #define YY_NO_UNPUT
  #define YY_DECL int f_lex (double *val)

  double atofEuro (char *);
%}

%option prefix="f_"
%option nounput
%option noinput

EURONUM [-+]?[0-9]*[,]?[0-9]+([eE][+-]?[0-9]+)?
NUMBER  [-+]?[0-9]*[\.]?[0-9]+([eE][+-]?[0-9]+)?
WS      [ \t\x0d]

%%

[!@#%&*/].*\n

^{WS}*{EURONUM}{WS}*  { *val = atofEuro (yytext); return (1); }
^{WS}*{NUMBER}{WS}*   { *val = atof (yytext); return (1); }

[\n]
.


%%

/*------------------------------------------------------------------------*/

int scan_f (FILE *in, double *vals, int max)
{
  double *val;
  int npts, rc;

  f_in = in;
  val  = vals;
  npts = 0;
  while (npts < max)
  {
    rc = f_lex (val);

    if (rc == 0)
      break;
    npts++;
    val++;
  }

  return (npts);
}

/*------------------------------------------------------------------------*/

int f_wrap ()
{
  return (1);
}

-5

Diğer cevaplar doğru düşük seviyeli ayrıntıları verir, bu yüzden kendimi daha yüksek bir seviyeyle sınırlayacağım: İlk olarak, her bir giriş satırının nasıl görünmesini beklediğinizi analiz edin . Resmi bir söz dizimi ile giriş tanımlamak için deneyin - şans ile bunu bir kullanılarak tarif edilebilir bulacaksınız düzenli dilbilgisi bir ya da en azından bağlam serbest dilbilgisi . Düzenli bir dilbilgisi yeterliyse, sonlu durumlu bir makineyi kodlayabilirsinizher komut satırını bir kerede bir karakter tanıyan ve yorumlayan. Kodunuz daha sonra bir satır okuyacaktır (diğer yanıtlarda açıklandığı gibi), sonra arabellekteki karakterleri durum makinesinden tarar. Belirli durumlarda durur ve şimdiye kadar taranan alt dizeyi bir sayıya veya herhangi bir şeye dönüştürürsünüz. Bu kadar basitse muhtemelen 'kendinizinkini yuvarlayabilirsiniz'; tam bir bağlamsız dilbilgisi istediğinizi bulursanız, mevcut ayrıştırma araçlarını (yeniden: lexve / yaccveya bunların varyantlarını) nasıl kullanacağınızı bulmak daha iyidir .


Sonlu durumlu bir makine fazla olabilir; dönüşümlerdeki taşmayı tespit etmenin daha kolay yolları ( errno == EOVERFLOWkullandıktan sonra kontrol edip etmemek gibi strtoll) mümkündür.
SS Anne

1
Flex bunları yazmayı önemsiz bir şekilde basitleştirdiğinde neden kendi sonlu durum makinenizi kodlarsınız?
jamesqf
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.