Saltar al contenido

¿Por qué “while (! Feof (archivo))” siempre es incorrecto?

Solución:

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

Simultaneidad y concurrencia

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 existe realmente “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 del estado simplemente no existe al mismo tiempo.

Permítame hacer esto más preciso: suponga que quiere preguntar “¿tiene más datos”? Puede preguntar esto a un contenedor concurrente o a su sistema de E / S. Pero la respuesta es generalmente inaccesible y, por lo tanto, sin sentido. Entonces, ¿qué pasa si el contenedor dice “sí”? Para cuando intente leer, es posible que ya no tenga datos. De manera similar, si la respuesta es “no”, para cuando intente leer, es posible que los datos hayan llegado. La conclusión es que simplemente es ninguna 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 búfer, donde posiblemente podría 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).

Entonces llegamos a la conclusión de que es imposible, y de hecho unrazonable, para preguntarle a un sistema de E / S si estarán 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 intento la operación y verifique si tuvo éxito o no. 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. (Este es un “punto de sincronización”, por así decirlo).

EOF

Ahora llegamos a EOF. EOF es el respuesta obtienes de un intentó 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 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, iostreams de C ++ u otras bibliotecas. Siempre que las operaciones de E / S tengan éxito, simplemente no puedo saber si además, las operaciones futuras tendrán éxito. usted debe Siempre intente primero la operación y luego responda al éxito o al fracaso.

Ejemplos de

En cada uno de los ejemplos, observe cuidadosamente que primero intente la operación de E / S y luego consumir el resultado si es válido. Tenga en cuenta además que nosotros siempre debe usar 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 == 0) { break; }
      }
    

El resultado que debemos utilizar es n, el 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 utilizar es std::cin sí mismo, que se puede evaluar en un contexto booleano y nos dice si la secuencia todavía está en el good() 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 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 utilizar es nbytes, el número de bytes hasta e incluyendo el salto de línea (o EOF si el archivo no terminó con un salto de 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 pocas veces deletreamos la palabra “EOF”. Por lo general, detectamos la condición de error de alguna otra manera que nos resulte más interesante de inmediato (por ejemplo, no realizar todas las E / S que hubiéramos deseado). En cada ejemplo, 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 muy ú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 en realidad 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 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í, para comprobar que la extracción formateada a value tuvo éxito. Pero luego, después de consumir también espacios en blanco, realizamos otra operación de E / S /, iss.get()y espere 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 puede lograr algo similar con la strto*l funciona comprobando que el puntero final ha 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 erróneamente un código que asume que está accediendo a datos que se leyeron correctamente, cuando en realidad esto nunca sucedió.

Está mal porque (en ausencia de un error de lectura) entra en el ciclo 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;
    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: %un", 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á constantemente uno más que el número de caracteres en el flujo de entrada (asumiendo que no hay 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 hace que el bucle se interrumpa.

Esto sucede en todos esos casos. feof() no vuelve verdadero hasta después una lectura en la secuencia encuentra el final del archivo. El propósito de feof() NO es comprobar si la siguiente 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. Similarmente si fgetc devoluciones EOF. feof() solo es útil después fread ha devuelto cero o fgetc ha regresado EOF. Antes de que eso suceda, feof() siempre devolverá 0.

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

Peor aún, considere el caso en el que se produce un error de lectura. En ese caso, fgetc() devoluciones EOF, feof() devuelve falso y el bucle nunca termina. En todos los casos donde while(!feof(p)) se utiliza, debe haber al menos una marca dentro del bucle para ferror(), o al menos la condición while debe ser reemplazada por while(!feof(p) && !ferror(p)) o existe 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 hay una situación en la que pueda ser semánticamente correcto escribir “while(!feof(f))“(aunque hay debe ser otra comprobación dentro del ciclo con una ruptura para evitar un ciclo infinito en un error de lectura), es casi seguro que siempre es incorrecto. E incluso si alguna vez 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 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 aconseja discreción).

No, no siempre está mal. Si su condición de bucle es “aunque no hemos intentado leer más allá del final del archivo”, utilice while (!feof(f)). Sin embargo, esta no es una condición de ciclo común; por lo general, desea probar algo más (como “¿puedo leer más?”). while (!feof(f)) no está mal, es solo usó incorrecto.

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *