Dlaczego „while (! Feof (file))” zawsze jest błędne?

573

Ostatnio widziałem ludzi próbujących czytać takie pliki w wielu postach:

#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;
}

Co jest nie tak z tą pętlą?

William Pursell
źródło

Odpowiedzi:

453

Chciałbym przedstawić abstrakcyjną perspektywę wysokiego poziomu.

Współbieżność i równoczesność

Operacje we / wy współdziałają ze środowiskiem. Środowisko nie jest częścią twojego programu i nie jest pod twoją kontrolą. Środowisko naprawdę istnieje „równolegle” z twoim programem. Podobnie jak w przypadku wszystkich rzeczy współbieżnych, pytania dotyczące „obecnego stanu” nie mają sensu: nie ma pojęcia „jednoczesności” między równoległymi zdarzeniami. Wiele właściwości stanu po prostu nie istnieje jednocześnie.

Pozwól, że uściślę: Załóżmy, że chcesz zapytać „czy masz więcej danych”. Możesz zapytać o współbieżny kontener lub swój system I / O. Ale odpowiedź jest generalnie bezczynna, a zatem bez znaczenia. Co z tego, jeśli pojemnik powie „tak” - zanim spróbujesz czytać, może już nie mieć danych. Podobnie, jeśli odpowiedź brzmi „nie”, do czasu próby odczytania dane mogły przybyć. Wniosek jest taki, że po prostu jestżadna właściwość, taka jak „Mam dane”, ponieważ nie można podjąć znaczących działań w odpowiedzi na jakąkolwiek możliwą odpowiedź. (Sytuacja jest nieco lepsza w przypadku buforowanych danych wejściowych, w których można uzyskać odpowiedź „tak, mam dane”, która stanowi pewną gwarancję, ale nadal będziesz musiał poradzić sobie z przypadkiem odwrotnym. I przy wyjściu z sytuacji jest z pewnością tak źle, jak opisałem: nigdy nie wiadomo, czy ten dysk lub bufor sieciowy jest pełny.)

Stwierdzamy zatem, że nie jest możliwe, a wręcz nieuzasadnione , zapytanie systemu we / wy, czy będzie on w stanie wykonać operację we / wy. Jedynym możliwym sposobem na interakcję z nim (podobnie jak przy równoczesnym kontenerze) jest próba wykonania operacji i sprawdzenie, czy się powiodła, czy nie. W tym momencie, w którym wchodzisz w interakcję ze środowiskiem, wtedy i tylko wtedy możesz wiedzieć, czy interakcja była rzeczywiście możliwa, i wtedy musisz zobowiązać się do wykonania interakcji. (Jest to „punkt synchronizacji”, jeśli chcesz).

EOF

Teraz dochodzimy do EOF. EOF to odpowiedź uzyskana z próby operacji we / wy. Oznacza to, że próbujesz coś odczytać lub napisać, ale nie udało ci się odczytać ani zapisać żadnych danych, a zamiast tego napotkano koniec wejścia lub wyjścia. Dotyczy to zasadniczo wszystkich interfejsów API we / wy, bez względu na to, czy jest to biblioteka standardowa C, iostreams C ++, czy inne biblioteki. Dopóki operacje we / wy zakończą się powodzeniem , po prostu nie będzie wiadomo, czy dalsze przyszłe operacje zakończą się powodzeniem. Zawsze musisz najpierw wypróbować operację, a następnie zareagować na sukces lub porażkę.

Przykłady

W każdym z przykładów zwróć uwagę, że najpierw próbujemy operacji We / Wy, a następnie wykorzystujemy wynik, jeśli jest prawidłowy. Zauważ ponadto, że zawsze musimy użyć wyniku operacji We / Wy, chociaż wynik ma różne kształty i formy w każdym przykładzie.

  • C stdio, odczytane z pliku:

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

    Wynik, którego musimy użyć n, to liczba odczytanych elementów (która może wynosić zaledwie zero).

  • C stdio scanf:

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

    Wynik, którego musimy użyć, to zwracana wartość scanfliczby przekonwertowanych elementów.

  • C ++, ekstrakcja sformatowana przez iostreams:

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

    Wynik, którego musimy użyć, to std::cinsam, który można ocenić w kontekście logicznym i mówi nam, czy strumień jest nadal w good()stanie.

  • C ++, iostreams getline:

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

    Rezultat, którego musimy użyć, jest ponownie std::cin, tak jak poprzednio.

  • POSIX, write(2)aby opróżnić bufor:

    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 */ }

    Wynik, którego tu używamy k, to liczba zapisanych bajtów. Chodzi o to, że możemy wiedzieć tylko, ile bajtów zostało napisanych po operacji zapisu.

  • 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);

    Wynik, którego musimy użyć nbytes, to liczba bajtów do nowej linii włącznie (lub EOF, jeśli plik nie kończy się nową linią).

    Zauważ, że funkcja wyraźnie zwraca -1(a nie EOF!), Gdy wystąpi błąd lub osiągnie EOF.

Możesz zauważyć, że bardzo rzadko przeliterujemy słowo „EOF”. Zazwyczaj wykrywamy stan błędu w inny sposób, który jest dla nas od razu interesujący (np. Brak wykonania tak dużej liczby operacji we / wy, jak chcieliśmy). W każdym przykładzie jest jakaś funkcja API, która może nam wyraźnie powiedzieć, że napotkano stan EOF, ale w rzeczywistości nie jest to bardzo przydatna informacja. To jest o wiele więcej szczegółów, niż nam się często zależy. Liczy się to, czy We / Wy się powiodło, bardziej niż to, w jaki sposób zawiodło.

  • Ostatni przykład, który faktycznie pyta o stan EOF: Załóżmy, że masz ciąg znaków i chcesz przetestować, czy reprezentuje on liczbę całkowitą w całości, bez dodatkowych bitów na końcu, z wyjątkiem białych znaków. Przy użyciu iostreams C ++ wygląda to tak:

    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
    }

    Używamy tutaj dwóch wyników. Pierwszym z nich jest isssam obiekt strumienia, aby sprawdzić, czy sformatowana ekstrakcja valuezakończyła się powodzeniem. Ale potem, po zużyciu spacji, wykonujemy kolejną operację I / O / iss.get()i oczekujemy, że zakończy się ona niepowodzeniem jako EOF, co ma miejsce, jeśli cały ciąg został już wykorzystany przez sformatowaną ekstrakcję.

    W standardowej bibliotece C można osiągnąć coś podobnego za pomocą strto*lfunkcji, sprawdzając, czy wskaźnik końca osiągnął koniec ciągu wejściowego.

Odpowiedź

while(!feof)jest błędny, ponieważ testuje coś, co jest nieistotne i nie sprawdza się pod kątem czegoś, co musisz wiedzieć. Powoduje to, że błędnie wykonujesz kod, który zakłada, że ​​uzyskuje dostęp do danych, które zostały pomyślnie odczytane, podczas gdy w rzeczywistości tak się nigdy nie stało.

Kerrek SB
źródło
34
@CiaPan: Nie sądzę, że to prawda. Zarówno C99, jak i C11 pozwalają na to.
Kerrek SB
11
Ale ANSI C nie.
CiaPan
3
@JathanathanMee: Jest zły z wszystkich wymienionych przeze mnie powodów: nie możesz patrzeć w przyszłość. Nie możesz powiedzieć, co się stanie w przyszłości.
Kerrek SB
3
@JonathanMee: Tak, byłoby to właściwe, chociaż zwykle można połączyć to sprawdzenie z operacją (ponieważ większość operacji iostreams zwraca obiekt strumienia, który sam ma konwersję boolowską), i w ten sposób dajesz do zrozumienia, że ​​nie jesteś ignorowanie wartości zwracanej.
Kerrek SB
4
Trzeci akapit jest wyjątkowo mylący / niedokładny dla przyjętej i bardzo pozytywnej odpowiedzi. feof()nie „pyta systemu I / O, czy ma więcej danych”. feof(), zgodnie ze stroną podręcznika (Linux) : „testuje wskaźnik końca pliku dla strumienia wskazywanego przez strumień, zwracając wartość niezerową, jeśli jest ustawiony”. (również wyraźne wezwanie do clearerr()jest jedynym sposobem na zresetowanie tego wskaźnika); Pod tym względem odpowiedź Williama Pursella jest znacznie lepsza.
Arne Vogel,
234

Jest to błędne, ponieważ (przy braku błędu odczytu) wchodzi w pętlę jeszcze raz, niż oczekuje autor. Jeśli wystąpi błąd odczytu, pętla nigdy się nie kończy.

Rozważ następujący kod:

/* 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;
}

Ten program będzie konsekwentnie drukował jeden większy niż liczba znaków w strumieniu wejściowym (przy założeniu braku błędów odczytu). Rozważ przypadek, w którym strumień wejściowy jest pusty:

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

W takim przypadku feof()jest wywoływany przed odczytaniem jakichkolwiek danych, więc zwraca wartość false. Pętla zostaje wprowadzona, fgetc()nazywa się (i zwraca EOF), a liczba jest zwiększana. Następnie feof()jest wywoływana i zwraca wartość true, co powoduje przerwanie pętli.

Dzieje się tak we wszystkich takich przypadkach. feof()nie zwraca prawda aż po odczytu na strumień napotka koniec pliku. Celem feof()NIE jest sprawdzenie, czy następny odczyt dotrze do końca pliku. Celem feof()jest rozróżnienie między błędem odczytu a osiągnięciem końca pliku. Jeśli fread()zwraca 0, musisz użyć feof/, ferroraby zdecydować, czy wystąpił błąd lub czy wszystkie dane zostały wykorzystane. Podobnie, jeśli fgetcpowróci EOF. feof()jest użyteczne tylko wtedy, gdy fread zwrócił zero lub fgetcpowrócił EOF. Zanim to nastąpi, feof()zawsze zwróci 0.

Zawsze konieczne jest sprawdzenie wartości zwracanej odczytu (an fread(), an fscanf()lub an fgetc()) przed wywołaniem feof().

Co gorsza, rozważ przypadek, w którym występuje błąd odczytu. W takim przypadku fgetc()zwraca EOF, feof()zwraca false, a pętla nigdy się nie kończy. We wszystkich przypadkach, w których while(!feof(p))jest używany, wewnątrz pętli musi być co najmniej sprawdzenie ferror(), a przynajmniej warunek while powinien zostać zastąpiony while(!feof(p) && !ferror(p))lub istnieje bardzo realna możliwość nieskończonej pętli, prawdopodobnie wyrzucającej wszelkiego rodzaju śmieci, ponieważ nieprawidłowe dane są przetwarzane.

Podsumowując, chociaż nie mogę z całą pewnością stwierdzić, że nigdy nie ma sytuacji, w której napisanie „ while(!feof(f))” byłoby poprawne semantycznie (chociaż musi być jeszcze jedno sprawdzenie wewnątrz pętli z przerwą, aby uniknąć nieskończonej pętli przy błędzie odczytu ) jest tak, że prawie na pewno zawsze się myli. I nawet jeśli kiedykolwiek pojawił się przypadek, w którym byłby poprawny, jest tak idiomatycznie zły, że nie byłby to właściwy sposób na napisanie kodu. Każdy, kto zobaczy ten kod, powinien natychmiast zawahać się i powiedzieć „to błąd”. I ewentualnie uderzyć autora (chyba że autor jest twoim szefem, w takim przypadku zaleca się dyskrecję).

William Pursell
źródło
7
Jasne, że to źle - ale poza tym nie jest „brzydko brzydkie”.
nobar
89
Powinieneś dodać przykład poprawnego kodu, ponieważ wyobrażam sobie, że wiele osób przyjdzie tutaj, szukając szybkiej poprawki.
jleahy
6
@Thomas: Nie jestem ekspertem od C ++, ale uważam, że file.eof () zwraca efektywnie ten sam wynik feof(file) || ferror(file), więc jest bardzo różny. Ale to pytanie nie ma dotyczyć C ++.
William Pursell
6
@ m-ric też nie jest poprawne, ponieważ nadal będziesz próbował przetworzyć odczyt, który się nie powiódł.
Mark Ransom,
4
to jest prawdziwa poprawna odpowiedź. feof () służy do poznania wyniku poprzedniej próby odczytu. Dlatego prawdopodobnie nie chcesz używać go jako warunku przerwania pętli. +1
Jack
63

Nie, nie zawsze jest źle. Jeśli warunek pętli jest „gdy nie próbowaliśmy odczytać końca pliku”, użyj while (!feof(f)). Nie jest to jednak częsty warunek pętli - zwykle chcesz przetestować coś innego (na przykład „czy mogę przeczytać więcej”). while (!feof(f))nie jest źle, jest po prostu źle użyte .

Erik
źródło
1
Zastanawiam się ... f = fopen("A:\\bigfile"); while (!feof(f)) { /* remove diskette */ }lub (zamierzam to przetestować)f = fopen(NETWORK_FILE); while (!feof(f)) { /* unplug network cable */ }
pmg
1
@pmg: Jak powiedziano, „nie jest to zwykły warunek pętli” hehe. Naprawdę nie mogę wymyślić żadnego przypadku, którego potrzebowałem, zwykle interesuje mnie „czy mogę przeczytać to, co chciałem” ze wszystkimi tego, co wiąże się z obsługą błędów
Erik
@pmg: Jak powiedziano, rzadko chceszwhile(!eof(f))
Erik
9
Dokładniej, warunek jest następujący: „dopóki nie próbowaliśmy czytać poza końcem pliku i nie wystąpił błąd odczytu”, feofnie chodzi o wykrywanie końca pliku; chodzi o określenie, czy odczyt był krótki z powodu błędu lub z powodu wyczerpania danych wejściowych.
William Pursell
35

feof()wskazuje, czy ktoś próbował odczytać poza końcem pliku. Oznacza to, że ma niewielki efekt predykcyjny: jeśli jest to prawda, masz pewność, że następna operacja wejścia nie powiedzie się (nie jesteś pewien, że poprzednia nie powiodła się BTW), ale jeśli jest to fałsz, nie jesteś pewien, operacja się powiedzie. Ponadto operacje wprowadzania mogą się nie powieść z innych powodów niż koniec pliku (błąd formatu sformatowanego wejścia, błąd samej operacji we / wy - awaria dysku, przekroczenie limitu czasu sieci - dla wszystkich rodzajów danych wejściowych), więc nawet jeśli możesz przewidywać koniec pliku (i każdy, kto próbował zaimplementować Ada one, który jest predykcyjny, powie ci, że może on być skomplikowany, jeśli potrzebujesz pominąć spacje, i że ma to niepożądane skutki na urządzeniach interaktywnych - czasami wymuszając wprowadzanie następnego linia przed rozpoczęciem obsługi poprzedniej),

Tak więc poprawnym idiomem w C jest zapętlenie z sukcesem operacji we / wy jako warunek pętli, a następnie przetestowanie przyczyny niepowodzenia. Na przykład:

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) */
}
AProgrammer
źródło
2
Dotarcie do końca pliku nie jest błędem, więc pytam o frazę „operacje wprowadzania mogą się nie powieść z innych powodów niż koniec pliku”.
William Pursell
@WilliamPursell, dotarcie do eof niekoniecznie jest błędem, ale niemożność wykonania operacji wprowadzania z powodu eof to jeden. I w C niemożliwe jest niezawodne wykrycie eof bez niepowodzenia operacji wejścia.
AProgrammer
Ostatnia Zgadzam elsenie możliwe sizeof(line) >= 2, a fgets(line, sizeof(line), file)jednak możliwe patologiczny size <= 0i fgets(line, size, file). Może nawet możliwe z sizeof(line) == 1.
chux - Przywróć Monikę
1
Cała ta gadka o „wartości predykcyjnej”… Nigdy o tym nie myślałem. W moim świecie feof(f)NIC NIE PRZEWIDZIE. Stwierdza, że ​​POPRZEDNIA operacja dotarła do końca pliku. Nic dodać nic ująć. A jeśli nie było poprzedniej operacji (właśnie ją otworzyłem), nie zgłasza końca pliku, nawet jeśli plik był pusty na początek. Tak więc, oprócz wyjaśnienia dotyczącego współbieżności w innej odpowiedzi powyżej, nie sądzę, aby istniał jakiś powód, aby nie zapętlać feof(f).
BitTickler
@AProgrammer: A „czytać aż do n bajtów” wniosek, że plony zera, czy ze względu na „stałe” EOF lub ponieważ nie ma więcej danych jest dostępna jeszcze nie jest błąd. Chociaż feof () może nie wiarygodnie przewidzieć, że przyszłe żądania przyniosą dane, może wiarygodnie wskazać, że przyszłe żądania nie będą . Być może powinna istnieć funkcja statusu, która wskazywałaby: „Jest prawdopodobne, że przyszłe żądania odczytu odniosą sukces”, z semantyką, że po odczytaniu do końca zwykłego pliku, wysokiej jakości implementacja powinna powiedzieć, że przyszłe odczyty prawdopodobnie nie powiodą się bez jakiegoś powodu wierzą, że mogą .
supercat
0

feof()nie jest bardzo intuicyjny. Moim bardzo skromnym zdaniem stan FILEkońca pliku powinien zostać ustawiony na, truejeśli jakakolwiek operacja odczytu spowoduje osiągnięcie końca pliku. Zamiast tego musisz ręcznie sprawdzić, czy do końca pliku został osiągnięty po każdej operacji odczytu. Na przykład coś takiego będzie działać, jeśli odczytujesz z pliku tekstowego przy użyciu 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;
}

Byłoby wspaniale, gdyby zamiast tego działało coś takiego:

#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;
}
Scott Deagan
źródło
1
printf("%c", fgetc(in));? To nieokreślone zachowanie. fgetc()zwraca intnie char.
Andrew Henle
Wydaje mi się, że standardowy idiom while( (c = getchar()) != EOF)to bardzo „coś takiego”.
William Pursell
while( (c = getchar()) != EOF)działa na jednym z moich komputerów z systemem GNU C 10.1.0, ale nie działa na moim Raspberry Pi 4 z systemem GNU C 9.3.0. W moim RPi4 nie wykrywa końca pliku i po prostu działa.
Scott Deagan
@AndrewHenle Masz rację! Przejście char cdo int cpracy! Dzięki!!
Scott Deagan