¿Por qué “while (! Feof (archivo))” siempre está mal?

He visto gente tratando de leer archivos como este en muchas publicaciones últimamente.

Código

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

¿Qué está mal con este while( !feof(fp)) loop?

Me gustaría proporcionar una perspectiva abstracta y de alto nivel.

Concurrencia y simultaneidad

Las operaciones de E / S interactúan con el entorno. El entorno no es parte de su progtwig y no está bajo su control. El entorno realmente existe “concurrentemente” con su progtwig. Al igual que con todas las cosas concurrentes, las preguntas sobre el “estado actual” no tienen sentido: no existe un concepto de “simultaneidad” entre los eventos concurrentes. Muchas propiedades del estado simplemente no existen al mismo tiempo.

Permítanme hacer esto más preciso: supongan que quieren preguntar, “¿tienen más datos?”. Puede solicitar esto de un contenedor simultáneo o de su sistema de E / S. Pero la respuesta es generalmente inaceptable, y por lo tanto sin sentido. Entonces, ¿qué pasa si el contenedor dice “sí”? Para el momento en que intenta leer, es posible que ya no tenga datos. De manera similar, si la respuesta es “no”, para el momento en que intente leer, es posible que los datos hayan llegado. La conclusión es que simplemente no hay propiedades como “Tengo datos”, ya que no puedes actuar de manera significativa en respuesta a cualquier posible respuesta. (La situación es un poco mejor con la entrada en el búfer, donde es posible que obtengas un “sí, tengo datos” que constituye algún tipo de garantía, pero aún así deberías poder tratar el caso contrario. Y con salida la situación sin duda es tan malo como lo describí: nunca se sabe si ese disco o ese búfer de red está lleno).

Por lo tanto, concluimos que es imposible, y de hecho no es razonable , preguntar a un sistema de E / S si podrá realizar una operación de E / S. La única forma posible en que podemos interactuar con ella (al igual que con un contenedor simultáneo) es intentar la operación y verificar si tuvo éxito o falló. En ese momento en el que interactúas con el entorno, entonces y solo entonces puedes saber si la interacción fue realmente posible, y en ese momento debes comprometerte a realizar la interacción. (Esto es un “punto de sincronización”, si lo desea).

EOF

Ahora llegamos a EOF. EOF es la respuesta que recibe de una operación de E / S intentada . Significa que estaba tratando de leer o escribir algo, pero al hacerlo no pudo leer ni escribir ningún dato, y en su lugar se encontró el final de la entrada o salida. Esto es cierto para prácticamente todas las API de E / S, ya sea la biblioteca estándar de C, iostreams de C ++ u otras bibliotecas. Siempre que las operaciones de E / S tengan éxito, simplemente no puede saber si futuras operaciones futuras tendrán éxito. Siempre debe probar primero la operación y luego responder al éxito o al fracaso.

Ejemplos

En cada uno de los ejemplos, observe cuidadosamente que primero intentamos la operación de E / S y luego consumimos el resultado si es válido. Tenga en cuenta además que siempre debemos usar el resultado de la operación de E / S, aunque el resultado adopta formas y formas diferentes en cada ejemplo.

  • C stdio, leer de un archivo:

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

    El resultado que debemos usar es n , la cantidad de elementos que se leyeron (que pueden ser tan bajos como cero).

  • C stdio, scanf :

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

    El resultado que debemos usar es el valor de retorno de scanf , la cantidad de elementos convertidos.

  • Extracción formateada C ++, iostreams:

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

    El resultado que debemos usar es std::cin , que se puede evaluar en un contexto booleano y nos dice si la secuencia todavía está en el estado good() .

  • C ++, iostreams getline:

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

    El resultado que debemos usar es nuevamente std::cin , igual que antes.

  • POSIX, write(2) para purgar 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 */ } 

    El resultado que usamos aquí es k , la cantidad de bytes escritos. El punto aquí es que solo podemos saber cuántos bytes se escribieron después de la operación de escritura.

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

    El resultado que debemos usar es nbytes , el número de bytes hasta e incluyendo la nueva línea (o EOF si el archivo no finalizó con una nueva línea).

    Tenga en cuenta que la función devuelve explícitamente -1 (¡y no EOF!) Cuando ocurre un error o llega a EOF.

Puede notar que raramente deletreamos la palabra real "EOF". Por lo general, detectamos la condición de error de alguna otra manera que nos interese más de inmediato (por ejemplo, no realizar la cantidad de E / S que deseábamos). En todos los ejemplos, hay alguna característica de la API que podría decirnos explícitamente que se ha encontrado el estado EOF, pero de hecho no es una información terriblemente útil. Es mucho más un detalle de lo que a menudo nos importa. Lo que importa es si la E / S tuvo éxito, más que cómo falló.

  • Un último ejemplo que realmente consulta el estado EOF: suponga que tiene una cadena y desea probar que representa un entero en su totalidad, sin bits adicionales al final, excepto espacios en blanco. Usando C ++ iostreams, dice así:

     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 } 

    Usamos dos resultados aquí. El primero es iss , el objeto de secuencia en sí, para verificar que la extracción formateada a value tuvo éxito. Pero luego, después de consumir también espacio en blanco, realizamos otra E / S / operación, iss.get() , y esperamos que falle como EOF, que es el caso si toda la cadena ya ha sido consumida por la extracción formateada.

    En la biblioteca estándar C puede lograr algo similar con las funciones strto*l comprobando que el puntero final ha llegado al final de la cadena de entrada.

La respuesta

while(!eof) está mal porque prueba algo que es irrelevante y no prueba algo que usted necesita saber. El resultado es que está ejecutando erróneamente código que supone que está accediendo a datos que se leyeron con éxito, cuando en realidad esto nunca sucedió.

Está mal porque (en ausencia de un error de lectura) ingresa al ciclo una vez más de lo que el autor espera. Si hay un error de lectura, el ciclo nunca termina.

Considera el siguiente código:

 /* WARNING: demonstration of bad coding technique*/ #include  #include  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; } 

Este progtwig imprimirá consistentemente uno mayor que el número de caracteres en la secuencia de entrada (suponiendo que no haya errores de lectura). Considere el caso donde la stream de entrada está vacía:

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

En este caso, se llama a feof() antes de que se haya leído cualquier dato, por lo que devuelve falso. Se ingresa el fgetc() , se llama a fgetc() (y devuelve EOF ) y se incrementa el recuento. Entonces se llama a feof() y devuelve verdadero, lo que hace que el ciclo se anule.

Esto sucede en todos esos casos. feof() no devuelve verdadero hasta que una lectura en la secuencia encuentra el final del archivo. El propósito de feof() NO es verificar si la próxima lectura llegará al final del archivo. El propósito de feof() es distinguir entre un error de lectura y haber llegado al final del archivo. Si fread() devuelve 0, debe usar feof / ferror para decidir. Del mismo modo si fgetc devuelve EOF . feof() solo es útil después de que fread haya devuelto cero o fgetc haya devuelto EOF . Antes de que eso suceda, feof() siempre devolverá 0.

Siempre es necesario verificar el valor de retorno de una lectura (ya sea un fread() , o un fscanf() , o un fgetc() ) antes de llamar a feof() .

Peor aún, considere el caso donde ocurre un error de lectura. En ese caso, fgetc() devuelve EOF , feof() devuelve falso y el bucle nunca termina. En todos los casos en while(!feof(p)) se utiliza while(!feof(p)) , debe haber al menos una comprobación dentro del ciclo para ferror() , o al menos la condición while debe reemplazarse con while(!feof(p) && !ferror(p)) o hay una posibilidad muy real de un bucle infinito, probablemente arrojando todo tipo de basura a medida que se procesan datos no válidos.

Entonces, en resumen, aunque no puedo afirmar con certeza que nunca haya una situación en la que pueda ser semánticamente correcto escribir " while(!feof(f)) " (aunque debe haber otra verificación dentro del ciclo con un salto a evitar un bucle infinito en un error de lectura), es el caso de que casi siempre es siempre incorrecto. E incluso si surgiera un caso donde sería correcto, es tan idiomáticamente erróneo que no sería la forma correcta de escribir el código. Cualquiera que vea el código debería vacilar inmediatamente y decir: "eso es un error". Y posiblemente abofetear al autor (a menos que el autor sea su jefe, en cuyo caso se recomienda discreción).

No, no siempre es incorrecto Si su condición de bucle es “mientras no hemos intentado leer el final del archivo”, entonces usa while (!feof(f)) . Sin embargo, esta no es una condición de bucle común; por lo general, desea probar otra cosa (como “puedo leer más”). while (!feof(f)) no está mal, solo se usa incorrectamente.

feof () indica si se ha intentado leer más allá del final del archivo. Eso significa que tiene poco efecto predictivo: si es cierto, está seguro de que la siguiente operación de entrada fallará (no está seguro de que la anterior haya fallado por cierto), pero si es falsa, no está seguro de que la próxima entrada la operación tendrá éxito. Además, las operaciones de entrada pueden fallar por otras razones que no sean el final del archivo (un error de formato para entrada formateada, una falla de IO pura – falla de disco, tiempo de espera de red – para todos los tipos de entrada), incluso si pudiera predecir al final del archivo (y cualquiera que haya intentado implementar Ada one, que es predictivo, le dirá que puede ser complejo si necesita saltear espacios, y que tiene efectos no deseados en dispositivos interactivos, a veces forzando la entrada del próximo línea antes de iniciar el manejo del anterior), debería poder manejar un error.

Entonces, la expresión correcta en C es hacer un ciclo con el éxito de la operación IO como condición de bucle, y luego probar la causa de la falla. Por ejemplo:

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

Gran respuesta, acabo de notar lo mismo porque estaba tratando de hacer un ciclo así. Por lo tanto, está mal en ese escenario, pero si desea tener un ciclo que finalice con gracia en el EOF, esta es una buena manera de hacerlo:

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