Pourquoi «while (! Feof (file))» est toujours faux?

J’ai vu des gens qui essayaient de lire des fichiers comme ça dans beaucoup de messages récemment.

Code

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

Quel est le problème avec cette boucle while( !feof(fp)) ?

J’aimerais donner une perspective abstraite de haut niveau.

Concurrence et simultanéité

Les opérations d’E / S interagissent avec l’environnement. L’environnement ne fait pas partie de votre programme et n’est pas sous votre contrôle. L’environnement existe réellement “simultanément” avec votre programme. Comme pour toutes les choses concurrentes, les questions sur «l’état actuel» n’ont pas de sens: il n’y a pas de concept de «simultanéité» entre des événements simultanés. De nombreuses propriétés d’état n’existent tout simplement pas simultanément.

Permettez-moi de préciser ceci: Supposons que vous vouliez demander “avez-vous plus de données”. Vous pourriez demander cela d’un conteneur simultané, ou de votre système d’E / S. Mais la réponse est généralement inutilisable et donc dénuée de sens. Alors, que se passe-t-il si le conteneur dit “oui” – au moment où vous essayez de lire, il se peut qu’il n’ait plus de données. De même, si la réponse est “non”, au moment où vous essayez de lire, les données peuvent être arrivées. La conclusion est qu’il n’y a tout simplement pas de propriété comme “J’ai des données”, car vous ne pouvez pas agir de manière significative en réponse à une réponse possible. (La situation est légèrement meilleure avec les entrées en mémoire tampon, où vous pourriez obtenir un “oui, j’ai des données” qui constitue une sorte de garantie, mais vous devriez toujours être capable de faire face à la situation inverse. est certainement aussi mauvais que j’ai décrit: vous ne savez jamais si ce disque ou ce tampon réseau est plein.)

Nous concluons donc qu’il est impossible, et en fait peu raisonnable , de demander à un système d’E / S s’il sera capable d’effectuer une opération d’E / S. La seule manière possible d’interagir avec elle (comme avec un conteneur simultané) consiste à tenter l’opération et à vérifier si elle a réussi ou échoué. Au moment où vous interagissez avec l’environnement, alors et seulement alors, vous pouvez savoir si l’interaction était réellement possible et à ce stade, vous devez vous engager à effectuer l’interaction. (Ceci est un “sharepoint synchronisation”, si vous voulez.)

EOF

Nous arrivons maintenant à EOF. EOF est la réponse que vous obtenez d’une tentative d’ opération d’E / S. Cela signifie que vous essayez de lire ou d’écrire quelque chose, mais lorsque vous faites cela, vous ne parvenez pas à lire ou à écrire des données et la fin de l’entrée ou de la sortie a été rencontrée. Cela est vrai essentiellement pour toutes les API d’E / S, qu’il s’agisse de la bibliothèque standard C, des iostreams C ++ ou d’autres bibliothèques. Tant que les opérations d’E / S réussissent, vous ne pouvez tout simplement pas savoir si les opérations futures réussiront. Vous devez toujours essayer d’abord l’opération, puis répondre au succès ou à l’échec.

Exemples

Dans chacun des exemples, notez bien que nous essayons d’ abord l’opération d’E / S et que nous consommons ensuite le résultat s’il est valide. Notez en outre que nous devons toujours utiliser le résultat de l’opération d’E / S, bien que le résultat prenne différentes formes et formes dans chaque exemple.

  • C stdio, lu dans un fichier:

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

    Le résultat que nous devons utiliser est n , le nombre d'éléments qui ont été lus (qui peuvent être aussi petits 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 ++, extraction formatée iostreams:

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

    Le résultat que nous devons utiliser est std::cin lui-même, qui peut être évalué dans un contexte booléen et nous indique si le stream est toujours dans l'état good() .

  • C ++, iostreams getline:

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

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

  • POSIX, write(2) pour vider 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 pouvons seulement 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).

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

Vous remarquerez peut-être que nous épelons très rarement le mot "EOF". Nous détectons généralement la condition d'erreur d'une autre manière qui nous intéresse plus immédiatement (par exemple, défaut d'effectuer autant d'E / S que souhaité). Dans chaque exemple, il existe une fonctionnalité API qui peut nous dire explicitement que l’état EOF a été rencontré, mais il ne s’agit en fait pas d’une information extrêmement utile. C'est beaucoup plus un détail que nous nous soucions souvent. Ce qui compte c'est de savoir si les E / S ont réussi, plus que leur échec.

  • Un dernier exemple qui interroge réellement l’état EOF: Supposons que vous ayez une chaîne et que vous voulez tester qu’elle représente un entier dans son intégralité, sans bits supplémentaires à la fin sauf les espaces. En utilisant iStream C ++, il va comme ceci:

     std::ssortingng input = " 123 "; // example std::issortingngstream 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. Le premier est iss , l'object stream lui-même, pour vérifier que l'extraction formatée a bien réussi. Mais ensuite, après avoir également consommé des espaces, nous effectuons une autre opération d'E / S, iss.get() , et attendons son échec sous la forme 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 les fonctions strto*l en vérifiant que le pointeur de fin a atteint la fin de la chaîne d'entrée.

La réponse

while(!eof) est faux car il teste quelque chose qui n'est pas pertinent et échoue à tester quelque chose que vous devez savoir. Le résultat est que vous exécutez par erreur du code qui suppose qu'il accède aux données qui ont été lues avec succès, alors que cela ne s'est jamais produit.

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

Considérez le code suivant:

 /* WARNING: demonstration of bad coding technique*/ #include  #include  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 en imprimera systématiquement un plus grand que le nombre de caractères du stream d’entrée (en supposant qu’il n’y ait pas d’erreurs de lecture). Considérons le cas où le stream d’entrée est vide:

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

Dans ce cas, feof() est appelé avant toute lecture de données, il renvoie donc false. La boucle est entrée, fgetc() est appelée (et renvoie EOF ) et le compte est incrémenté. Ensuite, feof() est appelée et retourne true, ce qui entraîne l'annulation de la boucle.

Cela se produit dans tous les cas. feof() ne retourne pas true avant qu'une lecture sur le stream ne rencontre la fin du fichier. Le but de feof() n'est PAS de vérifier si la prochaine lecture atteindra la fin du fichier. Le but de feof() est de faire la distinction entre une erreur de lecture et la fin du fichier. Si fread() renvoie 0, vous devez utiliser feof / ferror pour décider. De même si fgetc renvoie EOF . feof() n'est utile que lorsque fread a renvoyé zéro ou que fgetc a renvoyé EOF . Avant cela, feof() retournera toujours 0.

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

Pire encore, considérez le cas où une erreur de lecture se produit. Dans ce cas, fgetc() renvoie EOF , feof() renvoie false et la boucle ne se termine jamais. Dans tous les cas où while(!feof(p)) est utilisé, il doit y avoir au moins une vérification dans la boucle pour ferror() , ou à tout le moins la condition while doit être remplacée par while(!feof(p) && !ferror(p)) ou il existe une possibilité très réelle d'une boucle infinie, crachant probablement toutes sortes d'ordures car des données non valides sont en cours de traitement.

Donc, en résumé, bien que je ne puisse pas affirmer avec certitude qu’il n’ya jamais de situation sémantiquement correcte pour écrire " while(!feof(f)) " (bien qu’il doive y avoir une autre vérification dans la boucle avec une pause pour éviter une boucle infinie sur une erreur de lecture), il est presque certain que c'est toujours le cas. Et même si un cas se présentait où il serait correct, il est tellement idiomatique que ce ne serait pas la bonne façon d’écrire le code. Quiconque voit ce code doit immédiatement hésiter et dire "c'est un bug". Et peut-être gifler l'auteur (sauf si l'auteur est votre patron, auquel cas la discrétion est conseillée.)

Non, ce n’est pas toujours faux. Si votre condition de boucle est “alors que nous n’avons pas essayé de lire la fin du fichier”, alors vous utilisez while (!feof(f)) . Ce n’est cependant pas une condition de boucle commune – vous voulez généralement tester autre chose (comme “puis-je en lire plus”). while (!feof(f)) n’est pas faux, il est juste mal utilisé .

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: s’il est vrai, vous êtes sûr que la prochaine opération d’entrée échouera (vous n’êtes pas sûr que le précédent a échoué à BTW), mais si c’est faux, vous n’êtes pas sûr de la prochaine entrée l’opération 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 les entrées formatées, un pur échec d’E / S – panne de disque, délai d’expiration du réseau – pour tous les types d’entrées). la fin du fichier (et quiconque a essayé d’implémenter Ada one, qui est prédictif, vous dira qu’il peut être complexe si vous devez ignorer des espaces, et qu’il a des effets indésirables sur les périphériques interactifs – forçant parfois la saisie du prochain ligne avant de commencer le traitement du précédent), vous devrez être capable de gérer une panne.

Ainsi, le libellé correct en C consiste à boucler avec le succès de l’opération IO en tant que condition de boucle, puis à tester la cause de l’échec. Par exemple:

 while (fgets(line, sizeof(line), file)) { /* note that fgets don't ssortingp 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) */ } 

Bonne réponse, j’ai juste remarqué la même chose parce que j’essayais de faire une boucle comme ça. Donc, c’est faux 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  #include  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 }