Pourquoi le "while (!feof (fichier))" toujours tort?

j'ai vu des gens essayer de lire des fichiers comme celui-ci dans beaucoup de messages ces derniers temps.

Code

#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-ce qui ne va pas avec cette boucle while( !feof(fp)) ?

470
demandé sur William Pursell 2011-03-25 14:42:33

5 réponses

j'aimerais vous présenter une perspective abstraite et de haut niveau.

simultanéité et simultanéité

opérations d'e/S interagir avec l'environnement. L'environnement ne fait pas partie de votre programme, et non pas sous votre contrôle. L'environnement existe vraiment "simultanément" avec votre programme. Comme pour tout ce qui est concurrent, les questions sur "l'état actuel" n'ont pas de sens: il n'y a pas de concept de "simultanéité" entre les événements concurrents. Beaucoup les propriétés de l'état tout simplement ne existent concurremment.

permettez-moi de préciser: supposez que vous voulez demander, "Avez-vous plus de données". Vous pouvez demander cela d'un conteneur concurrent, ou de votre système D'e/s. Mais la réponse est généralement inacceptable, et donc dénuée de sens. Ainsi, que faire si le conteneur dit "oui" – au moment où vous essayez de lire, il peut ne plus avoir de données. De même, si la réponse est "non", au moment où vous essayez de lecture, les données peuvent avoir arriver. La conclusion est qu'il simplement est aucune propriété comme" j'ai des données", car vous ne pouvez pas agir de manière significative en réponse à toute réponse possible. (La situation est légèrement meilleure avec tampon d'entrée, où vous pourriez éventuellement obtenir un "oui, j'ai des données" qui constitue une sorte de garantie, mais vous auriez encore être en mesure de traiter le cas contraire. Et avec la sortie la situation est certainement aussi mauvaise que je l'ai décrit: on ne sait jamais si ce disque ou le tampon réseau est plein.)

nous pouvons Donc conclure qu'il est impossible, et, en fait, de l'onu raisonnable , à demander à un système d'e/S s'il sera capable d'effectuer une opération d'e/S. La seule façon possible d'interagir avec elle (tout comme avec un conteneur concurrent) est de tenter l'opération et de vérifier si elle a réussi ou échoué. Au moment où vous interagissez avec l'environnement, puis et c'est seulement alors que vous pouvez savoir si l'interaction était réellement possible, et à ce moment-là vous devez vous engager à effectuer l'interaction. (C'est une "synchronisation", si vous voulez.)

expressions du FOLKLORE

maintenant nous arrivons à L'EOF. EOF est le réponse vous obtenez d'un tentative opération D'e / s. Cela signifie que vous essayiez de lire ou d'écrire quelque chose, mais en le faisant, vous avez échoué à lire ou à écrire données, et à la place la fin de l'entrée ou de la sortie a été rencontrée. Cela est vrai pour l'essentiel de tous les API D'e / s, qu'il s'agisse de la bibliothèque C standard, de C++ iostream ou d'autres bibliothèques. Tant que les opérations D'E/S réussiront, vous ne pouvez pas savoir si d'autres opérations futures réussiront. Vous doit toujours d'abord essayer l'opération et puis répondre à la réussite ou l'échec.

Exemples

dans chacun des exemples, notez attentivement que nous d'abord essayer l'opération d'E/S et puis consommer le résultat si elle est valide. Notez en outre que nous toujours doit utiliser le résultat de l'opération d'e/s, bien que le résultat prend différentes formes et formes dans chaque exemple.

  • C stdio, lire à partir d'un fichier:

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

    le résultat nous devons utiliser est n , le nombre d'éléments qui ont été lus (qui peut être aussi peu que zéro).

  • c stdio, scanf :

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

    le résultat que nous devons utiliser est la valeur de retour de scanf , le nombre d'éléments convertis.

  • C++, iostreams formaté extraction:

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

    le résultat que nous devons utiliser est std::cin elle - même, qui peut être évaluée dans un contexte booléen et nous dit si le flux est encore dans l'état good() .

  • C++, iostreams getline:

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

    le résultat que nous devons utiliser est à nouveau std::cin , comme avant.

  • POSIX, write(2) pour rincer un tampon:

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

    Le résultat que nous utilisons ici est k , le nombre d'octets écrits. Le point ici est que nous ne pouvons savoir combien d'octets ont été écrits après l'opération d'écriture.

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

    le résultat que nous devons utiliser est nbytes , le nombre d'octets jusqu'à et y compris la nouvelle ligne (ou EOF si le fichier ne s'est pas terminé par une nouvelle ligne).

    noter que la fonction renvoie explicitement -1 (et non EOF!) lorsqu'une erreur se produit ou qu'elle atteint EOF.

vous remarquerez que nous épelons très rarement le vrai mot"EOF". Nous détectons habituellement la condition d'erreur d'une autre manière qui est plus immédiatement intéressante pour nous (par exemple l'incapacité à effectuer autant d'e/s que nous l'avions souhaité). Dans chaque exemple, il y a une fonction API qui pourrait nous indiquer explicitement que L'état EOF a été rencontré, mais c'est dans le fait n'est pas une information très utile. C'est un détail bien plus important que ce dont nous nous soucions souvent. Ce qui importe, c'est de savoir si le premier lieutenant a réussi, plus que de savoir comment il a échoué.

  • un dernier exemple qui interroge en fait L'état EOF: supposons que vous ayez une chaîne de caractères et que vous vouliez tester qu'elle représente un entier dans son intégralité, sans bits supplémentaires à la fin sauf les espaces. En utilisant C++ iostreams, il va comme ceci:

    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
    }
    

    Nous utilisons deux résultats ici. La première est iss , l'objet stream lui-même, pour vérifier que l'extraction formatée à value réussi. Mais ensuite, après avoir également consommé de l'espace, nous effectuons une autre opération d'e / s, iss.get() , et nous nous attendons à ce qu'elle échoue comme EOF, ce qui est le cas si la chaîne entière a déjà été consommée par l'extraction formatée.

    dans la bibliothèque standard C vous pouvez obtenir quelque chose de similaire avec le strto*l fonctions en vérifiant que la fin pointeur a atteint la fin de la chaîne d'entrée.

la réponse

while(!eof) est erroné parce qu'il teste pour quelque chose qui n'est pas pertinent et échoue à tester pour quelque chose que vous devez savoir. Le résultat est que vous exécutez par erreur du code qui suppose qu'il est l'accès aux données qui a été lu avec succès, alors qu'en fait, cela ne s'est jamais produit.

364
répondu Kerrek SB 2017-05-24 15:58:34

C'est faux parce que (en l'absence d'une erreur de lecture), il entre dans la boucle une fois de plus que l'auteur attend. S'il y a une erreur de lecture, la boucle ne se termine jamais.

Considérons le code suivant:

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

ce programme affichera systématiquement un nombre de caractères supérieur au nombre de caractères dans le flux d'entrée (en supposant qu'il n'y ait pas d'erreurs de lecture). Considérons le cas où le flux d'entrée est vide:

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

dans ce cas, feof() est appelé avant qu'aucune donnée n'ait été lue, donc il retourne false. La boucle est saisie, fgetc() est appelé (et retourne EOF ), et le compte est incrémenté. Puis feof() est appelé et retourne true, provoquant l'abandon de la boucle.

cela se produit dans tous ces cas. feof() ne retourne pas true avant après une lecture sur le ruisseau rencontre la fin du fichier. Le but de feof() est Pas pour vérifier si la prochaine lecture atteindra la fin du fichier. Le but de feof() est de distinguer entre une erreur de lecture et avoir atteint la fin du fichier. Si fread() renvoie 0, Vous devez utiliser feof / ferror pour décider. De même si fgetc renvoie EOF . feof() est seulement utile après fread a retourné zéro ou fgetc a retourné EOF . Avant que cela n'arrive, feof() retournera toujours 0.

il est toujours nécessaire de vérifier la valeur de retour d'une lecture (soit une fread() , soit une fscanf() , soit une fgetc() ) avant d'appeler feof() .

pire encore, considérez le cas où une erreur de lecture se produit. Dans ce cas, fgetc() retourne EOF , feof() retourne false, et la boucle ne se termine jamais. Dans tous les cas où while(!feof(p)) est utilisé, il doit y avoir au moins un contrôle à l'intérieur de la boucle pour ferror() , ou à tout le moins, la condition while devrait être remplacé par while(!feof(p) && !ferror(p)) ou il ya une possibilité très réelle d'une boucle infinie, probablement spewing toutes sortes de déchets pendant que les données invalides est en cours de traitement.

donc, en résumé, bien que je ne puisse pas affirmer avec certitude qu'il n'y a jamais une situation dans laquelle il peut être sémantiquement correct d'écrire " while(!feof(f)) "(bien qu'il doit être un autre contrôle à l'intérieur de la boucle avec une pause pour éviter une la boucle infinie sur une erreur de lecture), c'est le cas, elle est presque certainement toujours tort. Et même si un cas se présentait où ce serait correct, c'est tellement faux que ce ne serait pas la bonne façon d'écrire le code. Quiconque voit ce code devrait immédiatement hésiter et dire, "c'est un bug". Et peut-être gifler l'auteur (Sauf si l'auteur est votre patron dans ce cas, la discrétion est conseillée.)

199
répondu William Pursell 2018-03-16 23:54:25

non ce n'est pas toujours mal. Si votre condition de boucle est "alors que nous n'avons pas essayé de lire au-delà de la fin de fichier" puis while (!feof(f)) . Ce n'est toutefois pas une condition de boucle commune - habituellement, vous voulez tester pour quelque chose d'autre (comme "Puis-je lire plus"). while (!feof(f)) n'est pas mal, c'est juste utilisé faux.

57
répondu Erik 2011-03-25 11:49:12

feof () indique si l'on a essayé de lire après la fin du fichier. Cela signifie qu'il a peu d'effet prédictif: si c'est vrai, vous êtes sûr que la prochaine opération d'entrée échouera (vous n'êtes pas sûr que la précédente ait échoué BTW), mais si elle est fausse, vous n'êtes pas sûr que la prochaine opération d'entrée réussira. De plus, les opérations d'entrée peuvent échouer pour d'autres raisons que la fin du fichier (une erreur de format pour l'entrée formatée, un échec D'IO pur -- échec de disque, délai réseau -- pour toutes les entrées donc, même si vous pouvez prédire la fin du fichier (et quiconque a essayé D'implémenter Ada one, qui est prédictif, vous dira que cela peut être complexe si vous avez besoin de sauter des espaces, et que cela a des effets indésirables sur les appareils interactifs -- en forçant parfois l'entrée de la ligne suivante avant de commencer la manipulation de la précédente), vous devriez être capable de gérer un échec.

donc l'idiome correct en C est de boucler avec le succès de L'opération IO comme boucle et ensuite tester la cause de la défaillance. Par exemple:

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) */
}
27
répondu AProgrammer 2012-02-10 10:22:04

Grande réponse, j'ai juste remarqué la même chose parce que j'ai essayé de faire une boucle comme ça. Donc, c'est mal dans ce scénario, mais si vous voulez avoir une boucle qui se termine gracieusement à L'EOF, c'est une bonne façon de le faire:

#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
}
9
répondu tesch1 2013-06-03 02:47:25