¿Por qué "mientras (! Feof (archivo))" siempre es incorrecto?
¿Qué hay de malo en usar feof()
para controlar un bucle de lectura? Por ejemplo:
#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;
}
¿Qué hay de malo en este bucle?
TL;DR
while(!feof(file))
está mal porque prueba algo que es irrelevante y no prueba algo que usted necesita saber. El resultado es que está ejecutando erróneamente un código que supone que está accediendo a datos que se leyeron correctamente, cuando en realidad esto nunca sucedió.
Me gustaría ofrecer una perspectiva abstracta y de alto nivel. Continúe leyendo si está interesado en lo que while(!feof(file))
realmente hace.
Concurrencia y simultaneidad
Las operaciones de E/S interactúan con el entorno. El medio ambiente no es parte de su programa y no está bajo su control. El entorno realmente existe "al mismo tiempo" que su programa. Como ocurre con todas las cosas concurrentes, las preguntas sobre el "estado actual" no tienen sentido: no existe el concepto de "simultaneidad" entre eventos concurrentes. Muchas propiedades de estado simplemente no existen al mismo tiempo.
Permítanme ser más preciso: supongamos que desea preguntar "¿tiene más datos?". Podría preguntarle esto a un contenedor concurrente o a su sistema de E/S. Pero la respuesta es generalmente inaplicable y, por tanto, carece de sentido. Entonces, ¿qué pasa si el contenedor dice "sí"? Cuando intentes leer, es posible que ya no tenga datos. De manera similar, si la respuesta es "no", cuando intentes leer, es posible que ya hayan llegado datos. La conclusión es que simplemente no existe una propiedad como "Tengo datos", ya que no se puede actuar de manera significativa en respuesta a cualquier posible respuesta. (La situación es ligeramente mejor con la entrada almacenada en el búfer, donde es posible obtener un "sí, tengo datos" que constituye algún tipo de garantía, pero aún así tendría que poder lidiar con el caso opuesto. Y con la salida la situación es ciertamente 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 irrazonable , preguntarle a un sistema de E/S si podrá realizar una operación de E/S. La única forma posible de interactuar con él (al igual que con un contenedor concurrente) es intentar la operación y comprobar si tuvo éxito o falló. En ese momento en el que interactúas con el entorno, entonces y sólo entonces podrás saber si la interacción fue realmente posible, y en ese punto debes comprometerte a realizar la interacción. (Este es un "punto de sincronización", por así decirlo).
EOF
Ahora llegamos a EOF. EOF es la respuesta que se obtiene de un intento de operación de E/S. Significa que estaba intentando leer o escribir algo, pero al hacerlo no pudo leer ni escribir ningún dato y, en cambio, se encontró el final de la entrada o salida. Esto es cierto para esencialmente todas las API de E/S, ya sea la biblioteca estándar de C, C++ iostreams u otras bibliotecas. Mientras las operaciones de E/S tengan éxito, simplemente no se puede saber si otras operaciones futuras tendrán éxito. Siempre debes intentar primero la operación y luego responder al éxito o al fracaso .
Ejemplos
En cada uno de los ejemplos, tenga en cuenta 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 toma diferentes formas en cada ejemplo.
C stdio, leer desde un archivo:
for (;;) { size_t n = fread(buf, 1, bufsize, infile); consume(buf, n); if (n == 0) { break; } }
El resultado que debemos usar es
n
, la cantidad de elementos que se leyeron (que puede ser tan solo 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
, el número de elementos convertidos.C++, extracción formateada de iostreams:
for (int n; std::cin >> n; ) { consume(n); }
El resultado que debemos usar es
std::cin
él mismo, que puede evaluarse en un contexto booleano y nos dice si la secuencia todavía está en esegood()
estado.C++, línea de obtención de iostreams:
for (std::string line; std::getline(std::cin, line); ) { consume(line); }
El resultado que debemos usar es nuevamente
std::cin
, como antes.POSIX,
write(2)
para vaciar un búfer: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
el número 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 la nueva línea incluida (o EOF si el archivo no termina 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 muy rara vez deletreamos la palabra "EOF". Generalmente detectamos la condición de error de alguna otra manera que nos resulta más interesante (por ejemplo, no realizar tantas E/S como habíamos deseado). En cada ejemplo hay alguna característica API que podría decirnos explícitamente que se ha encontrado el estado EOF, pero en realidad esta no es una información muy útil. Es un detalle mucho más de lo que muchas veces 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 número entero en su totalidad, sin bits adicionales al final, excepto espacios en blanco. Usando C++ iostreams, es 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 flujo en sí, para verificar que la extracción formateada se haya value
realizado correctamente. Pero luego, después de consumir también espacios en blanco, realizamos otra operación de E/S/ iss.get()
y esperamos que falle como EOF, que es el caso si la extracción formateada ya ha consumido toda la cadena.
En la biblioteca estándar de C puedes lograr algo similar con las strto*l
funciones comprobando que el puntero final haya llegado al final de la cadena de entrada.
Está mal porque (en ausencia de un error de lectura) ingresa al bucle una vez más de lo que espera el autor. Si hay un error de lectura, el ciclo nunca termina.
Considere el siguiente código:
/* 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 = argc > 1 ? Fopen(argv[1], "r") : stdin;
unsigned 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;
}
Este programa imprimirá consistentemente uno más que el número de caracteres en el flujo de entrada (suponiendo que no haya errores de lectura). Considere el caso en el que el flujo de entrada está vacío:
$ ./a.out < /dev/null
Number of characters read: 1
En este caso, feof()
se llama antes de que se hayan leído los datos, por lo que devuelve falso. Se ingresa al bucle, fgetc()
se llama (y devuelve EOF
) y se incrementa el recuento. Luego feof()
se llama y devuelve verdadero, lo que provoca que el ciclo se cancele.
Esto sucede en todos estos casos. feof()
no devuelve verdadero hasta que una lectura en la secuencia encuentre 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 determinar el estado de una función de lectura anterior y distinguir entre una condición de error y el final del flujo de datos. Si fread()
devuelve 0, debe usar feof
/ ferror
para decidir si ocurrió un error o si se consumieron todos los datos. De manera similar, si fgetc
regresa EOF
. feof()
sólo 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 an fread()
, an fscanf()
o an fgetc()
) antes de llamar feof()
.
Peor aún, considere el caso en el que se produce un error de lectura. En ese caso, fgetc()
devuelve EOF
, feof()
devuelve falso y el ciclo nunca termina. En todos los casos en los que while(!feof(p))
se utiliza, debe haber al menos una verificación dentro del bucle for ferror()
, o al menos la condición while debe reemplazarse con while(!feof(p) && !ferror(p))
o existe una posibilidad muy real de un bucle infinito, probablemente arrojando todo tipo de basura como se están procesando datos no válidos.
En resumen, aunque no puedo afirmar con certeza que nunca existe una situación en la que pueda ser semánticamente correcto escribir " while(!feof(f))
" (aunque debe haber otra verificación dentro del bucle con una interrupción para evitar un bucle infinito en un error de lectura), lo cierto es que es casi seguro que siempre está mal. E incluso si alguna vez surgiera un caso en el que fuera correcto, es tan idiomáticamente incorrecto que no sería la forma correcta de escribir el código. Cualquiera que vea ese código debería dudar 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).
EDITAR: una forma de escribir el código correctamente, demostrando el uso correcto de feof
y ferror
:
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
int
main(int argc, char **argv)
{
FILE *in = stdin;
unsigned count = 0;
while( getc(in) != EOF ){
count++;
}
if( feof(in) ){
printf("Number of characters read: %u\n", count);
} else if( ferror(in) ){
perror("stdin");
} else {
assert(0);
}
return EXIT_SUCCESS;
}
No, no siempre está mal. Si su condición de bucle es "mientras no hemos intentado leer más allá del 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 algo más (como "¿puedo leer más"). while (!feof(f))
no está mal, simplemente se usa mal.
feof()
indica si uno ha intentado leer más allá del final del archivo. Eso significa que tiene poco efecto predictivo: si es verdadero, está seguro de que la siguiente operación de entrada fallará (no está seguro de que la anterior falló, por cierto), pero si es falso, no está seguro de la siguiente entrada. la operación tendrá éxito. Además, las operaciones de entrada pueden fallar por otras razones además del final del archivo (un error de formato para la entrada formateada, una falla pura de IO (falla del disco, tiempo de espera de la red) para todos los tipos de entrada), por lo que incluso si pudiera ser predictivo sobre al final del archivo (y cualquiera que haya intentado implementar Ada one, que es predictivo, le dirá que puede ser complejo si necesita omitir espacios y que tiene efectos indeseables en dispositivos interactivos, a veces forzando la entrada del siguiente línea antes de comenzar el manejo de la anterior), tendría que ser capaz de manejar una falla.
Entonces, el modismo correcto en C es realizar un bucle con el éxito de la operación IO como condición del bucle y luego probar la causa del fallo. 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) */
}
Las otras respuestas a esta pregunta son muy buenas, pero bastante largas. Si solo quieres el TL;DR, es este:
feof(F)
esta mal nombrado. No significa "comprobar si ahoraF
está al final del archivo "; más bien le indica por qué el intento anterior no pudo obtener ningún dato .F
El estado de fin de archivo puede cambiar fácilmente, porque un archivo puede crecer o reducirse, y un terminal informa EOF
una vez cada vez que presiona ^D
(en modo "preparado", en una línea que de otro modo estaría vacía).
A menos que realmente le importe por qué la lectura anterior no devolvió ningún dato, es mejor que olvide que la feof
función existe.