¿Por qué siempre está mal "while (! Feof (file))"?

574

Últimamente he visto personas tratando de leer archivos como este en muchas publicaciones:

#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é tiene de malo este bucle?

William Pursell
fuente

Respuestas:

454

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

Concurrencia y simultaneidad

Las operaciones de E / S interactúan con el entorno. El entorno no es parte de su programa y no está bajo su control. El entorno realmente existe "simultáneamente" con su programa. 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ítame hacer esto más preciso: suponga que quiere preguntar, "¿tiene más datos"? Puede pedir esto a un contenedor concurrente o a su sistema de E / S. Pero la respuesta generalmente es inaccesible y, por lo tanto, no tiene sentido. Entonces, ¿qué pasa si el contenedor dice "sí"? En el momento en que intente leer, es posible que ya no tenga datos. Del mismo modo, si la respuesta es "no", para el momento en que intente leer, los datos pueden haber llegado. La conclusión es que simplemente hayninguna propiedad como "Tengo datos", ya que no puede actuar de manera significativa en respuesta a cualquier respuesta posible. (La situación es un poco mejor con la entrada almacenada en el búfer, donde posiblemente podría 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 contrario. Y con la salida de la situación ciertamente es tan malo como lo describí: nunca se sabe si ese disco o ese búfer de red está lleno).

Así llegamos a la conclusión de que es imposible, y de hecho la ONU razonable , para pedir un sistema de E / S si será capaz de 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 tuvo éxito o falló. En ese momento donde interactúa con el entorno, entonces y solo entonces puede saber si la interacción fue realmente posible, y en ese punto debe comprometerse 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 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 esencialmente 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 futuras operaciones tendrán éxito. Siempre debe intentar primero la operación y luego responder al éxito o al fracaso.

Ejemplos

En cada uno de los ejemplos, tenga en cuenta 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 toma diferentes formas y formas en cada ejemplo.

  • C stdio, leído 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 nel número de elementos que se leyeron (que puede ser tan pequeño 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 con formato iostreams:

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

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

  • C ++, getline 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 kel 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 nbytesel número de bytes hasta la nueva línea (o EOF si el archivo no terminó con una nueva línea).

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

Puede notar que rara vez deletreamos la palabra real "EOF". Por lo general, detectamos la condición de error de alguna otra manera que nos interesa más inmediatamente (p. Ej., No realizar tantas E / S como hubiéramos deseado). En cada ejemplo, hay alguna característica de API que podría decirnos explícitamente que se ha encontrado el estado EOF, pero esta 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 de lo que 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 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í mismo, para comprobar que la extracción formateada se realizó correctamente value. 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 toda la cadena ya ha sido consumida por la extracción formateada.

    En la biblioteca estándar de C, puede lograr algo similar con las strto*lfunciones al verificar que el puntero final haya llegado al final de la cadena de entrada.

La respuesta

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

Kerrek SB
fuente
34
@CiaPan: No creo que sea verdad. Tanto C99 como C11 lo permiten.
Kerrek SB
11
Pero ANSI C no.
CiaPan
3
@ JonathanMee: Es malo por todas las razones que menciono: no se puede mirar hacia el futuro. No puedes decir lo que sucederá en el futuro.
Kerrek SB
3
@ JonathanMee: Sí, eso sería apropiado, aunque generalmente puede combinar esta verificación en la operación (ya que la mayoría de las operaciones de iostreams devuelven el objeto de flujo, que en sí tiene una conversión booleana), y de esa manera usted hace obvio que no está ignorando el valor de retorno.
Kerrek SB
44
El tercer párrafo es notablemente engañoso / inexacto para una respuesta aceptada y altamente votada. feof()no "pregunta al sistema de E / S si tiene más datos". feof(), de acuerdo con la página de manual (Linux) : "prueba el indicador de fin de archivo para la secuencia a la que apunta la secuencia, devolviendo un valor distinto de cero si está configurado". (también, una llamada explícita a clearerr()es la única forma de restablecer este indicador); A este respecto, la respuesta de William Pursell es mucho mejor.
Arne Vogel
234

Está mal porque (en ausencia de un error de lectura) ingresa al bucle una vez más de lo que el autor espera. 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;
    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;
}

Este programa imprimirá uno mayor que el número de caracteres en la secuencia de entrada (suponiendo 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, feof()se llama antes de que se haya leído ningún dato, por lo que devuelve falso. Se ingresa el ciclo, fgetc()se llama (y se devuelve EOF), y se incrementa el conteo. Luego feof()se llama y devuelve verdadero, lo que hace que el ciclo se cancele.

Esto sucede en todos estos casos. feof()no devuelve verdadero hasta después de 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/ ferrorpara decidir si se encontró un error o si se consumieron todos los datos. Del mismo modo si fgetcvuelve EOF. feof()solo es útil después de que fread haya devuelto cero o fgetchaya regresado EOF. Antes de que eso suceda, feof()siempre devolverá 0.

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

Peor aún, considere el caso donde ocurre un error de lectura. En ese caso, fgetc()devuelve EOF, feof()devuelve falso y el ciclo nunca termina. En todos los casos en que while(!feof(p))se usa, debe haber al menos una verificación dentro del bucle ferror(), o al menos la condición while debe reemplazarse 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.

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 bucle con una interrupción para evitar un bucle infinito en un error de lectura ), es casi seguro que siempre está mal. E incluso si surgiera un caso en el que sería correcto, es tan idiomáticamente incorrecto que no sería la forma correcta de escribir el código. Cualquiera que vea ese código debe 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).

William Pursell
fuente
77
Claro que está mal, pero aparte de eso no es "muy feo".
nobar
89
Debería agregar un ejemplo de código correcto, ya que imagino que mucha gente vendrá aquí buscando una solución rápida.
jleahy
66
@Thomas: no soy un experto en C ++, pero creo que file.eof () devuelve efectivamente el mismo resultado que feof(file) || ferror(file), por lo que es muy diferente. Pero esta pregunta no pretende ser aplicable a C ++.
William Pursell
66
@ m-ric tampoco está bien, porque aún intentarás procesar una lectura que falló.
Mark Ransom
44
Esta es la respuesta correcta real. feof () se utiliza para conocer el resultado del intento de lectura anterior. Por lo tanto, probablemente no quiera usarlo como su condición de ruptura de bucle. +1
Jack
63

No, no siempre está mal. Si su condición de bucle es "mientras no hemos tratado de 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 algo más (como "¿puedo leer más"). while (!feof(f))no está mal, solo se usa mal.

Erik
fuente
1
Me pregunto ... f = fopen("A:\\bigfile"); while (!feof(f)) { /* remove diskette */ }o (voy a probar esto)f = fopen(NETWORK_FILE); while (!feof(f)) { /* unplug network cable */ }
pmg
1
@pmg: Como se dijo, "no es una condición de bucle común" jeje. Realmente no puedo pensar en cualquier caso que he necesitado, por lo general estoy interesado en "pude leer lo que quería", con todo lo que implica de control de errores
Erik
@pmg: Como se dijo, rara vez quiereswhile(!eof(f))
Erik
99
Más exactamente, la condición es "mientras no hemos tratado de leer más allá del final del archivo y no hubo error de lectura" feofno se trata de detectar el final del archivo; se trata de determinar si una lectura fue corta debido a un error o porque la entrada está agotada.
William Pursell
35

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 fallara, por cierto), pero si es falsa, no está seguro de la siguiente 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 la entrada formateada, una falla de E / S pura - falla del disco, tiempo de espera de la red - para todos los tipos de entrada), por lo que incluso si pudiera predecir el 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 no deseados en dispositivos interactivos, a veces forzando la entrada del siguiente línea antes de comenzar el manejo del anterior),

Entonces, la expresión correcta en C es hacer un bucle 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) */
}
Un programador
fuente
2
Llegar al final de un archivo no es un error, por lo que cuestiono la fraseología "las operaciones de entrada pueden fallar por otras razones que no sean el final del archivo".
William Pursell
@WilliamPursell, alcanzar el eof no es necesariamente un error, pero no es posible realizar una operación de entrada debido a eof. Y en C es imposible detectar de manera confiable el eof sin haber fallado una operación de entrada.
Programador
De acuerdo por último elseno es posible con sizeof(line) >= 2y fgets(line, sizeof(line), file)pero posible con patológico size <= 0y fgets(line, size, file). Quizás incluso posible consizeof(line) == 1 .
chux - Restablecer Monica
1
Toda esa charla de "valor predictivo" ... Nunca lo pensé de esa manera. En mi mundo, feof(f)NO PREDICE nada. Establece que una operación ANTERIOR ha llegado al final del archivo. Nada más y nada menos. Y si no hubo una operación previa (solo la abrió), no informa el final del archivo, incluso si el archivo estaba vacío para empezar. Entonces, aparte de la explicación de concurrencia en otra respuesta anterior, no creo que haya ninguna razón para no seguir feof(f).
BitTickler
@AProgrammer: una solicitud de "lectura de hasta N bytes" que produce cero, ya sea debido a un EOF "permanente" o porque todavía no hay más datos disponibles , no es un error. Si bien feof () puede no predecir de manera confiable que las solicitudes futuras arrojarán datos, puede indicar de manera confiable que las solicitudes futuras no lo harán . Tal vez debería haber una función de estado que indique "Es plausible que futuras solicitudes de lectura tengan éxito", con la semántica de que después de leer hasta el final de un archivo ordinario, una implementación de calidad debería decir que es poco probable que las futuras lecturas tengan éxito sin alguna razón para creen que podrían .
supercat
0

feof()No es muy intuitivo. En mi muy humilde opinión, el estado FILEde fin de archivo debe establecerse en truesi cualquier operación de lectura resulta en el final del archivo. En su lugar, debe verificar manualmente si se alcanzó el final del archivo después de cada operación de lectura. Por ejemplo, algo como esto funcionará si lee de un archivo de texto usandofgetc() :

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

Sería genial si algo como esto funcionara en su lugar:

#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;
}
Scott Deagan
fuente
1
printf("%c", fgetc(in));? Ese es un comportamiento indefinido. fgetc()vuelve int, no char.
Andrew Henle
Me parece que el idioma estándar while( (c = getchar()) != EOF)es mucho "algo como esto".
William Pursell
while( (c = getchar()) != EOF)funciona en uno de mi escritorio con GNU C 10.1.0, pero falla en mi Raspberry Pi 4 con GNU C 9.3.0. En mi RPi4, no detecta el final del archivo y simplemente continúa.
Scott Deagan
@ AndrewHenle ¡Tienes razón! Cambiando char ca int cobras! ¡¡Gracias!!
Scott Deagan