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

574

J'ai vu des gens essayer de lire des fichiers comme celui-ci dans de nombreux articles récemment:

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

Quel est le problème avec cette boucle?

William Pursell
la source

Réponses:

454

Je voudrais fournir 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 vraiment "simultanément" avec votre programme. Comme pour tout ce qui est simultané, 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 simultanés. De nombreuses propriétés de l' Etat ne sont pas simplement existent en même temps.

Permettez-moi de préciser ceci: supposons que vous vouliez demander: «avez-vous plus de données». Vous pouvez le demander à un conteneur simultané ou à votre système d'E / S. Mais la réponse est généralement inutilisable, et donc dénuée de sens. Et 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 lire, les données peuvent être arrivées. La conclusion est qu'il ya tout simplement estaucune 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 une entrée en mémoire tampon, où vous pourriez peut-être obtenir un "oui, j'ai des données" qui constitue une sorte de garantie, mais vous devriez toujours être en mesure de faire face au cas contraire. Et avec la sortie, la situation est certainement aussi mauvais que je l'ai décrit: on ne sait jamais si ce disque ou ce tampon réseau est plein.)

Donc , nous concluons qu'il est impossible, et en fait un raisonnable , de demander un système d' E / S si elle sera en mesure d'effectuer une opération d'E / S. La seule façon possible d'interagir avec lui (tout comme avec un conteneur simultané) est de tenter l'opération et de vérifier si elle 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. (C'est un "point de 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 tentiez de lire ou d'écrire quelque chose, mais ce faisant, vous n'avez pas réussi à lire ou à écrire des données, et à la place la fin de l'entrée ou de la sortie a été rencontrée. Cela est vrai pour pratiquement 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 d'abord essayer l'opération, puis répondre au succès ou à l'échec.

Exemples

Dans chacun des exemples, notez attentivement que nous tentons d' abord l'opération d'E / S, puis consommons 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 des formes et des formes différentes dans chaque exemple.

  • C stdio, lu depuis 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 nle nombre d'éléments qui ont été 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::cinlui - même, qui peut être évalué dans un contexte booléen et nous indique si le flux est toujours à l' good()état.

  • 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 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 kle 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 nbytesle nombre d'octets jusqu'à et y compris la nouvelle ligne (ou EOF si le fichier ne se termine pas 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, l'échec à effectuer autant d'E / S que nous le souhaitions). Dans chaque exemple, il existe une fonctionnalité d'API qui pourrait nous dire explicitement que l'état EOF a été rencontré, mais ce n'est en fait pas une information extrêmement utile. C'est beaucoup plus un détail que ce dont nous nous soucions souvent. Ce qui importe, c'est de savoir si les E / S ont réussi, plus que comment elles ont échoué.

  • Un dernier exemple qui interroge réellement l'état EOF: Supposons que vous ayez une chaîne et que vous souhaitiez tester qu'elle représente un entier dans son intégralité, sans bits supplémentaires à la fin, sauf les espaces. En utilisant les iostreams C ++, cela se passe comme suit:

    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 ici deux résultats. Le premier est iss, l'objet de flux lui-même, de vérifier que l'extraction formatée a valueréussi. Mais ensuite, après avoir également consommé des espaces, nous effectuons une autre opération d'E / S / iss.get()et nous nous attendons à ce qu'elle é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 strto*lfonctions en vérifiant que le pointeur de fin a atteint la fin de la chaîne d'entrée.

La réponse

while(!feof)est erroné car il teste quelque chose qui n'est pas pertinent et ne parvient pas à 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 à des données qui ont été lues avec succès, alors qu'en fait cela ne s'est jamais produit.

Kerrek SB
la source
34
@CiaPan: Je ne pense pas que ce soit vrai. C99 et C11 le permettent.
Kerrek SB
11
Mais ANSI C ne fonctionne pas.
CiaPan
3
@JonathanMee: C'est mauvais pour toutes les raisons que je mentionne: vous ne pouvez pas regarder vers l'avenir. Vous ne pouvez pas dire ce qui se passera à l'avenir.
Kerrek SB
3
@JonathanMee: Oui, ce serait approprié, mais vous pouvez généralement combiner cette vérification dans l'opération (car la plupart des opérations iostreams renvoient l'objet de flux, qui lui-même a une conversion booléenne), et de cette façon, vous rendez évident que vous n'êtes pas ignorant la valeur de retour.
Kerrek SB
4
Le troisième paragraphe est remarquablement trompeur / inexact pour une réponse acceptée et très appréciée. feof()ne "demande pas au système d'E / S s'il a plus de données". feof(), selon la page de manuel (Linux) : "teste l'indicateur de fin de fichier pour le flux pointé par stream, retournant différent de zéro s'il est défini." (également, un appel explicite à clearerr()est le seul moyen de réinitialiser cet indicateur); À cet égard, la réponse de William Pursell est bien meilleure.
Arne Vogel
234

C'est faux parce que (en l'absence d'erreur de lecture), il entre dans la boucle une fois de plus que ce que l'auteur 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 <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 plus grand que le nombre de caractères dans le flux d'entrée (en supposant qu'aucune erreur de lecture). Considérez 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 la lecture des données, il renvoie donc false. La boucle est entrée, fgetc()est appelée (et retourne EOF) et le nombre est incrémenté. Puis feof()est appelé et renvoie true, provoquant l'abandon de la boucle.

Cela se produit dans tous ces cas. feof()ne renvoie vrai qu'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 distinguer entre une erreur de lecture et avoir atteint la fin du fichier. Si fread()renvoie 0, vous devez utiliser feof/ ferrorpour décider si une erreur a été rencontrée ou si toutes les données ont été consommées. De même si fgetcretourne EOF. feof()n'est utile que lorsque fread a renvoyé zéro ou fgetcest revenu EOF. Avant cela, feof()renvoie 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érons le cas où une erreur de lecture se produit. Dans ce cas, fgetc()renvoie EOF, feof()renvoie faux et la boucle ne se termine jamais. Dans tous les cas où il while(!feof(p))est utilisé, il doit y avoir au moins une vérification à l'intérieur de la boucle ferror(), ou tout au moins la condition while doit être remplacée par while(!feof(p) && !ferror(p))ou il existe une possibilité très réelle de boucle infinie, déversant probablement toutes sortes de déchets comme 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 doit y avoir 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 vrai que c'est presque certainement toujours faux. Et même si un cas se présentait où il serait correct, il est si idiomatiquement 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 (sauf si l'auteur est votre patron, auquel cas la discrétion est conseillée.)

William Pursell
la source
7
Bien sûr, c'est faux - mais à part ça, ce n'est pas "horriblement laid".
nobar
89
Vous devriez ajouter un exemple de code correct, car j'imagine que beaucoup de gens viendront ici à la recherche d'une solution rapide.
jleahy
6
@Thomas: Je ne suis pas un expert en C ++, mais je pense que file.eof () renvoie effectivement le même résultat que feof(file) || ferror(file), donc c'est très différent. Mais cette question n'est pas destinée à s'appliquer au C ++.
William Pursell
6
@ m-ric qui n'est pas correct non plus, car vous essayerez toujours de traiter une lecture qui a échoué.
Mark Ransom
4
c'est la vraie réponse correcte. feof () est utilisé pour connaître le résultat de la tentative de lecture précédente. Par conséquent, vous ne voulez probablement pas l'utiliser comme condition de rupture de boucle. +1
Jack
63

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", vous utilisez while (!feof(f)). Ce n'est cependant pas une condition de boucle courante - généralement, vous voulez tester autre chose (comme "puis-je en savoir plus"). while (!feof(f))ce n'est pas faux, c'est juste mal utilisé .

Erik
la source
1
Je me demande ... f = fopen("A:\\bigfile"); while (!feof(f)) { /* remove diskette */ }ou (va tester cela)f = fopen(NETWORK_FILE); while (!feof(f)) { /* unplug network cable */ }
pmg
1
@pmg: Comme dit, "pas une condition de boucle commune" hehe. Je ne peux pas vraiment penser à un cas où j'en ai eu besoin, généralement je suis intéressé par "pourrais-je lire ce que je voulais" avec tout ce qui implique la gestion des erreurs
Erik
@pmg: Comme dit, vous en avez rarement enviewhile(!eof(f))
Erik
9
Plus précisément, la condition est "alors que nous n'avons pas essayé de lire après la fin du fichier et qu'il n'y a pas eu d'erreur de lecture" feofne concerne pas la détection de la fin du fichier; il s'agit de déterminer si une lecture a été courte à cause d'une erreur ou parce que l'entrée est épuisée.
William Pursell
35

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 a échoué BTW), mais si elle est fausse, 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 une entrée formatée, une panne pure d'E / S - panne de disque, délai d'expiration du réseau - pour tous les types d'entrée), donc même si vous pouviez être prédictif sur 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 avez besoin de sauter des espaces, et qu'il a des effets indésirables sur les appareils interactifs - forçant parfois l'entrée du suivant avant de commencer le traitement de la précédente),

Ainsi, l'idiome correct en C est de boucler avec le succès de l'opération d'E / S comme condition de boucle, puis de 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) */
}
AProgrammer
la source
2
Arriver à la fin d'un fichier n'est pas une erreur, je remets donc en question le libellé "les opérations d'entrée peuvent échouer pour d'autres raisons que la fin du fichier".
William Pursell
@WilliamPursell, atteindre l'eof n'est pas nécessairement une erreur, mais être incapable d'effectuer une opération d'entrée à cause de eof en est une. Et il est impossible en C de détecter de manière fiable l'eof sans avoir fait échouer une opération d'entrée.
AProgrammer
D'accord dernier elsepas possible avec sizeof(line) >= 2et fgets(line, sizeof(line), file)mais possible avec pathologique size <= 0et fgets(line, size, file). Peut-être même possible avec sizeof(line) == 1.
chux
1
Tout ce discours sur la "valeur prédictive" ... Je n'y ai jamais pensé de cette façon. Dans mon monde, feof(f)cela ne prédit rien. Il indique qu'une opération PRÉCÉDENTE a atteint la fin du fichier. Ni plus ni moins. Et s'il n'y avait pas eu d'opération précédente (juste l'ouvrir), il ne signale pas la fin du fichier même si le fichier était vide au départ. Donc, à part l'explication de la concurrence dans une autre réponse ci-dessus, je ne pense pas qu'il y ait de raison de ne pas continuer feof(f).
BitTickler
@AProgrammer: A « lire jusqu'à N octets » demande que les rendements zéro, que ce soit à cause d'un EOF « permanent » ou parce que plus de données est disponible encore , ne sont pas une erreur. Bien que feof () ne puisse pas prédire de manière fiable que les demandes futures produiront des données, il peut indiquer de manière fiable que les demandes futures ne le seront pas . Il devrait peut-être y avoir une fonction de statut qui indiquerait "Il est plausible que les futures demandes de lecture aboutissent", avec une sémantique selon laquelle, après la lecture à la fin d'un fichier ordinaire, une implémentation de qualité devrait dire que les futures lectures ne réussiront probablement pas en l' absence de raison. croient qu'ils pourraient .
supercat
0

feof()n'est pas très intuitif. À mon humble avis, l'état FILEde fin de fichier doit être défini sur truesi une opération de lecture aboutit à la fin du fichier. Au lieu de cela, vous devez vérifier manuellement si la fin du fichier a été atteinte après chaque opération de lecture. Par exemple, quelque chose comme ça fonctionnera si vous lisez à partir d'un fichier texte en utilisant fgetc():

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

Ce serait génial si quelque chose comme ça fonctionnait à la place:

#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
la source
1
printf("%c", fgetc(in));? C'est un comportement indéfini. fgetc()renvoie int, non char.
Andrew Henle
Il me semble que l'idiome standard while( (c = getchar()) != EOF)est vraiment "quelque chose comme ça".
William Pursell
while( (c = getchar()) != EOF)fonctionne sur l'un de mes postes de travail exécutant GNU C 10.1.0, mais échoue sur mon Raspberry Pi 4 exécutant GNU C 9.3.0. Sur mon RPi4, il ne détecte pas la fin du fichier et continue simplement.
Scott Deagan
@AndrewHenle Vous avez raison! Passer char caux int ctravaux! Merci!!
Scott Deagan