“While (! Feof (file))” neden her zaman yanlış?


573

Son zamanlarda bu tür dosyaları okumaya çalışan insanlar gördüm:

#include <stdio.h>
#include <stdlib.h>

int
main(int argc, char **argv)
{
    char *path = "stdin";
    FILE *fp = argc > 1 ? fopen(path=argv[1], "r") : stdin;

    if( fp == NULL ) {
        perror(path);
        return EXIT_FAILURE;
    }

    while( !feof(fp) ) {  /* THIS IS WRONG */
        /* Read and process data from file… */
    }
    if( fclose(fp) != 0 ) {
        perror(path);
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}

Bu döngüde sorun nedir?



Yanıtlar:


453

Soyut, üst düzey bir bakış açısı sunmak istiyorum.

Eşzamanlılık ve eşzamanlılık

G / Ç işlemleri çevre ile etkileşime girer. Ortam programınızın bir parçası değildir ve kontrolünüz altında değildir. Ortam, programınızla gerçekten "eşzamanlı" olarak bulunur. Eşzamanlı her şeyde olduğu gibi, "mevcut durum" ile ilgili sorular anlamlı değildir: Eşzamanlı olaylar arasında "eşzamanlılık" kavramı yoktur. Devletin birçok özelliği eşzamanlı olarak mevcut değildir .

Bunu daha açık bir şekilde ifade edeyim: "Daha fazla veriye sahip misiniz?" Bunu eşzamanlı bir kapsayıcıdan veya G / Ç sisteminizden isteyebilirsiniz. Ancak cevap genellikle hareketsiz ve dolayısıyla anlamsızdır. Peki ya konteyner "evet" diyorsa - okumaya çalıştığınız zaman, artık veri olmayabilir. Benzer şekilde, cevap "hayır" ise, okumaya çalıştığınız zaman, veriler gelmiş olabilir. Sonuç şudur ki ,"Verilerim var" gibi bir özellik yoktur, çünkü olası bir yanıta yanıt olarak anlamlı bir şekilde hareket edemezsiniz. (Durum, tamponlu girdi ile biraz daha iyidir, burada bir çeşit garanti oluşturan "evet, verilerim var" olabilir, ancak yine de karşı dava ile başa çıkabilmeniz gerekir. kesinlikle açıkladığım kadar kötü: bu diskin mi yoksa ağ arabelleğinin dolu olup olmadığını asla bilemezsiniz.)

Biz imkansız olduğunu ve aslında un olduğu sonucuna Yani makul bunun olmadığını bir I / O sistemi sormaya, olacak bir I / O işlemi gerçekleştirmek mümkün. Onunla etkileşimde bulunabilmemizin tek yolu (aynı anda bir kapta olduğu gibi) işlemi denemek ve başarılı veya başarısız olup olmadığını kontrol etmektir. Çevre ile etkileşime girdiğiniz anda, o zaman ve ancak o zaman etkileşimin gerçekten mümkün olup olmadığını biliyorsunuz ve bu noktada etkileşimi gerçekleştirmeyi taahhüt etmelisiniz. (İsterseniz bu bir "senkronizasyon noktası" dır.)

EOF

Şimdi EOF'a geçiyoruz. EOF ise tepki bir olsun teşebbüs I / O işlemi. Bu, bir şey okumaya veya yazmaya çalıştığınız anlamına gelir, ancak bunu yaparken herhangi bir veri okuyamaz veya yazamazsınız ve bunun yerine giriş veya çıkışın sonu ile karşılaşılır. Bu, ister C standart kitaplığı, C ++ iostreams veya diğer kitaplıklar olsun, tüm I / O API'leri için geçerlidir. G / Ç işlemleri başarılı olduğu sürece , ilerideki işlemlerin başarılı olup olmayacağını bilemezsiniz . Her zaman önce işlemi denemeli ve sonra başarı veya başarısızlığa cevap vermelisiniz .

Örnekler

Örneklerin her birinde, önce G / Ç işlemini denediğimizi ve ardından geçerliyse sonucu kullandığımızı dikkatlice not edin . Ayrıca , her örnekte farklı şekiller ve formlar alsa da, her zaman G / Ç işleminin sonucunu kullanmamız gerektiğini unutmayın .

  • C stdio, bir dosyadan okunur:

    for (;;) {
        size_t n = fread(buf, 1, bufsize, infile);
        consume(buf, n);
        if (n < bufsize) { break; }
    }

    Kullanmamız gereken sonuç n, okunan öğelerin sayısıdır (sıfır kadar az olabilir).

  • Cı, stdio scanf:

    for (int a, b, c; scanf("%d %d %d", &a, &b, &c) == 3; ) {
        consume(a, b, c);
    }

    Kullanmamız gereken sonuç scanf, dönüştürülen öğe sayısı olan dönüş değeridir .

  • C ++, iostreams formatlı çıkarma:

    for (int n; std::cin >> n; ) {
        consume(n);
    }

    Kullanmamız gereken sonuç std::cin, boole bağlamında değerlendirilebilen ve akışın hala good()devlette olup olmadığını bize söyleyen kendisidir .

  • C ++, iostreams getline:

    for (std::string line; std::getline(std::cin, line); ) {
        consume(line);
    }

    Kullanmamız gereken sonuç, daha std::cinönce olduğu gibi tekrar .

  • write(2)Bir arabelleği temizlemek için POSIX :

    char const * p = buf;
    ssize_t n = bufsize;
    for (ssize_t k = bufsize; (k = write(fd, p, n)) > 0; p += k, n -= k) {}
    if (n != 0) { /* error, failed to write complete buffer */ }

    Burada kullandığımız sonuç k, yazılan bayt sayısıdır. Buradaki nokta, sadece yazma işleminden sonra kaç bayt yazıldığını bilmemizdir .

  • POSIX getline()

    char *buffer = NULL;
    size_t bufsiz = 0;
    ssize_t nbytes;
    while ((nbytes = getline(&buffer, &bufsiz, fp)) != -1)
    {
        /* Use nbytes of data in buffer */
    }
    free(buffer);

    Kullanmamız gereken sonuç nbytes, satırsonuna kadar (veya dosya satırsonu ile bitmediyse EOF) dahil olmak üzere bayt sayısıdır.

    -1Bir hata oluştuğunda veya EOF'a ulaştığında işlevin açıkça döndüğünü (EOF! Değil) unutmayın.

"EOF" kelimesini nadiren dile getirdiğimizi fark edebilirsiniz. Hata durumunu genellikle bizim için daha ilginç olan başka bir şekilde tespit ederiz (örneğin, istediğimiz kadar G / Ç gerçekleştirememe). Her örnekte, bize açıkça EOF durumuna rastlandığını söyleyebilen bazı API özellikleri vardır, ancak bu aslında çok kullanışlı bir bilgi değildir. Sıklıkla önemsediğimizden çok daha ayrıntılı. Önemli olan, G / Ç'nin başarılı olup olmadığı, başarısız olduğu durumdan daha fazla.

  • EOF durumunu gerçekten sorgulayan son bir örnek: Bir dizeye sahip olduğunuzu ve bunun bir tamsayıyı temsil ettiğini test etmek istediğinizi varsayalım. C ++ iostreams kullanarak, şu şekilde gider:

    std::string input = "   123   ";   // example
    
    std::istringstream iss(input);
    int value;
    if (iss >> value >> std::ws && iss.get() == EOF) {
        consume(value);
    } else {
        // error, "input" is not parsable as an integer
    }

    Burada iki sonuç kullanıyoruz. Birincisi iss, biçimlendirilmiş çıkarma işleminin valuebaşarılı olup olmadığını kontrol etmek için stream nesnesinin kendisidir . Ancak, beyaz alanı da tükettikten sonra, başka bir I / O / işlemi gerçekleştiririz iss.get()ve EOF olarak başarısız olmasını bekleriz, bu da tüm dizenin biçimlendirilmiş ekstraksiyon tarafından zaten kullanılmış olması durumunda olur.

    C standart kütüphanesinde strto*l, uç işaretçisinin giriş dizesinin sonuna gelip gelmediğini kontrol ederek işlevlere benzer bir şey elde edebilirsiniz .

Cevap

while(!feof)yanlıştır, çünkü alakasız olan bir şeyi test eder ve bilmeniz gereken bir şeyi test edemez. Sonuç olarak, aslında bu hiç gerçekleşmediğinde, başarıyla okunan verilere eriştiğini varsayan kodu yanlışlıkla yürütüyorsunuz.


34
@CiaPan: Bunun doğru olduğunu düşünmüyorum. Hem C99 hem de C11 buna izin verir.
Kerrek SB

11
Ancak ANSI C bunu yapmaz.
CiaPan

3
@JonathanMee: Bahsettiğim tüm nedenlerden dolayı kötü: geleceğe bakamazsın. Gelecekte ne olacağını söyleyemezsiniz.
Kerrek SB

3
@JonathanMee: Evet, bu uygun olurdu, ancak genellikle bu kontrolü operasyonla birleştirebilirsiniz (çünkü çoğu iostreams işlemi, boolean dönüşümü olan akış nesnesini döndürür) ve bu şekilde, dönüş değerini göz ardı etmek.
Kerrek SB

4
Üçüncü paragraf, kabul edilmiş ve ileri derecede onaylanmış bir cevap için oldukça yanıltıcı / yanlıştır. feof()"G / Ç sistemine daha fazla veri olup olmadığını sormaz". feof(), (Linux) kılavuzuna göre : "akış tarafından gösterilen akış için dosya sonu göstergesini test eder ve ayarlanmışsa sıfırdan farklı bir değer döndürür." (ayrıca, clearerr()bu göstergeyi sıfırlamanın tek yolu açık bir çağrıdır ); Bu açıdan William Pursell'in cevabı çok daha iyi.
Arne Vogel

234

Yanlış çünkü (okuma hatası olmadığında) döngüye yazarın beklediğinden bir kez daha girer. Bir okuma hatası varsa, döngü asla sonlanmaz.

Aşağıdaki kodu göz önünde bulundurun:

/* WARNING: demonstration of bad coding technique!! */

#include <stdio.h>
#include <stdlib.h>

FILE *Fopen(const char *path, const char *mode);

int main(int argc, char **argv)
{
    FILE *in;
    unsigned count;

    in = argc > 1 ? Fopen(argv[1], "r") : stdin;
    count = 0;

    /* WARNING: this is a bug */
    while( !feof(in) ) {  /* This is WRONG! */
        fgetc(in);
        count++;
    }
    printf("Number of characters read: %u\n", count);
    return EXIT_SUCCESS;
}

FILE * Fopen(const char *path, const char *mode)
{
    FILE *f = fopen(path, mode);
    if( f == NULL ) {
        perror(path);
        exit(EXIT_FAILURE);
    }
    return f;
}

Bu program sürekli olarak giriş akışındaki karakter sayısından daha fazla bir tane yazdıracaktır (okuma hatası olmadığı varsayılarak). Giriş akışının boş olduğu durumu düşünün:

$ ./a.out < /dev/null
Number of characters read: 1

Bu durumda, feof() herhangi bir veri okunmadan önce çağrılır, böylece false döndürür. Döngü girilir, fgetc()çağrılır (ve geri döner EOF) ve sayım artırılır. Sonra feof()çağrılır ve döngünün iptal edilmesine neden olarak true değerini döndürür.

Bu, tüm bu durumlarda olur. feof()kadar gerçek dönmez sonra akışınızda bir okuma dosyasının sonuna karşılaşır. Amacı feof(), bir sonraki okumanın dosyanın sonuna ulaşıp ulaşmayacağını kontrol ETMEMEKTEDİR. Amacı, feof()bir okuma hatası ile dosyanın sonuna ulaşma arasında ayrım yapmaktır. Eğer fread()getiri 0, kullanmak gerekir feof/ ferrorbir hata ile karşılaşıldı veya verilerin tümünün tüketilmesinden gerekmediğine karar vermek için. Benzer şekilde fgetcdönerse EOF. feof()Sadece yararlıdır sonra freadla sıfır döndü veyafgetc geri döndü EOF. Bu gerçekleşmeden önce feof()her zaman 0 döndürür.

Her zaman okunan değerin (an fread(), veya an fscanf()veyafgetc() )feof() .

Daha da kötüsü, bir okuma hatasının oluştuğu durumu düşünün. Bu durumda, fgetc()döndürür EOF, feof()yanlış döndürür ve döngü hiçbir zaman sona ermez. while(!feof(p))Kullanılan tüm durumlarda , döngü içinde en az bir kontrol olmalıdır.ferror() veya en azından while koşulu ile değiştirilmelidir while(!feof(p) && !ferror(p))veya muhtemelen her türlü çöpü püskürten sonsuz bir döngü olasılığı vardır. geçersiz veri işleniyor.

Yani, özet olarak, " while(!feof(f))" yazmanın anlamsal olarak doğru olabileceği bir durum asla olmadığı kesin olarak söyleyemememe rağmen (her ne kadar bir okuma hatası üzerinde sonsuz bir döngüden kaçınmak için döngü içinde bir mola ile başka bir kontrol olması gerekir) ), neredeyse her zaman yanlış olduğu durumdur. Ve bir dava doğru olacağı yerde ortaya çıksa bile, bu kadar deyimsel olarak yanlıştır, kodu yazmanın doğru yolu olmaz. Bu kodu gören herkes derhal tereddüt etmeli ve "bu bir hata" demelidir. Ve muhtemelen yazarı tokatlayın (yazar sizin patronunuz değilse, bu durumda takdir önerilir.)


7
Tabii yanlış - ama bunun dışında "minnetle çirkin" değil.
nobar

89
Doğru kodun bir örneğini eklemelisiniz, çünkü birçok insanın hızlı bir düzeltme için buraya geleceğini hayal ediyorum.
jleahy

6
@Thomas: Ben bir C ++ uzmanı değilim, ama file.eof () etkili bir şekilde aynı sonucu döndürdüğüne inanıyorum feof(file) || ferror(file), bu yüzden çok farklı. Ancak bu soru C ++ için geçerli değildir.
William Pursell

6
@ m-ric da doğru değil, çünkü yine de başarısız olan bir okumayı işlemeye çalışacaksınız.
Mark Ransom

4
asıl doğru cevap bu. feof (), önceki okuma girişiminin sonucunu bilmek için kullanılır. Böylece, muhtemelen döngü kırılma durumunuz olarak kullanmak istemezsiniz. +1
Jack

63

Hayır, her zaman yanlış değildir. Döngü koşulunuz "dosyanın sonunu okumayı denemediğimiz halde" ise, kullanırsınız while (!feof(f)). Ancak bu yaygın bir döngü koşulu değildir - genellikle başka bir şeyi test etmek istersiniz ("daha fazla okuyabilir miyim" gibi). while (!feof(f))yanlış değil, sadece yanlış kullanılıyor .


1
Merak ediyorum ... f = fopen("A:\\bigfile"); while (!feof(f)) { /* remove diskette */ }ya da (bunu test edecek)f = fopen(NETWORK_FILE); while (!feof(f)) { /* unplug network cable */ }
pmg

1
@pmg: Söylendiği gibi, "ortak bir döngü durumu değil" hehe. İhtiyacım olan herhangi bir durumu gerçekten düşünemiyorum, genellikle hata işlemeyi ima eden her şeyle "istediğimi okuyabilir miyim" ile ilgileniyorum
Erik

@pmg: Dediğin gibi, nadiren istiyorsunwhile(!eof(f))
Erik

9
Daha doğrusu, "dosyanın sonuna kadar okumaya çalışmadık ve okuma hatası feofolmadı " durumu dosya sonunu tespit etmekle ilgili değildir; hata nedeniyle veya girişin tükendiği için okumanın kısa olup olmadığını belirlemekle ilgilidir.
William Pursell

35

feof()dosyanın sonunu okumaya çalışıp çalışmadığını gösterir. Bu, çok az öngörücü etkisi olduğu anlamına gelir: eğer doğruysa, bir sonraki giriş işleminin başarısız olacağından eminsiniz (bir öncekinin BTW'de başarısız olduğundan emin değilsiniz), ancak yanlışsa, bir sonraki girişten emin değilsiniz operasyon başarılı olacaktır. Dahası, giriş işlemleri dosya sonundan başka bir nedenden dolayı başarısız olabilir (biçimlendirilmiş giriş için bir biçim hatası, saf bir G / Ç hatası - disk hatası, ağ zaman aşımı - tüm giriş türleri için), bu nedenle dosyanın sonu (ve öngörücü olan Ada birini uygulamaya çalışan herkes, boşlukları atlamanız gerektiğinde karmaşık olabileceğini ve etkileşimli cihazlarda istenmeyen etkilere sahip olduğunu söyleyebilir - bazen bir sonraki girdinin girişini zorlar bir öncekinin işlemine başlamadan önce),

Bu nedenle, C'deki doğru deyim, döngü koşulu olarak IO işlem başarısı ile döngü yapmak ve daha sonra hatanın nedenini test etmektir. Örneğin:

while (fgets(line, sizeof(line), file)) {
    /* note that fgets don't strip the terminating \n, checking its
       presence allow to handle lines longer that sizeof(line), not showed here */
    ...
}
if (ferror(file)) {
   /* IO failure */
} else if (feof(file)) {
   /* format error (not possible with fgets, but would be with fscanf) or end of file */
} else {
   /* format error (not possible with fgets, but would be with fscanf) */
}

2
Bir dosyanın sonuna ulaşmak bir hata değildir, bu yüzden "giriş işlemleri dosyanın sonundan başka nedenlerle başarısız olabilir" ifadesini soruyorum.
William Pursell

@WilliamPursell, eof'e ulaşmak mutlaka bir hata değildir, ancak eof nedeniyle bir girdi işlemi yapamamak bir tanedir. Ve C'de bir giriş işlemi yapmadan oeof'in güvenilir bir şekilde tespit edilmesi imkansızdır.
AProgrammer

Katılıyorum elsemümkün değil sizeof(line) >= 2ve fgets(line, sizeof(line), file)patolojik size <= 0ve mümkün fgets(line, size, file). Belki de mümkün sizeof(line) == 1.
chux - Monica

1
Tüm bu "tahmini değer" konuşması ... Bunu hiç böyle düşünmemiştim. Benim feof(f)dünyamda hiçbir şey TAHMİN ETMEMEKTEDİR. ÖNCEKİ bir işlemin dosyanın sonuna geldiğini belirtir. Ne fazla ne eksik. Önceden bir işlem yapılmadıysa (yeni açılmışsa), dosyanın başlaması boş olsa bile dosyanın sonunu bildirmez. Bu nedenle, yukarıdaki başka bir cevaptaki eşzamanlılık açıklaması dışında, dönmemek için herhangi bir neden olduğunu düşünmüyorum feof(f).
BitTickler

@AProgrammer: isteği bir verimleri daha fazla veri mevcut olduğu için bir "sürekli" EOF olsun veya olmasın, sıfır olduğu "N bayt kadar okuma" yapılmamış , bir hata değildir. Feof () güvenilir bir şekilde gelecek talepleri verileri verecektir tahmin olmasa da, güvenilir gelecekteki istekleri gösterebilir olmaz . Belki de, sıradan bir dosyanın sonuna kadar okuduktan sonra, kaliteli bir uygulamanın gelecekteki okumaların başarmak için bir sebep olmadığını söylemesi gereken "gelecekteki okuma isteklerinin başarılı olacağı akla yatkın" diyen bir durum işlevi olmalıdır . inanabilirler .
supercat

0

feof()çok sezgisel değil. Benim düşünceme göre, herhangi bir okuma işleminin dosya sonuna ulaşılmasıyla sonuçlanırsa, dosyanın dosya sonu FILEdurumu olarak ayarlanmalıdır true. Bunun yerine, her okuma işleminden sonra dosyanın sonuna ulaşılıp ulaşılmadığını manuel olarak kontrol etmeniz gerekir. Örneğin, aşağıdakileri kullanarak bir metin dosyasından okursanız böyle bir şey işe yarayacaktır fgetc():

#include <stdio.h>

int main(int argc, char *argv[])
{
  FILE *in = fopen("testfile.txt", "r");

  while(1) {
    char c = fgetc(in);
    if (feof(in)) break;
    printf("%c", c);
  }

  fclose(in);
  return 0;
}

Bunun yerine böyle bir şeyin işe yaraması harika olurdu:

#include <stdio.h>

int main(int argc, char *argv[])
{
  FILE *in = fopen("testfile.txt", "r");

  while(!feof(in)) {
    printf("%c", fgetc(in));
  }

  fclose(in);
  return 0;
}

1
printf("%c", fgetc(in));? Bu tanımsız davranış. fgetc()döner int, değil char.
Andrew Henle

Bana öyle geliyor ki standart deyim while( (c = getchar()) != EOF)çok "böyle bir şey".
William Pursell

while( (c = getchar()) != EOF)GNU C 10.1.0 çalıştıran masaüstümden birinde çalışıyor, ancak GNU C 9.3.0 çalıştıran Raspberry Pi 4'ümde başarısız oluyor. RPi4'ümde dosyanın sonunu algılamıyor ve devam ediyor.
Scott Deagan

@AndrewHenle Haklısın! İşlere char cgeçiş int c! Teşekkürler!!
Scott Deagan
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.