web-dev-qa-db-fra.com

Pourquoi «while (! Feof (fichier))» est-il toujours faux?

J'ai vu des gens essayer de lire des fichiers comme celui-ci dans beaucoup de publications 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;
    }
}

Quel est le problème avec cette boucle?

536
William Pursell

J'aimerais donner une perspective abstraite et 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 vraiment "en même temps" 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é" pour tous les événements simultanés. De nombreuses propriétés d'état n'existent tout simplement pas .

Permettez-moi de préciser la chose: supposons que vous vouliez demander "avez-vous plus de données". Vous pouvez demander cela à un conteneur simultané ou à votre système d'E/S. Mais la réponse est généralement inactive et donc dénuée de sens. Et si le conteneur disait "oui" - au moment où vous essayez de lire, il n’aura peut-être plus de données. De même, si la réponse est "non", au moment où vous essayez de lire, des données peuvent être arrivées. La conclusion est qu’il n’existe simplement aucune propriété comme "J'ai des données", car vous ne pouvez agir de manière significative en réponse à une réponse possible. (La situation est légèrement meilleure avec une entrée en mémoire tampon, où vous pouvez éventuellement obtenir un "oui, j'ai des données" qui constitue une sorte de garantie, mais vous devriez toujours pouvoir traiter le cas contraire. Et avec la sortie, la situation est certainement aussi grave que ce que je viens de décrire: on ne sait jamais si ce disque ou ce tampon réseau est plein.)

Nous concluons donc qu’il est impossible, et en fait un raisonnable , de demander à un système d’E/S s’il sera capable d’effectuer une opération d’E/S. Le seul moyen possible d'interagir avec celui-ci (comme avec un conteneur simultané) consiste à tenter l'opération et vérifier si l'opération a réussi ou échoué. À ce 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 "point de synchronisation", si vous voulez.)

EOF

Nous arrivons maintenant à EOF. EOF est la réponse obtenue après une tentative Opération d'E/S. Cela signifie que vous avez essayé de lire ou d'écrire quelque chose, mais que vous avez échoué lors de la lecture ou de l'écriture de données et que la fin de l'entrée ou de la sortie a été rencontrée. Cela concerne essentiellement 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 d'autres opérations seront couronnées de succès. Vous devez toujours essayer d'abord l'opération, puis répondre au succès ou à l'échec.

Exemples

Dans chacun des exemples, notez soigneusement que nous essayons d’abord d’opérer l’opération d’E/S et alors consomme le résultat s'il est valide. Notez en outre que nous toujours devons 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, extrait d'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 lus (qui peut être aussi petit 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 au format 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 flux est toujours 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 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 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’à 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 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, l'incapacité à effectuer autant d'entrées/sorties que nous le souhaitions). Dans chaque exemple, certaines fonctionnalités de l'API peuvent nous indiquer explicitement que l'état EOF a été rencontré, mais il ne s'agit en réalité pas d'un élément d'information extrêmement utile. C'est beaucoup plus un détail que nous nous soucions souvent. Ce qui compte, c’est de savoir si l’entrée/sortie a réussi, plus que la manière dont elle a échoué.

  • Un dernier exemple qui interroge réellement l'état EOF: Supposons que vous ayez une chaîne et que vous souhaitez vérifier qu'elle représente un entier dans son intégralité, sans aucun bit supplémentaire à la fin, à l'exception des espaces. Utilisation de C++ iostreams, cela ressemble à 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. Le premier est iss, l'objet flux lui-même, pour vérifier que l'extraction formatée vers value a réussi. Mais ensuite, après avoir également consommé des espaces, nous effectuons une autre opération I/O /, iss.get(), et nous nous attendons à ce qu'il échoue en tant qu'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 erroné car il recherche quelque chose de non pertinent et n'essaie pas de détecter quelque chose que vous devez savoir. Le résultat est que vous exécutez par erreur un code qui suppose qu’il accède à des données lues avec succès, alors que cela n’est jamais arrivé.

431
Kerrek SB

C'est faux parce que (en l'absence d'une erreur de lecture) il entre dans la boucle une fois de plus que prévu. 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! */
        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 imprimera systématiquement un nombre supérieur au nombre de caractères du flux d'entrée (en supposant qu'il n'y ait pas d'erreur de lecture). Prenons 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ée avant la lecture des données. Elle 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é et renvoie true, provoquant l’abandon de la boucle.

Cela se produit dans tous les cas. feof() ne renvoie pas vrai tant que après une lecture sur le flux 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 l’atteinte de la fin du fichier. Si fread() renvoie 0, vous devez utiliser feof/ferror pour déterminer si une erreur a été rencontrée ou si toutes les données ont été consommées. De même si fgetc renvoie EOF. feof() n'est utile que après que Fread a renvoyé zéro ou fgetc à retourné EOF. Avant que cela se produise, feof() renverra 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érons 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 à l'intérieur de la boucle pour ferror(), ou tout au moins la condition while doit être remplacée par while(!feof(p) && !ferror(p)) ou il y a une erreur. très réelle possibilité d'une boucle infinie, probablement crachant toutes sortes de déchets lorsque 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'y a jamais de situation dans laquelle il peut être sémantiquement correct d'écrire "while(!feof(f))" (bien qu'il y ait obligatoirement soit une autre vérification à l'intérieur de la boucle avec une pause pour éviter une boucle infinie sur une erreur de lecture), il est presque toujours faux. Et même si un cas se présentait, il serait tellement erroné 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 éventuellement gifler l'auteur (à moins que l'auteur ne soit votre patron, auquel cas la discrétion est conseillée.)

224
William Pursell

Non, ce n'est pas toujours faux. Si votre condition de boucle est "tant que nous n'avons pas essayé de lire après la fin du fichier", vous utilisez while (!feof(f)). Cependant, ce n'est pas une condition de boucle commune - vous voulez généralement tester quelque chose d'autre (comme "puis-je en lire plus"). while (!feof(f)) n'est pas mauvais, c'est juste tilisé faux.

60
Erik

feof() indique si une personne a tenté de lire après la fin du fichier. Cela signifie que cela a peu d'effet prédictif: si c'est vrai, vous êtes sûr que la prochaine opération d'entrée va échouer (vous n'êtes pas sûr que la précédente a échoué BTW), mais si c'est faux, vous n'êtes pas sûr de la prochaine entrée l'opération va réussir. De plus, les opérations d’entrée peuvent échouer pour des raisons autres que la fin du fichier (une erreur de formatage pour une entrée formatée, une défaillance pure IO - panne de disque, délai d’installation du réseau - pour tous les types d’entrée), donc même si vous pouviez être prédictif quant à 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 devez sauter des espaces et que cela a des effets indésirables sur les périphériques interactifs - parfois forçant l’entrée de la ligne suivante avant de commencer le traitement de la ligne précédente), vous devez être capable de gérer un échec.

Par conséquent, le langage correct dans C consiste à effectuer une boucle avec la condition de boucle IO, puis à tester la cause de l'échec. 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) */
}
32
AProgrammer

Excellente réponse, j'ai juste remarqué la même chose parce que j'essayais de faire une boucle comme celle-là. Donc, c'est faux dans ce scénario, mais si vous voulez avoir une boucle qui finit 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
}
10
tesch1