680 Stimmen

Warum ist "while( !feof(file) )" immer falsch?

Was ist falsch an der Verwendung von feof() um eine Leseschleife zu steuern? Zum Beispiel:

#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 an dieser Schleife falsch?

552voto

Kerrek SB Punkte 445528

TL;DR

while(!feof) ist falsch, weil es auf etwas prüft, das nicht relevant ist, und nicht auf etwas prüft, das man wissen muss. Das Ergebnis ist, dass Sie fälschlicherweise einen Code ausführen, der davon ausgeht, dass er auf erfolgreich gelesene Daten zugreift, obwohl dies in Wirklichkeit nie geschehen ist.

Ich möchte eine abstrakte, übergeordnete Perspektive bieten. Lesen Sie also weiter, wenn Sie daran interessiert sind, was while(!feof) tatsächlich tut.

Gleichzeitigkeit und Simultaneität

E/A-Operationen interagieren mit der Umgebung. Die Umgebung ist nicht Teil Ihres Programms und unterliegt nicht Ihrer Kontrolle. Die Umgebung existiert wirklich "gleichzeitig" mit Ihrem Programm. Wie bei allen Dingen, die gleichzeitig ablaufen, machen Fragen nach dem "aktuellen Zustand" keinen Sinn: Es gibt kein Konzept der "Gleichzeitigkeit" bei gleichzeitigen Ereignissen. Viele Eigenschaften des Zustands sind einfach nicht existieren gleichzeitig.

Lassen Sie mich das genauer formulieren: Nehmen wir an, Sie wollen fragen: "Haben Sie noch mehr Daten". Sie könnten diese Frage an einen gleichzeitigen Container oder an Ihr E/A-System richten. Aber die Antwort ist im Allgemeinen nicht beantwortbar und daher bedeutungslos. Was soll's, wenn der Container "ja" sagt - bis Sie versuchen zu lesen, hat er vielleicht keine Daten mehr. Wenn die Antwort "nein" lautet, kann es sein, dass zu dem Zeitpunkt, an dem Sie versuchen, die Daten zu lesen, bereits Daten angekommen sind. Die Schlussfolgerung ist, dass es einfach est keine Eigenschaft wie "Ich habe Daten", da Sie nicht sinnvoll auf eine mögliche Antwort reagieren können. (Etwas besser sieht es bei gepufferten Eingaben aus, wo man vielleicht ein "Ja, ich habe Daten" erhält, das eine Art Garantie darstellt, aber man müsste immer noch in der Lage sein, mit dem umgekehrten Fall umzugehen. Und bei der Ausgabe ist die Situation sicherlich genauso schlecht, wie ich sie beschrieben habe: Sie wissen nie, ob die Festplatte oder der Netzwerkpuffer voll ist.)

Daraus schließen wir, dass es unmöglich ist, und zwar un vernünftig um ein E/A-System zu fragen, ob es wird sein eine E/A-Operation durchführen können. Die einzige Möglichkeit, mit ihm zu interagieren (genau wie bei einem nebenläufigen Container), ist Versuch den Vorgang und prüft, ob er erfolgreich war oder nicht. In dem Moment, in dem Sie mit der Umgebung interagieren, können Sie wissen, ob die Interaktion tatsächlich möglich war, und zu diesem Zeitpunkt müssen Sie sich verpflichten, die Interaktion durchzuführen. (Dies ist ein "Synchronisationspunkt", wenn Sie so wollen.)

EOF

Jetzt kommen wir zum EOF. EOF ist die Antwort Sie erhalten von einem Versucht E/A-Betrieb. Es bedeutet, dass Sie versucht haben, etwas zu lesen oder zu schreiben, dabei aber keine Daten lesen oder schreiben konnten und stattdessen auf das Ende der Eingabe oder Ausgabe gestoßen sind. Dies gilt im Wesentlichen für alle E/A-APIs, ob es sich um die C-Standardbibliothek, C++ iostreams oder andere Bibliotheken handelt. Solange die E/A-Operationen erfolgreich sind, können Sie einfach kann nicht wissen ob weitere, zukünftige Operationen erfolgreich sein werden. Sie muss immer zuerst den Vorgang versuchen und dann auf Erfolg oder Misserfolg reagieren.

Beispiele

Beachten Sie in jedem der Beispiele sorgfältig, dass wir erste den E/A-Vorgang versuchen und では verbrauchen das Ergebnis, wenn es gültig ist. Beachten Sie außerdem, dass wir immer muss das Ergebnis der E/A-Operation verwenden, wobei das Ergebnis in jedem Beispiel eine andere Form hat.

  • C stdio, Lesen aus einer Datei:

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

    Das Ergebnis, das wir verwenden müssen, lautet n die Anzahl der gelesenen Elemente (die auch Null sein kann).

  • 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 umgewandelten Elemente.

  • C++, iostreams-formatierte Extraktion:

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

    Das Ergebnis, das wir verwenden müssen, lautet std::cin selbst, das in einem booleschen Kontext ausgewertet werden kann und uns sagt, ob sich der Stream noch in der good() Zustand.

  • C++, iostreams getline:

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

    Das Ergebnis, das wir verwenden müssen, ist wieder std::cin genau wie 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, lautet k die Anzahl der geschriebenen Bytes. Der Punkt hier ist, dass wir nur wissen können, wie viele Bytes geschrieben wurden den Schreibvorgang.

  • 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, lautet nbytes die Anzahl der Bytes bis einschließlich des Zeilenumbruchs (oder EOF, wenn die Datei nicht mit einem Zeilenumbruch endet).

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

Sie werden feststellen, dass wir das Wort "EOF" nur sehr selten buchstabieren. Normalerweise erkennen wir die Fehlerbedingung auf eine andere Art und Weise, die für uns unmittelbar interessanter ist (z. B. das Versagen, so viel E/A durchzuführen, wie wir gewünscht hatten). In jedem Beispiel gibt es eine API-Funktion, die uns explizit mitteilen könnte, dass der EOF-Zustand eingetreten ist, aber das ist eigentlich keine besonders nützliche Information. Es handelt sich dabei um ein viel größeres Detail, als uns oft lieb ist. Wichtig ist vielmehr, ob die E/A erfolgreich war, und nicht, wie sie fehlgeschlagen ist.

  • Ein letztes Beispiel, das tatsächlich den EOF-Status abfragt: Nehmen wir an, Sie haben eine Zeichenkette und wollen prüfen, ob sie eine ganze Zahl darstellt, ohne zusätzliche Bits am Ende außer Leerzeichen. Mit C++ iostreams geht das 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 iss das Stream-Objekt selbst, um zu prüfen, ob die formatierte Extraktion nach value gelungen. Aber dann, nachdem auch Leerraum verbraucht wurde, führen wir eine weitere E/A-Operation durch, iss.get() und erwarten, dass es als EOF fehlschlägt, was der Fall ist, wenn die gesamte Zeichenkette bereits durch die formatierte Extraktion verbraucht wurde.

In der C-Standardbibliothek können Sie etwas Ähnliches mit der strto*l funktioniert, indem sie überprüft, ob der Endzeiger das Ende der Eingabezeichenkette erreicht hat.

283voto

William Pursell Punkte 188248

Es ist falsch, weil es (in Abwesenheit eines Lesefehlers) ein weiteres Mal in die Schleife eintritt, als der Autor erwartet. Wenn ein Lesefehler auftritt, wird die Schleife nie 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 wird stets ein Zeichen mehr ausgeben, als im Eingabestrom vorhanden sind (vorausgesetzt, es treten keine Lesefehler auf). Betrachten wir den Fall, dass der Eingabestrom leer ist:

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

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

Dies geschieht in allen solchen Fällen. feof() gibt nicht true zurück, bis ein Lesevorgang auf dem Stream auf das Ende der Datei stößt. Der Zweck von feof() ist NICHT dazu da, um zu prüfen, ob der nächste Lesevorgang das Ende der Datei erreicht. Der Zweck von feof() ist es, den Status einer früheren Lesefunktion zu ermitteln und zwischen einem Fehlerzustand und dem Ende des Datenstroms zu unterscheiden. Wenn fread() 0 zurückgibt, müssen Sie feof / ferror um zu entscheiden, ob ein Fehler aufgetreten ist oder ob alle Daten verbraucht wurden. Ähnlich verhält es sich, wenn fgetc gibt zurück. EOF . feof() ist nur nützlich fread hat Null zurückgegeben oder fgetc ist zurückgekehrt EOF . Bevor das passiert, feof() wird immer 0 zurückgeben.

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

Noch schlimmer ist der Fall, dass ein Lesefehler auftritt. In diesem Fall, fgetc() gibt zurück. EOF , feof() gibt false zurück, und die Schleife wird nie beendet. In allen Fällen, in denen while(!feof(p)) verwendet wird, muss zumindest innerhalb der Schleife eine Prüfung auf ferror() oder zumindest sollte die while-Bedingung ersetzt werden durch while(!feof(p) && !ferror(p)) oder es besteht die sehr reale Möglichkeit einer Endlosschleife, die wahrscheinlich alle Arten von Müll ausspuckt, da ungültige Daten verarbeitet werden.

Zusammenfassend lässt sich also sagen, dass ich zwar nicht mit Sicherheit sagen kann, dass es niemals eine Situation gibt, in der es semantisch korrekt wäre, " while(!feof(f)) " (obwohl es muss eine weitere Prüfung innerhalb der Schleife mit einer Unterbrechung sein, um eine Endlosschleife bei einem Lesefehler zu vermeiden), ist es der Fall, dass es fast sicher immer falsch ist. Und selbst wenn es jemals einen Fall gäbe, in dem es richtig wäre, ist es so idiomatisch falsch, dass es nicht die richtige Art 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 ohrfeigen (es sei denn, der Autor ist Ihr Chef, in diesem Fall ist Diskretion geboten).

78voto

Erik Punkte 85308

Nein, es ist nicht immer falsch. Wenn Ihre Schleifenbedingung lautet "solange wir nicht versucht haben, über das Ende der Datei hinaus zu lesen", dann verwenden Sie while (!feof(f)) . Dies ist jedoch keine übliche Schleifenbedingung - normalerweise möchte man auf etwas anderes testen (z. B. "kann ich mehr lesen"). while (!feof(f)) ist nicht falsch, es ist nur gebraucht falsch.

47voto

AProgrammer Punkte 49452

feof() zeigt an, ob versucht wurde, über das Ende der Datei hinaus zu lesen. Das bedeutet, dass sie wenig vorhersagende Wirkung hat: Wenn sie wahr ist, ist man sicher, dass die nächste Eingabeoperation fehlschlägt (man ist übrigens nicht sicher, dass die vorherige fehlgeschlagen ist), aber wenn sie falsch ist, ist man nicht sicher, dass die nächste Eingabeoperation erfolgreich sein wird. Außerdem können Eingabeoperationen auch aus anderen Gründen als dem Dateiende fehlschlagen (ein Formatfehler bei formatierten Eingaben, ein reiner IO-Fehler - Festplattenfehler, Netzwerk-Timeout - bei allen Eingabearten), so dass man selbst dann, wenn man das Dateiende voraussagen könnte (und jeder, der versucht hat, Ada one zu implementieren, das voraussagend ist, wird Ihnen sagen, dass es kompliziert sein kann, wenn Sie Leerzeichen überspringen müssen, und dass es unerwünschte Auswirkungen auf interaktive Geräte hat - manchmal wird die Eingabe der nächsten Zeile erzwungen, bevor mit der Bearbeitung der vorhergehenden begonnen wird), in der Lage sein müsste, einen Fehler zu behandeln.

Das korrekte Idiom in C ist also, eine Schleife mit dem Erfolg der IO-Operation als Schleifenbedingung zu bilden und dann die Ursache des Fehlers 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) */
}

-2voto

Scott Deagan Punkte 187

feof() ist nicht sehr intuitiv. Meiner sehr bescheidenen Meinung nach ist die FILE Der End-of-File-Status der Datei sollte auf true wenn ein Lesevorgang dazu führt, dass das Ende der Datei erreicht wird. Stattdessen müssen Sie nach jedem Lesevorgang manuell prüfen, ob das Ende der Datei erreicht wurde. So etwas funktioniert zum Beispiel, wenn man aus einer Textdatei liest, indem man 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 toll, wenn stattdessen so etwas 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;
}

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X