Warum ist "while (! Feof (Datei))" immer falsch?

573

Ich habe in letzter Zeit in vielen Posts Leute gesehen, die versucht haben, solche Dateien zu lesen:

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

Was ist los mit dieser Schleife?

William Pursell
quelle

Antworten:

453

Ich möchte eine abstrakte Perspektive auf hoher Ebene bieten.

Parallelität und Gleichzeitigkeit

E / A-Vorgänge interagieren mit der Umgebung. Die Umgebung ist nicht Teil Ihres Programms und nicht unter Ihrer Kontrolle. Die Umgebung existiert wirklich "gleichzeitig" mit Ihrem Programm. Wie bei allen gleichzeitigen Dingen sind Fragen zum "aktuellen Zustand" nicht sinnvoll: Es gibt kein Konzept der "Gleichzeitigkeit" zwischen gleichzeitigen Ereignissen. Viele Eigenschaften von Staat einfach nicht existieren gleichzeitig.

Lassen Sie mich das präzisieren: Angenommen, Sie möchten fragen: "Haben Sie mehr Daten?". Sie können dies von einem gleichzeitigen Container oder von Ihrem E / A-System verlangen. Aber die Antwort ist im Allgemeinen nicht handlungsfähig und daher bedeutungslos. Was ist, wenn der Container "Ja" sagt? Wenn Sie versuchen zu lesen, enthält er möglicherweise keine Daten mehr. Wenn die Antwort "Nein" lautet, sind zum Zeitpunkt des Leseversuchs möglicherweise Daten eingetroffen. Die Schlussfolgerung ist, dass es einfach gibtKeine Eigenschaft wie "Ich habe Daten", da Sie auf eine mögliche Antwort nicht sinnvoll reagieren können. (Bei gepufferten Eingaben ist die Situation etwas besser, da Sie möglicherweise ein "Ja, ich habe Daten" erhalten, das eine Art Garantie darstellt, aber Sie müssten immer noch in der Lage sein, den umgekehrten Fall zu behandeln. Und bei der Ausgabe die Situation ist sicherlich genauso schlimm wie ich beschrieben habe: Man weiß nie, ob diese Festplatte oder dieser Netzwerkpuffer voll ist.)

So schließen wir , dass es unmöglich ist, und in der Tat un vernünftig , ein I / O - System zu fragen , ob es sein wird , kann eine E / A - Operation auszuführen. Die einzige Möglichkeit, mit ihm zu interagieren (genau wie mit einem gleichzeitigen Container), besteht darin , den Vorgang zu versuchen und zu überprüfen, ob er erfolgreich war oder fehlgeschlagen ist. In dem Moment, in dem Sie mit der Umgebung interagieren, können Sie dann und nur dann wissen, ob die Interaktion tatsächlich möglich war, und an diesem Punkt müssen Sie sich zur Durchführung der Interaktion verpflichten. (Dies ist ein "Synchronisationspunkt", wenn Sie so wollen.)

EOF

Jetzt kommen wir zu EOF. EOF ist die Antwort, die Sie von einem versuchten E / A-Vorgang erhalten. Dies bedeutet, dass Sie versucht haben, etwas zu lesen oder zu schreiben, dabei jedoch keine Daten gelesen oder geschrieben haben und stattdessen das Ende der Eingabe oder Ausgabe festgestellt wurde. Dies gilt im Wesentlichen für alle E / A-APIs, unabhängig davon, ob es sich um die C-Standardbibliothek, C ++ - Iostreams oder andere Bibliotheken handelt. Solange die E / A-Vorgänge erfolgreich sind, können Sie einfach nicht wissen, ob weitere zukünftige Vorgänge erfolgreich sein werden. Sie müssen immer zuerst den Vorgang versuchen und dann auf Erfolg oder Misserfolg reagieren.

Beispiele

Beachten Sie in jedem der Beispiele sorgfältig, dass wir zuerst die E / A-Operation versuchen und dann das Ergebnis verwenden, wenn es gültig ist. Beachten Sie außerdem, dass wir immer das Ergebnis der E / A-Operation verwenden müssen, obwohl das Ergebnis in jedem Beispiel unterschiedliche Formen und Formen annimmt.

  • C stdio, aus einer Datei lesen:

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

    Das Ergebnis, das wir verwenden müssen, ist ndie Anzahl der gelesenen Elemente (die möglicherweise nur Null betragen).

  • C stdio , scanf:

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

    Das Ergebnis, das wir verwenden müssen, ist der Rückgabewert von scanf, die Anzahl der konvertierten Elemente.

  • C ++, iostreams formatierte Extraktion:

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

    Das Ergebnis, das wir verwenden müssen, ist std::cinselbst, das in einem booleschen Kontext ausgewertet werden kann und uns sagt, ob sich der Stream noch im good()Status befindet.

  • C ++, iostreams getline:

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

    Das Ergebnis, das wir verwenden müssen, ist wieder std::cinwie zuvor.

  • POSIX, write(2)um einen Puffer zu leeren:

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

    Das Ergebnis, das wir hier verwenden, ist kdie Anzahl der geschriebenen Bytes. Der Punkt hier ist, dass wir nur wissen können, wie viele Bytes nach dem Schreibvorgang geschrieben wurden.

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

    Das Ergebnis, das wir verwenden müssen, ist nbytesdie Anzahl der Bytes bis einschließlich der neuen Zeile (oder EOF, wenn die Datei nicht mit einer neuen Zeile endete).

    Beachten Sie, dass die Funktion explizit -1(und nicht EOF!) Zurückgibt, wenn ein Fehler auftritt oder EOF erreicht.

Sie werden feststellen, dass wir das eigentliche Wort "EOF" sehr selten buchstabieren. Normalerweise erkennen wir den Fehlerzustand auf eine andere Weise, die für uns unmittelbar interessanter ist (z. B. wenn nicht so viele E / A-Vorgänge ausgeführt werden, wie wir es gewünscht hatten). In jedem Beispiel gibt es eine API-Funktion, die uns explizit mitteilen könnte, dass der EOF-Status aufgetreten ist, aber dies ist in der Tat keine besonders nützliche Information. Es ist viel mehr ein Detail, als uns oft wichtig ist. Entscheidend ist, ob die E / A erfolgreich war, mehr als wie sie fehlgeschlagen ist.

  • Ein letztes Beispiel, das den EOF-Status tatsächlich abfragt: Angenommen, Sie haben eine Zeichenfolge und möchten testen, ob sie eine Ganzzahl in ihrer Gesamtheit darstellt, ohne zusätzliche Bits am Ende außer Leerzeichen. Mit C ++ iostreams geht es so:

    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
    }
    

    Wir verwenden hier zwei Ergebnisse. Das erste ist issdas Stream-Objekt selbst, um zu überprüfen, ob die formatierte Extraktion valueerfolgreich war. Nachdem wir jedoch auch Leerzeichen verbraucht haben, führen wir eine weitere E / A / Operation aus iss.get()und erwarten, dass diese als EOF fehlschlägt. Dies ist der Fall, wenn die gesamte Zeichenfolge bereits von der formatierten Extraktion verbraucht wurde.

    In der C-Standardbibliothek können Sie mit den strto*lFunktionen etwas Ähnliches erreichen, indem Sie überprüfen, ob der Endzeiger das Ende der Eingabezeichenfolge erreicht hat.

Die Antwort

while(!feof)ist falsch, weil es auf etwas testet, das irrelevant ist und nicht auf etwas testet, das Sie wissen müssen. Das Ergebnis ist, dass Sie fälschlicherweise Code ausführen, der davon ausgeht, dass er auf Daten zugreift, die erfolgreich gelesen wurden, obwohl dies tatsächlich nie geschehen ist.

Kerrek SB
quelle
34
@CiaPan: Ich denke nicht, dass das stimmt. Sowohl C99 als auch C11 erlauben dies.
Kerrek SB
11
ANSI C jedoch nicht.
CiaPan
3
@ JonathanMee: Es ist schlecht aus all den Gründen, die ich erwähne: Sie können nicht in die Zukunft schauen. Sie können nicht sagen, was in Zukunft passieren wird.
Kerrek SB
3
@JonathanMee: Ja, das wäre angemessen, obwohl Sie diese Prüfung normalerweise in der Operation kombinieren können (da die meisten iostreams-Operationen das Stream-Objekt zurückgeben, das selbst eine boolesche Konvertierung aufweist), und auf diese Weise machen Sie deutlich, dass dies nicht der Fall ist Rückgabewert ignorieren.
Kerrek SB
4
Der dritte Absatz ist bemerkenswert irreführend / ungenau für eine akzeptierte und hoch bewertete Antwort. feof()fragt nicht "das E / A-System, ob es mehr Daten hat". feof()Laut der (Linux-) Manpage : "Testet den Indikator für das Dateiende für den Stream, auf den der Stream zeigt, und gibt ungleich Null zurück, wenn er gesetzt ist." (auch ein expliziter Aufruf von clearerr()ist die einzige Möglichkeit, diesen Indikator zurückzusetzen); In dieser Hinsicht ist William Pursells Antwort viel besser.
Arne Vogel
234

Es ist falsch, weil es (ohne Lesefehler) einmal mehr in die Schleife eintritt, als der Autor erwartet. Wenn ein Lesefehler auftritt, wird die Schleife niemals beendet.

Betrachten Sie den folgenden Code:

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

Dieses Programm druckt konsistent ein Zeichen, das größer als die Anzahl der Zeichen im Eingabestream ist (unter der Annahme, dass keine Lesefehler vorliegen). Betrachten Sie den Fall, in dem der Eingabestream leer ist:

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

In diesem Fall feof()wird aufgerufen, bevor Daten gelesen wurden, sodass false zurückgegeben wird. Die Schleife wird eingegeben, fgetc()aufgerufen (und zurückgegeben EOF) und die Anzahl wird erhöht. Dann feof()wird aufgerufen und gibt true zurück, wodurch die Schleife abgebrochen wird.

Dies geschieht in all diesen Fällen. feof()gibt true erst zurück, nachdem ein Lesevorgang im Stream das Dateiende erreicht hat. Der Zweck von feof()ist NICHT zu überprüfen, ob der nächste Lesevorgang das Ende der Datei erreicht. Der Zweck von feof()besteht darin, zwischen einem Lesefehler und dem Ende der Datei zu unterscheiden. Wenn fread()0 zurückgegeben wird, müssen Sie mit feof/ ferrorentscheiden, ob ein Fehler aufgetreten ist oder ob alle Daten verbraucht wurden. Ähnliches gilt, wenn fgetczurückgegeben wird EOF. feof()nur ist nützlich , nachdem fread Null oder zurückgekehrt fgetcist zurückgekehrt EOF. Bevor dies geschieht, feof()wird immer 0 zurückgegeben.

Es ist immer notwendig, den Rückgabewert eines Lesevorgangs (entweder ein fread()oder ein fscanf()oder ein fgetc()) vor dem Aufruf zu überprüfen feof().

Betrachten Sie noch schlimmer den Fall, in dem ein Lesefehler auftritt. In diesem Fall wird fgetc()return zurückgegeben EOF, feof()false zurückgegeben, und die Schleife wird niemals beendet. In allen Fällen, in denen while(!feof(p))verwendet wird, muss mindestens eine Überprüfung innerhalb der Schleife durchgeführt werden ferror(), oder zumindest sollte die while-Bedingung durch ersetzt werden, while(!feof(p) && !ferror(p))oder es besteht die sehr reale Möglichkeit einer Endlosschleife, die wahrscheinlich alle Arten von Müll ausspuckt ungültige Daten werden verarbeitet.

So in der Zusammenfassung, obwohl ich kann nicht Staat mit Sicherheit , dass es nie eine Situation , in der es semantisch Schreib richtig sein kann „ while(!feof(f))“ (obwohl es muss mit einer Pause erneut die Kontrolle innerhalb der Schleife sein , eine Endlosschleife auf einem Lesefehler zu vermeiden ) ist es so, dass es mit ziemlicher Sicherheit immer falsch ist. Und selbst wenn jemals ein Fall auftauchte, in dem es richtig wäre, ist es so idiomatisch falsch, dass es nicht der richtige Weg wäre, den Code zu schreiben. Jeder, der diesen Code sieht, sollte sofort zögern und sagen: "Das ist ein Fehler." Und möglicherweise den Autor schlagen (es sei denn, der Autor ist Ihr Chef. In diesem Fall wird Diskretion empfohlen.)

William Pursell
quelle
7
Sicher ist es falsch - aber abgesehen davon ist es nicht "krass hässlich".
Nobar
89
Sie sollten ein Beispiel für korrekten Code hinzufügen, da ich mir vorstelle, dass viele Leute hierher kommen werden, um eine schnelle Lösung zu finden.
Jleahy
6
@Thomas: Ich bin kein C ++ - Experte, aber ich glaube, dass file.eof () effektiv das gleiche Ergebnis wie zurückgibt feof(file) || ferror(file), also ist es sehr unterschiedlich. Diese Frage soll jedoch nicht auf C ++ anwendbar sein.
William Pursell
6
@ m-ric das stimmt auch nicht, weil du immer noch versuchst, einen fehlgeschlagenen Lesevorgang zu verarbeiten.
Mark Ransom
4
Dies ist die richtige Antwort. feof () wird verwendet, um das Ergebnis des vorherigen Leseversuchs zu ermitteln. Daher möchten Sie es wahrscheinlich nicht als Schleifenunterbrechungsbedingung verwenden. +1
Jack
63

Nein, das ist nicht immer falsch. Wenn Ihre Schleifenbedingung "während wir nicht versucht haben, das vergangene Dateiende zu lesen" lautet, verwenden Sie while (!feof(f)). Dies ist jedoch keine übliche Schleifenbedingung - normalerweise möchten Sie auf etwas anderes testen (z. B. "Kann ich mehr lesen"). while (!feof(f))nicht falsch, es einfach ist gebrauchte falsch.

Erik
quelle
1
Ich frage mich ... f = fopen("A:\\bigfile"); while (!feof(f)) { /* remove diskette */ }oder (werde das testen)f = fopen(NETWORK_FILE); while (!feof(f)) { /* unplug network cable */ }
pmg
1
@pmg: Wie gesagt, "keine übliche Schleifenbedingung" hehe. Ich kann mir keinen Fall vorstellen, in dem ich ihn gebraucht habe. Normalerweise interessiert mich "könnte ich lesen, was ich wollte" mit allem, was mit Fehlerbehandlung zu tun hat
Erik
@pmg: Wie gesagt, Sie wollen seltenwhile(!eof(f))
Erik
9
Genauer gesagt handelt es sich bei der Bedingung "Während wir nicht versucht haben, über das Ende der Datei hinaus zu lesen und es gab keinen Lesefehler" feofnicht darum, das Ende der Datei zu erkennen. Es geht darum festzustellen, ob ein Lesevorgang aufgrund eines Fehlers kurz war oder weil die Eingabe erschöpft ist.
William Pursell
35

feof()zeigt an, ob versucht wurde, über das Dateiende hinaus zu lesen. Das bedeutet, dass es wenig prädiktive Wirkung hat: Wenn es wahr ist, sind Sie sicher, dass die nächste Eingabeoperation fehlschlägt (Sie sind nicht sicher, ob die vorherige BTW fehlgeschlagen ist), aber wenn es falsch ist, sind Sie nicht sicher, ob die nächste Eingabe fehlschlägt Operation wird erfolgreich sein. Darüber hinaus können Eingabevorgänge aus anderen Gründen als dem Dateiende fehlschlagen (ein Formatfehler für formatierte Eingaben, ein reiner E / A-Fehler - Festplattenfehler, Netzwerk-Timeout - für alle Eingabearten), selbst wenn Sie dies vorhersagen können Das Ende der Datei (und jeder, der versucht hat, Ada One zu implementieren, was voraussagend ist, wird Ihnen sagen, dass es komplex sein kann, wenn Sie Leerzeichen überspringen müssen, und dass es unerwünschte Auswirkungen auf interaktive Geräte hat - manchmal erzwingt es die Eingabe des nächsten Zeile vor Beginn der Behandlung der vorherigen),

Die korrekte Redewendung in C besteht also darin, eine Schleife mit dem Erfolg der E / A-Operation als Schleifenbedingung durchzuführen und dann die Fehlerursache zu testen. Zum Beispiel:

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) */
}
Ein Programmierer
quelle
2
Das Ende einer Datei zu erreichen ist kein Fehler, daher stelle ich die Formulierung "Eingabevorgänge können aus anderen Gründen als dem Dateiende fehlschlagen" in Frage.
William Pursell
@WilliamPursell, das Erreichen des eof ist nicht unbedingt ein Fehler, aber eine Eingabeoperation aufgrund von eof nicht ausführen zu können, ist einer. Und es ist in C unmöglich, den eof zuverlässig zu erfassen, ohne dass eine Eingabeoperation fehlgeschlagen ist.
AProgrammer
Einverstanden zuletzt elsenicht möglich mit sizeof(line) >= 2und fgets(line, sizeof(line), file)aber möglich mit pathologischen size <= 0und fgets(line, size, file). Vielleicht sogar möglich mit sizeof(line) == 1.
chux
1
All diese "prädiktiven Werte" sprechen ... Ich habe nie so darüber nachgedacht. In meiner Welt feof(f)sagt nichts voraus. Es heißt, dass eine VORHERIGE Operation das Ende der Datei erreicht hat. Nicht mehr, nicht weniger. Und wenn es keinen vorherigen Vorgang gab (nur geöffnet), wird das Dateiende nicht gemeldet, selbst wenn die Datei zu Beginn leer war. Abgesehen von der Erklärung der Parallelität in einer anderen Antwort oben glaube ich nicht, dass es einen Grund gibt, nicht weiterzumachen feof(f).
BitTickler
@AProgrammer: A „lesen von bis zu N Bytes“ -Anfrage , dass Null ergibt, sei es wegen eines „permanenten“ EOF oder weil keine weiteren Daten verfügbar ist noch , kein Fehler ist. Während feof () möglicherweise nicht zuverlässig vorhersagt, dass zukünftige Anforderungen Daten liefern, zeigt es möglicherweise zuverlässig an, dass zukünftige Anforderungen dies nicht tun . Vielleicht sollte es eine Statusfunktion geben, die anzeigt, dass "Es ist plausibel, dass zukünftige Leseanforderungen erfolgreich sind", mit der Semantik, dass nach dem Lesen bis zum Ende einer normalen Datei eine Qualitätsimplementierung besagen sollte, dass zukünftige Lesevorgänge ohne Grund wahrscheinlich nicht erfolgreich sind glaube sie könnten .
Supercat
0

feof()ist nicht sehr intuitiv. Meiner sehr bescheidenen Meinung nach sollte der FILEStatus "Dateiende" auf "" gesetzt werden, truewenn eine Leseoperation dazu führt, dass das Dateiende erreicht wird. Stattdessen müssen Sie nach jedem Lesevorgang manuell prüfen, ob das Dateiende erreicht ist. So etwas funktioniert beispielsweise, wenn Sie aus einer Textdatei lesen mit 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;
}

Es wäre großartig, wenn so etwas stattdessen funktionieren würde:

#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
quelle
1
printf("%c", fgetc(in));? Das ist undefiniertes Verhalten. fgetc()kehrt zurück int, nicht char.
Andrew Henle
Es scheint mir, dass die Standardsprache while( (c = getchar()) != EOF)sehr viel "so etwas" ist.
William Pursell
while( (c = getchar()) != EOF)Funktioniert auf einem meiner Desktops mit GNU C 10.1.0, schlägt jedoch auf meinem Raspberry Pi 4 mit GNU C 9.3.0 fehl. Auf meinem RPi4 erkennt es das Dateiende nicht und macht einfach weiter.
Scott Deagan vor
@ AndrewHenle Du hast recht! Wechsel char czu int cWerken! Vielen Dank!!
Scott Deagan