Question Pourquoi "while (! Feof (file))" est toujours faux?


J'ai vu des gens essayer de lire des fichiers comme ça 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;
    }
}

Quel est le probleme avec ca while( !feof(fp)) boucle?


456
2018-03-25 11:42


origine


Réponses:


J'aimerais 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 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 les événements simultanés. De nombreuses propriétés de l’État ne font tout simplement pas exister simultanément.

Permettez-moi de préciser ceci: Supposons que vous vouliez demander: «Avez-vous plus de données? Vous pourriez demander ceci d'un conteneur concurrent, ou de votre système d'E / S. Mais la réponse est généralement inactive, et donc sans signification. Alors 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 lire, les données peuvent être arrivées. La conclusion est qu'il y a simplement est pas de propriété comme "J'ai des données", puisque vous ne pouvez pas agir de manière significative en réponse à une réponse possible. (La situation est légèrement meilleure avec l'entrée tamponnée, où l'on pourrait éventuellement obtenir un "oui, j'ai des données" qui constitue une sorte de garantie, mais il faudrait quand même être capable de traiter le cas contraire. est certainement aussi mauvais que je l'ai décrit: vous ne savez jamais si ce disque ou ce tampon réseau est plein.)

Nous concluons donc que c'est impossible, et en fait unraisonnable, demander à un système E / S si sera capable d'effectuer une opération d'E / S. La seule façon possible d'interagir avec elle (comme avec un conteneur simultané) est de tentative l'opération et vérifiez 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 "point de synchronisation", si vous voulez.)

EOF

Nous arrivons maintenant à EOF. EOF est le réponse vous obtenez d'un tenté Opération I / O. Cela signifie que vous essayiez de lire ou d'écrire quelque chose, mais que vous ne parveniez pas à lire ou à écrire des données et qu'à la place, la fin de l'entrée ou de la sortie était 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 devez simplement ne peux pas savoir si plus loin, les opérations futures réussiront. Toi doit toujours d'abord essayer l'opération et ensuite répondre au succès ou à l'échec.

Exemples

Dans chacun des exemples, notez bien que nous premier tenter l'opération d'E / S et puis consommer le résultat s'il 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 prenne différentes formes et formes dans chaque exemple.

  • C stdio, lisez 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 le good() Etat.

  • 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) 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 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 manière qui nous intéresse plus immédiatement (par exemple, si nous ne réalisons pas autant d'E / S que nous le souhaitions). 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 d'un détail que nous nous soucions souvent. Ce qui importe est de savoir si les E / S ont réussi, plus que cela n'a échoué.

  • 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 C ++ iostreams, ça 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. Le premier est iss, l'objet de flux lui-même, pour vérifier que l'extraction formatée value réussi. Mais ensuite, après avoir consommé des espaces blancs, nous effectuons une autre opération d'E / S, iss.get(), et attendez-vous à ce qu’il échoue en tant que 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 réaliser quelque chose de similaire avec le strto*l fonctions 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 teste quelque chose qui n'est pas pertinent et ne teste pas 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.


347
2017-10-24 22:28



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 n'en 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! */
        (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 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é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ée avant que toute donnée ait été lue, elle renvoie donc false. La boucle est entrée, fgetc() est appelé (et retourne EOF), et le compte est incrémenté. alors feof() est appelée et renvoie true, provoquant l'annulation de la boucle.

Cela arrive dans tous ces cas. feof() ne retourne pas vrai jusqu'à ce 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 la fin du fichier. Si fread() renvoie 0, vous devez utiliser feof/ferror decider. De même si fgetc résultats EOF. feof() est seulement utile après le fread a retourné zéro ou fgetc est revenu EOF. Avant que cela arrive, feof() retournera toujours 0.

Il est toujours nécessaire de vérifier la valeur de retour d'une lecture (soit un fread(), ou 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() résultats 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 un contrôle à l'intérieur de la boucle pour ferror(), ou à tout le moins la condition while devrait être remplacée par while(!feof(p) && !ferror(p)) ou il y a une possibilité très réelle d'une boucle infinie, crachant probablement toutes sortes de déchets alors que des données invalides sont 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 puisse être sémantiquement correct d'écrire "while(!feof(f))"(bien qu'il y ait doit 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 vrai que c'est certainement toujours faux. Et même si un cas se présentait où ce serait correct, il est tellement idiot 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 (à moins que l'auteur soit votre patron, auquel cas la discrétion est conseillée.)


192
2018-03-25 12:39



Non, ce n'est pas toujours faux. Si votre condition de boucle est "alors que nous n'avons pas essayé de lire la fin passée du fichier" alors vous utilisez while (!feof(f)). Ce n'est cependant pas une condition de boucle commune - habituellement vous voulez tester quelque chose d'autre (comme "puis-je lire plus"). while (!feof(f)) n'est pas faux, c'est juste utilisé faux.


55
2018-03-25 11:49



feof () indique si 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 l'opération d'entrée suivante échouera (vous n'êtes pas sûr que le précédent a échoué BTW), mais si elle est fausse, vous n'êtes pas sûr de l'entrée suivante 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 la gestion de la précédente), vous devrez être capable de gérer un échec.

Donc, l'idiome correct dans C est de 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 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) */
}

26
2018-02-10 10:22



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 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
2018-06-02 01:43