Why is " while (!feof (plik))" zawsze źle?

Widziałem ostatnio ludzi próbujących czytać takie pliki w wielu postach.

kod

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

int main(int argc, char **argv)
{
    char * path = argc > 1 ? argv[1] : "input.txt";

    FILE * fp = fopen(path, "r");
    if( fp == NULL ) {
        perror(path);
        return EXIT_FAILURE;
    }

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

Co jest nie tak z tą pętlą while( !feof(fp))?

Author: William Pursell, 2011-03-25

5 answers

Chciałbym przedstawić abstrakcyjną, wysokopoziomową perspektywę.

Współbieżność i jednoczesność

Operacje wejścia/Wyjścia oddziałują na środowisko. Środowisko nie jest częścią twojego programu i nie jest pod twoją kontrolą. Środowisko naprawdę istnieje "równocześnie" z Twoim programem. Podobnie jak w przypadku wszystkich rzeczy współbieżnych, pytania o "obecny stan" nie mają sensu: nie ma pojęcia "symultaniczności" między zdarzeniami współbieżnymi. Wiele właściwości Państwa po prostu nie exist

Doprecyzuję to: przypuśćmy, że chcesz zapytać :" czy masz więcej danych". Możesz zapytać o to współbieżny kontener lub Twój system We/Wy. Ale odpowiedź jest na ogół nie do zastosowania, a więc bez znaczenia. Co z tego, jeśli kontener powie "tak" – zanim spróbujesz czytać, może nie mieć już danych. Podobnie, jeśli odpowiedź brzmi "nie", do czasu próby czytania, dane mogły już dotrzeć. Wniosek jest taki, że po prostu jest nie właściwości takie jak "mam dane", ponieważ nie można działać w sposób znaczący w odpowiedzi na jakąkolwiek możliwą odpowiedź. (Sytuacja jest nieco lepsza z buforowanym wejściem, gdzie można by przypuszczać, że "tak, mam dane" stanowi jakąś gwarancję, ale nadal musiałbyś być w stanie poradzić sobie z przeciwną sprawą. A z wyjściem sytuacja jest z pewnością tak zła, jak opisałem: nigdy nie wiadomo, czy dysk lub bufor sieciowy jest pełny.)

Wnioskujemy więc, że jest to niemożliwe, i w rzeczywistości unrozsądne, aby zapytać system we/wy, czy będzie on w stanie wykonać operację we/wy. Jedynym możliwym sposobem na interakcję z nim (tak jak z współbieżnym kontenerem) jest próba operacji i sprawdzenie, czy się powiodła, czy nie. W tym momencie, gdy wchodzicie w interakcję z otoczeniem, wtedy i tylko wtedy możecie wiedzieć, czy interakcja była rzeczywiście możliwa, i w tym momencie musicie zobowiązać się do przeprowadzenia interakcji. (To "punkt synchronizacji", jeśli wolisz.)

EOF

Teraz mamy EOF. EOF jest odpowiedzią otrzymywaną z próby operacji wejścia/Wyjścia. Oznacza to, że próbowałeś coś odczytać lub napisać, ale nie udało Ci się odczytać lub zapisać żadnych danych, a zamiast tego napotkano koniec wejścia lub wyjścia. Dotyczy to zasadniczo wszystkich interfejsów API we/wy, niezależnie od tego, czy jest to biblioteka standardowa C, iostreams C++, czy inne biblioteki. Dopóki operacje wejścia / Wyjścia sukces, po prostu nie możesz wiedzieć czy dalsze, przyszłe operacje odniosą sukces. Musisz zawsze najpierw spróbować operacji, a następnie odpowiedzieć na sukces lub porażkę.

Przykłady

W każdym z przykładów zwróć uwagę, że najpierw spróbujemy operacji wejścia/wyjścia i następnie pochłonie wynik, jeśli jest poprawny. Zauważ ponadto, że zawsze musimy użyć wyniku operacji We/Wy, chociaż wynik przybiera różne kształty i formularze 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; }
    }
    

    Wynikiem, którego musimy użyć, jest n, liczba odczytanych elementów (która może wynosić nawet zero).

  • C stdio, scanf:

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

    Wynikiem, którego musimy użyć, jest wartość zwracana scanf, liczba przekonwertowanych elementów.

  • C++, iostreams sformatowany ekstrakcja:

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

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

  • C++, iostreams getline:

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

    Wynik, którego musimy użyć, jest ponownie std::cin, tak jak wcześniej.

  • POSIX, write(2) do spłukiwania bufora:

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

    Wynikiem, którego tutaj używamy, jest k, liczba zapisanych bajtów. Chodzi o to, że możemy wiedzieć tylko ile bajtów zostało zapisanych 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);
    

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

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

Możesz zauważyć, że bardzo rzadko piszemy prawdziwe słowo "EOF". Zwykle wykrywamy stan błędu w inny sposób, który jest dla nas bardziej interesujący (np. brak wykonania tak dużej ilości We / Wy, jak sobie tego życzyliśmy). W każdym przykładzie jest jakaś funkcja API, która może nam wyraźnie powiedzieć, że stan EOF został napotkany, ale w rzeczywistości nie jest to zbyt przydatna informacja. To o wiele więcej szczegółów, niż nam często zależy. Najważniejsze jest to, czy I/O się powiodło się, bardziej niż jak się nie powiodło.

  • Ostatni przykład, który faktycznie zapytuje stan EOF: Załóżmy, że masz ciąg znaków i chcesz sprawdzić, czy reprezentuje on liczba całkowita w całości, bez dodatkowych bitów na końcu, z wyjątkiem białych znaków. Używając C++ iostreams, 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 dwóch wyników tutaj. Pierwszym z nich jest iss, sam obiekt stream, aby sprawdzić, czy sformatowana ekstrakcja do value powiodła się. Ale potem, po zużyciu również whitespace, wykonujemy kolejną operację we / wy / iss.get() i spodziewamy się, że zakończy się ona niepowodzeniem jako EOF, co ma miejsce, jeśli cały łańcuch został już zużyty przez sformatowaną ekstrakcję.

    W C biblioteka standardowa możesz osiągnąć coś podobnego za pomocą funkcji strto*l, sprawdzając, czy wskaźnik końcowy osiągnął koniec łańcucha wejściowego.

Odpowiedź

while(!eof) jest źle, ponieważ testuje na coś, co jest nieistotne i nie testuje na coś, co musisz wiedzieć. W rezultacie 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 nigdy się to nie zdarzyło.

 364
Author: Kerrek SB,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-24 15:58:34

Jest źle, ponieważ (w przypadku braku błędu odczytu) wchodzi do pętli jeszcze raz, niż autor się spodziewa. 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! */
        (void) 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ł o jeden większy niż liczba znaków w strumieniu wejściowym (zakładając, że nie ma błędów odczytu). Rozważmy przypadek, w którym strumień wejściowy jest pusty:

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

W tym przypadku, feof() jest wywoływana przed odczytaniem jakichkolwiek danych, więc zwraca false. Pętla jest wprowadzana, wywoływana jest fgetc() (i zwraca EOF), a liczba jest zwiększana. Następnie wywołane jest feof() i zwraca true, powodując przerwanie pętli.

Dzieje się tak we wszystkich takich przypadkach. feof() nie zwraca true dopóki po odczyt strumienia napotka koniec pliku. Celem feof() nie jest sprawdzenie, czy następny odczyt dotrze do końca pliku. Celem feof() jest rozróżnienie pomiędzy błędem odczytu a osiągnięciem końca pliku. Jeśli fread() zwróci 0, musisz użyć feof/ferror zdecydować. Podobnie jeśli fgetc zwraca EOF. {[2] } jest użyteczne tylko Po tym, jak fread zwrócił zero lub fgetc zwrócił EOF. Zanim to nastąpi, feof() zawsze zwróci 0.

Przed wywołaniem feof() należy zawsze sprawdzić zwracaną wartość read (albo an fread(), albo an fscanf(), albo an fgetc()).

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

Tak więc, podsumowując, chociaż nie mogę stwierdzić z całą pewnością, że nigdy nie ma sytuacji, w której pisanie może być semantycznie poprawne "[28]}" (chociaż tam musi być kolejna Kontrola wewnątrz pętli z przerwą, aby uniknąć nieskończonej pętli na błąd odczytu), jest to przypadek, że prawie na pewno zawsze jest źle. I nawet jeśli kiedykolwiek powstał przypadek, w którym byłby poprawny, jest to tak idiomatycznie błędne, ż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 spoliczkować autora (chyba że autor jest twoim szefem w takim przypadku dyskrecja jest doradzam.)

 199
Author: William Pursell,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-03-16 23:54:25

Nie zawsze jest źle. Jeśli warunek pętli to "while we haven' t tried to read past end of file", to używasz while (!feof(f)). Nie jest to jednak częsty warunek pętli - Zwykle chcesz przetestować coś innego (na przykład "can I read more"). while (!feof(f)) nie jest źle, jest po prostu używane źle.

 57
Author: Erik,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-03-25 11:49:12

Feof() wskazuje, czy ktoś próbował odczytać koniec pliku. Oznacza to, że ma niewielki efekt predykcyjny: jeśli to prawda, jesteś pewien, że następna operacja wejściowa się nie powiedzie( nie jesteś pewien, że poprzednia nie powiodła się BTW), ale jeśli jest fałszywa, nie jesteś pewien, czy następna operacja wejściowa się powiedzie. Co więcej, operacje wejścia mogą się nie udać z innych powodów niż koniec pliku( błąd formatu dla sformatowanego wejścia, czysta awaria IO -- awaria dysku, limit czasu sieci -- dla wszystkich rodzajów wejść), więc nawet jeśli możesz być predykcyjny co do końca pliku (a każdy, kto próbował zaimplementować Ada one, która jest predykcyjna, powie Ci, że może być złożona, jeśli musisz pominąć spacje i że ma niepożądane skutki na urządzeniach interaktywnych-czasami wymuszając wejście następnej linii przed rozpoczęciem obsługi poprzedniej), będziesz musiał być w stanie obsłużyć awarię.

Więc prawidłowym idiomem w C jest pętla z operacją IO jako warunek pętli, a następnie test przyczyną porażki. 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) */
}
 27
Author: AProgrammer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-02-10 10:22:04

Świetna odpowiedź, właśnie zauważyłem to samo, ponieważ próbowałem zrobić taką pętlę. Tak, to jest złe w tym scenariuszu, ale jeśli chcesz mieć pętlę, która wdzięcznie kończy się na EOF, jest to dobry sposób, aby to zrobić: {]}

#include <stdio.h>
#include <sys/stat.h>
int main(int argc, char *argv[])
{
  struct stat buf;
  FILE *fp = fopen(argv[0], "r");
  stat(filename, &buf);
  while (ftello(fp) != buf.st_size) {
    (void)fgetc(fp);
  }
  // all done, read all the bytes
}
 9
Author: tesch1,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-06-03 02:47:25