Perché "while (! Feof (file))" è sempre sbagliato?


574

Di recente ho visto persone che cercavano di leggere file come questo in molti post:

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

Cosa c'è di sbagliato in questo loop?



Risposte:


454

Vorrei fornire una prospettiva astratta e di alto livello.

Concorrenza e simultaneità

Le operazioni di I / O interagiscono con l'ambiente. L'ambiente non fa parte del programma e non è sotto il tuo controllo. L'ambiente esiste davvero "contemporaneamente" al tuo programma. Come per tutte le cose simultanee, le domande sullo "stato attuale" non hanno senso: non esiste un concetto di "simultaneità" tra eventi simultanei. Molte proprietà dello stato semplicemente non esistono contemporaneamente.

Consentitemi di renderlo più preciso: supponiamo di voler chiedere "avete più dati". Potresti chiedere questo a un contenitore simultaneo o al tuo sistema I / O. Ma la risposta è generalmente non azionabile e quindi insignificante. Quindi cosa succede se il contenitore dice "sì" - quando provi a leggere, potrebbe non avere più dati. Allo stesso modo, se la risposta è "no", quando si tenta di leggere, i dati potrebbero essere arrivati. La conclusione è che semplicemente sianessuna proprietà come "I have data", poiché non puoi agire in modo significativo in risposta a una possibile risposta. (La situazione è leggermente migliore con input bufferizzati, dove si potrebbe presumibilmente ottenere un "sì, ho dei dati" che costituisce una sorta di garanzia, ma dovresti comunque essere in grado di affrontare il caso opposto. E con l'output della situazione è certamente altrettanto brutto come ho descritto: non si sa mai se quel disco o quel buffer di rete sono pieni.)

Quindi concludiamo che è impossibile, e di fatto non ragionevole , chiedere a un sistema I / O se sarà in grado di eseguire un'operazione I / O. L'unico modo possibile per interagire con esso (proprio come con un contenitore simultaneo) è tentare l'operazione e verificare se ha avuto esito positivo o negativo. In quel momento in cui interagisci con l'ambiente, allora e solo allora puoi sapere se l'interazione era effettivamente possibile, e a quel punto devi impegnarti a eseguire l'interazione. (Questo è un "punto di sincronizzazione", se vuoi.)

EOF

Ora arriviamo a EOF. EOF è la risposta che si ottiene da una tentata operazione di I / O. Significa che stavi cercando di leggere o scrivere qualcosa, ma nel farlo non sei riuscito a leggere o scrivere alcun dato e invece è stata rilevata la fine dell'input o dell'output. Questo vale essenzialmente per tutte le API di I / O, sia che si tratti della libreria C standard, iostreams C ++ o altre librerie. Finché le operazioni di I / O hanno esito positivo, semplicemente non si può sapere se ulteriori operazioni future avranno esito positivo. È necessario sempre provare prima l'operazione e quindi rispondere al successo o il fallimento.

Esempi

In ciascuno degli esempi, notare attentamente che prima tentiamo l'operazione di I / O e quindi consumiamo il risultato se è valido. Si noti inoltre che è sempre necessario utilizzare il risultato dell'operazione di I / O, anche se il risultato assume forme e forme diverse in ciascun esempio.

  • C stdio, letto da un file:

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

    Il risultato che dobbiamo usare è nil numero di elementi letti (che può essere pari a zero).

  • C stdio, scanf:

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

    Il risultato che dobbiamo usare è il valore di ritorno di scanf, il numero di elementi convertiti.

  • C ++, estrazione formattata iostreams:

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

    Il risultato che dobbiamo usare è esso std::cinstesso, che può essere valutato in un contesto booleano e ci dice se il flusso è ancora nello good()stato.

  • C ++, getline di iostreams:

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

    Il risultato che dobbiamo usare è di nuovo std::cin, proprio come prima.

  • POSIX, write(2)per svuotare un buffer:

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

    Il risultato che usiamo qui è kil numero di byte scritti. Il punto qui è che possiamo solo sapere quanti byte sono stati scritti dopo l'operazione di scrittura.

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

    Il risultato che dobbiamo usare è nbytesil numero di byte fino alla nuova riga inclusa (o EOF se il file non termina con una nuova riga).

    Si noti che la funzione restituisce esplicitamente -1(e non EOF!) Quando si verifica un errore o raggiunge EOF.

Si può notare che raramente si scrive la parola "EOF". Di solito rileviamo la condizione di errore in un altro modo che ci interessa più immediatamente (ad es. Incapacità di eseguire tutto l'I / O desiderato). In ogni esempio c'è qualche funzione API che potrebbe dirci esplicitamente che lo stato EOF è stato riscontrato, ma in realtà questa non è un'informazione estremamente utile. È molto più un dettaglio di quanto ci preoccupiamo spesso. Ciò che conta è se l'I / O ha avuto successo, più di quanto non abbia funzionato.

  • Un ultimo esempio che in realtà interroga lo stato EOF: supponiamo di avere una stringa e di voler verificare che rappresenti un numero intero nella sua interezza, senza bit extra alla fine tranne gli spazi bianchi. Usando iostreams C ++, va così:

    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
    }

    Usiamo due risultati qui. Il primo è iss, l'oggetto flusso stesso, per verificare che l'estrazione formattata abbia valueavuto esito positivo. Ma poi, dopo aver consumato anche spazi bianchi, eseguiamo un'altra operazione I / O / iss.get()e ci aspettiamo che fallisca come EOF, come nel caso in cui l'intera stringa sia già stata consumata dall'estrazione formattata.

    Nella libreria standard C puoi ottenere qualcosa di simile con le strto*lfunzioni controllando che il puntatore di fine abbia raggiunto la fine della stringa di input.

La risposta

while(!feof)è sbagliato perché verifica qualcosa che è irrilevante e non riesce a verificare qualcosa che devi sapere. Il risultato è che si sta eseguendo erroneamente il codice che presume che stia accedendo ai dati letti correttamente, quando in realtà ciò non è mai accaduto.


34
@CiaPan: non penso sia vero. Sia C99 che C11 lo consentono.
Kerrek SB,

11
Ma ANSI C no.
CiaPan,

3
@JonathanMee: è un male per tutti i motivi che ho citato: non puoi guardare al futuro. Non puoi dire cosa accadrà in futuro.
Kerrek SB,

3
@JonathanMee: Sì, sarebbe appropriato, anche se di solito è possibile combinare questo controllo nell'operazione (poiché la maggior parte delle operazioni di iostreams restituisce l'oggetto stream, che a sua volta ha una conversione booleana), e in questo modo si rende evidente che non si è ignorando il valore restituito.
Kerrek SB,

4
Il terzo paragrafo è notevolmente fuorviante / impreciso per una risposta accettata e altamente votata. feof()non "chiede al sistema I / O se ha più dati". feof(), secondo la manpage (Linux) : "testa l'indicatore di fine file per lo stream a cui punta lo stream, restituendo un valore diverso da zero se impostato." (inoltre, una chiamata esplicita a clearerr()è l'unico modo per ripristinare questo indicatore); A questo proposito, la risposta di William Pursell è molto migliore.
Arne Vogel,

234

È sbagliato perché (in assenza di un errore di lettura) entra nel ciclo ancora una volta di quanto l'autore si aspetti. Se si verifica un errore di lettura, il ciclo non termina mai.

Considera il seguente codice:

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

Questo programma stamperà costantemente uno maggiore del numero di caratteri nel flusso di input (presupponendo che non vi siano errori di lettura). Considera il caso in cui il flusso di input è vuoto:

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

In questo caso, feof()viene chiamato prima che i dati siano stati letti, quindi restituisce false. Il ciclo viene inserito, fgetc()viene chiamato (e restituisce EOF) e il conteggio viene incrementato. Quindi feof()viene chiamato e restituisce true, causando l'interruzione del loop.

Questo succede in tutti questi casi. feof()non restituisce true fino a quando una lettura sullo stream non rileva la fine del file. Lo scopo di feof()NON è verificare se la lettura successiva raggiungerà la fine del file. Lo scopo di feof()è di distinguere tra un errore di lettura e aver raggiunto la fine del file. Se fread()restituisce 0, è necessario utilizzare feof/ ferrorper decidere se si è verificato un errore o se tutti i dati sono stati consumati. Allo stesso modo se fgetcritorna EOF. feof()è utile solo dopo che Fread ha restituito zero o fgetcè tornato EOF. Prima che ciò accada, feof()restituirà sempre 0.

È sempre necessario controllare il valore restituito di una lettura (an fread(), oppure an fscanf(), o an fgetc()) prima di chiamare feof().

Ancora peggio, considera il caso in cui si verifica un errore di lettura. In tal caso, fgetc()restituisce EOF, feof()restituisce false e il ciclo non termina mai. In tutti i casi in cui while(!feof(p))viene utilizzato, deve esserci almeno un controllo all'interno del ciclo per ferror(), o almeno la condizione while deve essere sostituita con while(!feof(p) && !ferror(p))o c'è una possibilità molto reale di un ciclo infinito, probabilmente vomitando ogni sorta di immondizia come dati non validi in fase di elaborazione.

Quindi, in sintesi, anche se non posso affermare con certezza che non c'è mai una situazione in cui potrebbe essere semanticamente corretto scrivere " while(!feof(f))" (anche se ci deve essere un altro controllo all'interno del ciclo con una pausa per evitare un ciclo infinito su un errore di lettura ), è quasi sempre sempre sbagliato. E anche se un caso dovesse sorgere dove sarebbe corretto, è così idiomaticamente sbagliato che non sarebbe il modo giusto di scrivere il codice. Chiunque veda quel codice dovrebbe immediatamente esitare e dire "questo è un bug". E possibilmente schiaffeggiare l'autore (a meno che l'autore sia il tuo capo nel qual caso è consigliata la discrezione.)


7
Certo che è sbagliato, ma a parte questo non è "brutto grato".
nobar,

89
Dovresti aggiungere un esempio di codice corretto, poiché immagino che molte persone verranno qui in cerca di una soluzione rapida.
jleahy,

6
@Thomas: Non sono un esperto di C ++, ma credo che file.eof () restituisca effettivamente lo stesso risultato di feof(file) || ferror(file), quindi è molto diverso. Ma questa domanda non intende essere applicabile al C ++.
William Pursell,

6
@ m-ric non è neanche corretto, perché proverai comunque a elaborare una lettura non riuscita.
Mark Ransom,

4
questa è la vera risposta corretta. feof () viene utilizzato per conoscere l'esito del precedente tentativo di lettura. Quindi probabilmente non vuoi usarlo come condizione di interruzione del ciclo. +1
Jack,

63

No, non è sempre sbagliato. Se la condizione del tuo ciclo è "mentre non abbiamo provato a leggere la fine del file", allora usi while (!feof(f)). Questa, tuttavia, non è una condizione di loop comune - di solito si desidera verificare qualcos'altro (come "posso leggere di più"). while (!feof(f))non è sbagliato, è solo usato male.


1
Mi chiedo ... f = fopen("A:\\bigfile"); while (!feof(f)) { /* remove diskette */ }o (andando a testarlo)f = fopen(NETWORK_FILE); while (!feof(f)) { /* unplug network cable */ }
pmg

1
@pmg: come detto, "non una condizione di loop comune" hehe. Non riesco davvero a pensare a nessun caso in cui ne abbia avuto bisogno, di solito mi interessa "potrei leggere quello che volevo" con tutto ciò che implica la gestione degli errori
Erik

@pmg: Come detto, raramente vuoiwhile(!eof(f))
Erik

9
Più precisamente, la condizione è "mentre non abbiamo provato a leggere oltre la fine del file e non si è verificato alcun errore di lettura" feofnon riguarda il rilevamento della fine del file; si tratta di determinare se una lettura è stata breve a causa di un errore o perché l'input è esaurito.
William Pursell,

35

feof()indica se si è tentato di leggere oltre la fine del file. Ciò significa che ha poco effetto predittivo: se è vero, sei sicuro che l'operazione di input successiva fallirà (non sei sicuro che la precedente abbia fallito BTW), ma se è falsa, non sei sicuro dell'input successivo l'operazione avrà esito positivo. Inoltre, le operazioni di input potrebbero non riuscire per ragioni diverse dalla fine del file (un errore di formattazione per input formattato, un puro errore IO - errore del disco, timeout di rete - per tutti i tipi di input), quindi anche se si potrebbe essere predittivi la fine del file (e chiunque abbia tentato di implementare Ada one, che è predittivo, ti dirà che può essere complesso se devi saltare gli spazi e che ha effetti indesiderati sui dispositivi interattivi, a volte forzando l'input del prossimo linea prima di iniziare la gestione della precedente),

Quindi il linguaggio corretto in C è quello di eseguire il loop con il successo dell'operazione IO come condizione del loop, quindi testare la causa dell'errore. Per esempio:

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
Arrivare alla fine di un file non è un errore, quindi metto in dubbio la frase "le operazioni di input potrebbero non riuscire per ragioni diverse dalla fine del file".
William Pursell,

@WilliamPursell, raggiungere l'eof non è necessariamente un errore, ma non essere in grado di eseguire un'operazione di input a causa di eof è uno. Ed è impossibile in C rilevare in modo affidabile l'ef senza aver fallito un'operazione di input.
Programmatore

Concordo ultimo elsenon possibile con sizeof(line) >= 2e fgets(line, sizeof(line), file)ma possibile con patologico size <= 0e fgets(line, size, file). Forse anche possibile con sizeof(line) == 1.
chux - Ripristina Monica il

1
Tutti quei discorsi sul "valore predittivo" ... Non ci ho mai pensato in quel modo. Nel mio mondo, feof(f)non PREDICARE nulla. Indica che un'operazione PRECEDENTE ha colpito la fine del file. Niente di più, niente di meno. E se non c'erano operazioni precedenti (appena aperto), non riporta la fine del file anche se il file era vuoto all'inizio. Quindi, a parte la spiegazione della concorrenza in un'altra risposta sopra, non credo che ci sia motivo di non fare il giro feof(f).
BitTickler,

@AProgrammer: una richiesta di "lettura fino a N byte" che restituisce zero, sia a causa di un EOF "permanente" o perché non sono ancora disponibili altri dati , non è un errore. Mentre feof () potrebbe non prevedere in modo affidabile che le richieste future forniranno dati, può indicare in modo affidabile che le richieste future non lo faranno . Forse dovrebbe esserci una funzione di stato che indichi "È plausibile che le future richieste di lettura avranno successo", con la semantica che dopo la lettura alla fine di un file ordinario, un'implementazione di qualità dovrebbe dire che è improbabile che le letture future abbiano successo in assenza di una ragione per credo che potrebbero .
supercat

0

feof()non è molto intuitivo. Secondo la mia modesta opinione, lo FILEstato di fine del file dovrebbe essere impostato su truese qualsiasi operazione di lettura determina il raggiungimento della fine del file. Invece, devi controllare manualmente se è stata raggiunta la fine del file dopo ogni operazione di lettura. Ad esempio, qualcosa del genere funzionerà se si legge da un file di testo usando 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;
}

Sarebbe bello se qualcosa del genere funzionasse invece:

#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));? Questo è un comportamento indefinito. fgetc()ritorna int, no char.
Andrew Henle

Mi sembra che il linguaggio standard while( (c = getchar()) != EOF)sia molto "qualcosa del genere".
William Pursell,

while( (c = getchar()) != EOF)funziona su uno dei miei desktop con GNU C 10.1.0, ma non funziona su Raspberry Pi 4 con GNU C 9.3.0. Sul mio RPi4, non rileva la fine del file e continua a funzionare.
Scott Deagan,

@AndrewHenle Hai ragione! Passare char cal int clavoro! Grazie!!
Scott Deagan,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.