dev - ¿Por qué "while(! Feof(file))" siempre es incorrecto?
feof c (5)
Últimamente, he visto personas que intentan leer archivos como este en muchas publicaciones.
Código
#include <stdio.h>
#include <stdlib.h>
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 bucle while( !feof(fp))
?
Está mal porque (en ausencia de un error de lectura) entra en el bucle una vez más de lo que el autor espera. Si hay un error de lectura, el bucle 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;
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 programa imprimirá consistentemente uno mayor que la cantidad de caracteres en el flujo de entrada (asumiendo que no haya errores de lectura). Considere el caso donde el flujo de entrada está vacío:
$ ./a.out < /dev/null
Number of characters read: 1
En este caso, se llama a feof()
antes de leer cualquier dato, por lo que devuelve falso. Se ingresa al bucle, se llama a fgetc()
(y devuelve EOF
), y el conteo se incrementa. Entonces se llama a feof()
y devuelve true, lo que provoca que el bucle se aborte.
Esto sucede en todos estos casos. feof()
no devuelve true hasta después de que una lectura en la ruta 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. Similarmente 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 se produce un error de lectura. En ese caso, fgetc()
devuelve EOF
, feof()
devuelve falso y el bucle nunca termina. En todos los casos en los while(!feof(p))
, debe haber al menos una comprobación dentro del bucle para ferror()
, o al menos la condición while debe reemplazarse por while(!feof(p) && !ferror(p))
o existe una posibilidad muy real de un bucle infinito, probablemente arrojando todo tipo de basura cuando se procesan datos no válidos.
Entonces, en resumen, aunque no puedo afirmar con certeza que nunca hay una situación en la que pueda ser semánticamente correcto escribir " while(!feof(f))
" (aunque debe haber otra comprobación dentro del bucle con una ruptura de evitar un bucle infinito en un error de lectura), es el caso de que es casi seguro que siempre está mal. E incluso si surgiera un caso en el que sería correcto, es un error tan idiomático que no sería la forma correcta de escribir el código. Cualquiera que vea ese código debería vacilar inmediatamente y decir: "eso es un error". Y posiblemente abofetee al autor (a menos que el autor sea su jefe, en cuyo caso se recomienda discreción).
Gran respuesta, me di cuenta de lo mismo porque estaba tratando de hacer un bucle como ese. Por lo tanto, es incorrecto en ese escenario, pero si quieres tener un bucle que termine con gracia en el EOF, esta es una buena manera de hacerlo:
#include <stdio.h>
#include <sys/stat.h>
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
}
Me gustaría ofrecer una perspectiva abstracta de alto nivel.
Concurrencia y simultaneidad.
Las operaciones de E / S interactúan con el entorno. El ambiente no es parte de su programa, y no está bajo su control. El ambiente realmente existe "concurrentemente" con su programa. Al igual que con todas las cosas concurrentes, las preguntas sobre el "estado actual" no tienen sentido: no hay un concepto de "simultaneidad" entre los eventos concurrentes. Muchas propiedades del estado simplemente no existen simultáneamente.
Permítame hacer esto más preciso: suponga que quiere preguntar, "¿tiene más datos?". Puede solicitar esto de un contenedor concurrente o de su sistema de E / S. Pero la respuesta es generalmente inaccesible y, por lo tanto, sin sentido. Entonces, ¿qué sucede si el contenedor dice "sí"? Cuando intente leer, es posible que ya no tenga datos. De manera similar, si la respuesta es "no", cuando intenta leer, los datos pueden haber llegado. La conclusión es que simplemente no existe una propiedad como "Tengo datos", ya que no puede actuar de manera significativa en respuesta a cualquier respuesta posible. (La situación es ligeramente mejor con la entrada almacenada en búfer, donde posiblemente pueda obtener un "sí, tengo datos" que constituye algún tipo de garantía, pero aún tendría que ser capaz de lidiar con el caso opuesto. Y con el resultado, la situación ciertamente es tan malo como lo describí: nunca se sabe si el disco o el búfer de la red están llenos.)
Entonces, llegamos a la conclusión de que es imposible, y de hecho poco razonable , preguntar 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 verificar si se realizó correctamente o no. En ese momento en el que interactúa con el entorno, solo entonces podrá saber si la interacción fue realmente posible, y en ese momento debe comprometerse a realizar la interacción. (Este es un "punto de sincronización", si lo desea).
EOF
Ahora llegamos a EOF. EOF es la respuesta que obtiene de un intento de operación de E / S. Significa que estaba intentando leer o escribir algo, pero al hacerlo no pudo leer o 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. Mientras las operaciones de E / S tengan éxito, simplemente no puede saber si las operaciones futuras tendrán éxito. Siempre debe primero intentar 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 utilizar el resultado de la operación de E / S, aunque el resultado toma diferentes formas y formas 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
, el número de elementos que se leyeron (que puede ser tan poco 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
, el número de elementos convertidos.C ++, extracción en formato iostreams:
for (int n; std::cin >> n; ) { consume(n); }
El resultado que debemos usar es
std::cin
sí mismo, que se puede evaluar en un contexto booleano y nos dice si el flujo todavía está en el estadogood()
.C ++, iostreams getline:
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 e incluyendo la nueva línea (o EOF si el archivo no terminó con una nueva línea).Tenga en cuenta que la función devuelve explícitamente
-1
(¡y no EOF!) Cuando se produce un error o alcanza EOF.
Puede notar que muy rara vez deletreamos la palabra real "EOF". Por lo general, detectamos la condición de error de alguna otra manera que es más interesante para nosotros de inmediato (por ejemplo, falla al realizar la E / S que deseábamos). En cada ejemplo, hay una 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 muy útil. Es mucho más detalle de lo que a menudo nos importa. Lo que importa es si la E / S tuvo éxito, más que si 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 los espacios en blanco. Usando iostreams de C ++, 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í mismo, para verificar que la extracción formateada avalue
tenido éxito. Pero luego, después de consumir también espacios en blanco, realizamos otra operación de E / S / operación,iss.get()
, y esperamos que falle como EOF, que es el caso si la cadena con formato ya ha consumido toda la cadena.En la biblioteca estándar de C, puede lograr algo similar con las funciones
strto*l
comprobando que el puntero final haya llegado al final de la cadena de entrada.
La respuesta
while(!eof)
es incorrecto porque prueba algo que es irrelevante y no prueba algo que necesitas saber. El resultado es que está ejecutando erróneamente un código que asume que está accediendo a datos que se leyeron con éxito, cuando en realidad esto nunca sucedió.
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 usted usa while (!feof(f))
. Sin embargo, esto 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 mal.
feof () indica si uno 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 próxima operación de entrada fallará (no está seguro de que la anterior haya fallado), pero si es falso, no está seguro de la próxima entrada la operación tendrá éxito. Más aún, las operaciones de entrada pueden fallar por otras razones además del final del archivo (un error de formato para la entrada con formato, una falla de E / S pura - falla del disco, tiempo de espera de la red - para todos los tipos de entrada), así que incluso si puede ser predictivo el final del archivo (y cualquiera que haya intentado implementar Ada uno, 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 antes de iniciar el manejo de la anterior), tendría que ser capaz de manejar una falla.
Por lo tanto, el idioma correcto en C es hacer un bucle con la operación IO exitosa como condición de 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) */
}