680 votes

« alors que ( ! feof (fichier)) » est toujours mauvaise

J’ai commencé à voir `` dans beaucoup de messages ces derniers temps, et je n’ai pas trouvé un bon lien à une référence d’expliquer pourquoi c’est faux. Donc je pensais que je prendrais un coup de poignard à expliquer ici.

549voto

Kerrek SB Points 194696

J'aimerais fournir un résumé, une perspective de haut niveau.

La simultanéité et la 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 avec toutes les choses simultanées, des questions sur "l'état actuel" ne faites pas de sens: Il n'y a pas de notion de "simultanéité" à travers les événements simultanés. De nombreuses propriétés de l'etat n'ont tout simplement pas exister en même temps.

Permettez-moi de rendre cela plus précis: Supposons que vous souhaitez demander, "vous avez plus de données". Vous pourriez vous demander ce de un conteneur simultané, ou de votre système d'e/S. Mais la réponse est généralement unactionable, et donc vide de sens. De sorte que si le conteneur dit "oui" – au moment où vous essayez de lire, il ne peut 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 être arrivés. La conclusion est que, n' est pas la propriété comme "j'ai des données", puisque vous ne pouvez pas agir de manière significative dans la réponse à toute les réponses possibles. (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 de la situation est certainement tout aussi mauvais que j'ai décrit: vous ne savez jamais si ce disque ou réseau de la mémoire tampon est pleine.)

Donc, nous concluons qu'il est impossible, et, en fait, de l'onuraisonnable, de demander à un système d'e/S s'il sera capable d'effectuer une opération d'e/S. La seule façon de communiquer avec elle (tout comme avec un conteneur simultané) est de 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 pouvez-vous savoir si l'interaction est en fait possible, et à ce point que vous devez vous engager à l'exécution de l'interaction. (C'est une "synchronisation", si vous voulez.)

EOF

Nous en arrivons maintenant à l'EOF. EOF est la réponse que vous obtenez à partir d'une tentative d' opération d'e/S. Cela signifie que vous étiez en train de lire ou d'écrire quelque chose, mais en faisant de sorte que vous avez omis de lire ou d'écrire des données, et au lieu de la fin de l'entrée ou de la sortie a été rencontrée. Cela est vrai pour pratiquement tous les I/O Api, que ce soit la bibliothèque standard C, C++ iostreams, ou d'autres bibliothèques. Aussi longtemps que les opérations d'e/S à réussir, vous simplement ne peut pas savoir si d'autres, les opérations à venir va réussir. Vous devez toujours essayer d'abord le fonctionnement et répondre ensuite à la réussite ou à l'échec.

Exemples

Dans chacun des exemples, bien noter que nous première tentative de l'opération d'e/S et ensuite consommer le résultat si elle est valide. A noter de plus, que nous avons toujours devez utiliser le résultat de l'opération d'e/S, si le résultat prend des formes différentes 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é lues.

  • C stdio, scanf:

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

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

  • C++, iostreams formaté extraction:

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

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

  • C++, iostreams getline:

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

    Le résultat, nous devons utiliser est de nouveau en std::cin, tout comme avant.

  • Posix, write(2) pour le rinçage d'un tampon:

    char const * p = buf;
    ssize_t n = bufsize;
    for (ssize_t k = bufsize; k = write(fd, p, n); 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.

Vous pouvez remarquer que nous avons très rarement épeler le mot "expressions du FOLKLORE". Nous avons l'habitude de détecter la condition de l'erreur d'une autre façon qui est plus immédiatement intéressant pour nous (par exemple, l'échec à effectuer plus d'I/O comme nous l'avions souhaité). Dans chaque exemple, il y a certaines API fonctionnalité qui pourrait nous dire explicitement que les expressions du FOLKLORE de l'état a été rencontrée, mais ce est en fait pas très utile morceau de l'information. Il est beaucoup plus d'un détail que nous avons souvent des soins. Ce qui importe est de savoir si les I/O a réussi, plus-de sorte que la façon dont il a échoué.

  • Un dernier exemple qui fait des requêtes de l'EOF état: Supposons que vous avez une chaîne et que vous souhaitez tester qu'il représente un nombre entier dans son intégralité, sans aucun supplément de bits à la fin, sauf les espaces. À l'aide de 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, le flux de l'objet lui-même, pour vérifier que la mise en forme d'extraction à l' value réussi. Mais alors, après avoir également consommer de l'espace, nous effectuons un autre I/O/ opération, iss.get(), et s'attendre à l'échec puisque les expressions du FOLKLORE, ce qui est le cas si l'ensemble de la chaîne de caractères a déjà été consommée par la mise en forme de l'extraction.

    Dans la bibliothèque C standard, vous pouvez obtenir quelque chose de similaire avec l' 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 faux, car il des tests pour quelque chose qui n'est pas pertinent et ne parvient pas à tester pour quelque chose que vous devez savoir. Le résultat est que vous avez tort, l'exécution de code qui suppose que l'accès aux données qui a été lu correctement, alors qu'en fait, ce n'est jamais arrivé.

282voto

William Pursell Points 56211

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. Si il y a une erreur de lecture, la boucle 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 sera toujours l'impression de plus grand que le nombre de caractères dans le flux d'entrée (en supposant qu'aucun des 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 que les données ont été lues, elle retourne false. La boucle est entré, fgetc() est appelé (et les retours EOF), et le compteur est incrémenté. Ensuite, feof() est appelée et renvoie vrai, entraînant la boucle pour l'abandonner.

Ce qui se passe dans tous ces cas. feof() ne retourne pas vrai jusqu'à ce que , après une lecture sur le flux des rencontres de la fin du fichier. Le but de l' feof() n'est PAS le vérifier si la prochaine lecture sera d'atteindre la fin du fichier. Le but de l' feof() est la distinction entre une erreur de lecture et d'avoir atteint la fin du fichier. Si fread() retourne 0, vous devez utiliser feof/ferror d'en décider. De même si fgetc retours EOF. feof() n'est utile qu' après fread a repris de zéro ou fgetc a retourné EOF. Avant que cela arrive, feof() retourne toujours 0.

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

Pire encore, prenons le cas d'une erreur de lecture se produit. Dans ce cas, fgetc() retours EOF, feof() retourne false, et la boucle se termine jamais. Dans tous les cas où l' while(!feof(p)) est utilisé, il doit y avoir au moins une case à l'intérieur de la boucle pour ferror(), ou à tout le moins, la condition du while devrait être remplacé par while(!feof(p) && !ferror(p)) ou il y a une possibilité très réelle d'une boucle infinie, probablement crachant toutes sortes de déchets comme des données non valides sont en cours de traitement.

Donc, en résumé, bien que je ne peut pas affirmer avec certitude qu'il n'y est jamais une situation dans laquelle il peut être sémantiquement correct d'écrire "while(!feof(f))" (bien qu'il doit être une autre vérification à l'intérieur de la boucle, avec une pause pour éviter une boucle infinie sur une erreur de lecture), c'est le cas, elle est presque certainement toujours tort. Et même si une affaire n'est jamais né, où il serait correct, il est donc idiomatique de mal qu'il ne serait pas la bonne façon d'écrire le code. Quelqu'un de voir que le code devrait immédiatement hésitez pas et de dire, "que c'est un bug". Et éventuellement claque de l'auteur (à moins que l'auteur est à votre patron auquel cas, la discrétion est conseillé.)

78voto

Erik Points 38942

Non, ce n’est pas toujours mauvais. Si votre condition de boucle est « alors que nous n’avons pas tenté de lire au-delà de fin de fichier », puis vous utilisez . Ce n’est toutefois pas une condition de boucle commune - habituellement, vous voulez tester pour autre chose (comme « peux j’ai lire la suite »).n’est pas mal, c’est juste utilisé mal.

68voto

Chrono Kitsune Points 2980

Pour ceux qui recherchent une solution rapide, ce post contient quelques-uns des moyens sûrs pour lire un fichier en ISO C, bien qu'ils ne sont évidemment pas les seules façons. while (!feof(fp)) n'est pas toujours mauvais, mais quand vous apprenez à travailler avec des fichiers de plus en plus fortement, il y a des choses subtiles, vous devez être conscient de. Bien sûr, vous devriez toujours vérifier vos valeurs de retour des fonctions qui les fournissent. ;-)

Exemple 1: à l'aide d' fgetc correctement

/*
 * ch MUST be an int. Otherwise you can't distinguish between
 * (unsigned char)255 and EOF, and this won't work.
 */
int ch;

while ((ch = fgetc(fp)) != EOF) {
    // do something with ch
}
if (ferror(fp)) {
    // handle the error, usually exit or return
} else {
    // continue execution
}

fgetc retours EOF lorsqu'une erreur se produit ou lorsque la fin du fichier est atteinte. Vous faites une distinction entre les deux boucle cas de résiliation à l'aide de l'une des ferror ou feof après la boucle.

Exemple 2: à l'aide d' fgets correctement

while (fgets(buffer, buffer_size, fp)) {
    // do something with buffer
}
if (ferror(fp)) {
    // handle the error, usually exit or return
} else {
    // continue execution
}

fgets retours NULL sur l'erreur, et le contenu de la mémoire tampon sont imprévisibles. Dans le cas de la fin du fichier est atteinte si les caractères ont été lus, il retourne buffer. Si aucun des caractères ont été lus, il retourne NULL. Encore une fois, utilisez ferror ou feof de distinguer entre les deux cas, pour qui fgets retours NULL.

Exemple 3: utilisation d' fread correctement

size_t nret;
size_t nmemb = 256;

while (nmemb == (nret = fread(buffer, sizeof *buffer, nmemb, fp))) {
    // do something with buffer
}

if (nret) {
    // do the same thing with buffer that was in the loop
}

if (ferror(fp)) {
    // handle the error, probably exit/return
} else {
    // continue execution
}

Celui-ci est un peu différente en raison de la fread travaux. La fonction lit les données binaires à partir d'un fichier et le stocke dans une mémoire tampon, la lecture, nmemb des éléments de la taille spécifiée (sizeof *buffer dans l'exemple) ou autant qu'il le peut.

fread retourne le nombre d'éléments de la taille spécifiée correctement lues. Si elle est inférieure à nmemb, une erreur s'est produite ou la fin du fichier a été atteinte. Si la fin du fichier a été atteinte ou une erreur s'est produite, il peut y avoir de lecture de données, signification, nret est supérieur à 0. En conséquence, ce qui a été fait avec les données lues à l'intérieur de la boucle doit être fait une fois de plus pour terminer le traitement du fichier, autant que possible, si vous le souhaitez (si vous choisissez de traiter les données à lire lorsqu'une erreur se produit est votre option.)

Après cela, l'habituel ferror/feof vérification arrive.

Exemple 4: utilisation de l' fscanf correctement

int nfields = 3;
size_t lineno = 0;

// Read a CSV file containing a list of 3-D vectors as floats.
while (!feof(fp)) {
    ++lineno;
    while (nfields == (nret = fscanf(fp, "%f,%f,%f", &vec[0], &vec[1], &vec[2]))) {
        // do something with the vector read
        ++lineno;
    }

    if (ferror(fp)) {
        // handle the error, usually exit/return
    } else if (nret != EOF) {
        fprintf(stderr, "warning: ignoring malformed line %zu\n", lineno);
        fscanf(fp, "%*[^\n]");
    }
}
// continue execution

fscanfs'comportement est sans doute un mélange de tant de fgetc et fread. Il retourne EOF si une erreur ou à la fin du fichier est détecté, et pas d'entrée conversions ont été appariés encore. Sinon, elle renvoie le nombre de attribué éléments d'entrée. Comme fread, ce sont peut-être moins que prévu, ce qui indique incorrectement formatée de données ou à une erreur ou à la fin du fichier.

Si la valeur n'a pas de signal une erreur, et la valeur retournée par fscanf n'est pas EOF, ce qui signifie que ni ferror ni feof doit retourner une valeur autre que 0, il y a une mauvaise mise en forme des données (il n'aurait pas sorti de la boucle interne contraire.) Si elle n'est pas vraie, à la fin du fichier a été détecté, et que l'extérieur de la condition de boucle est déclenchée, en sortant de la boucle, ce qui signale la fin du fichier a été atteinte.

Notez que j'ai utilisé un scanset dans le dernier bit d'ignorer une ligne non valide entièrement, qui ne fonctionne qu'avec un conforme C99 bibliothèque. On pourrait faire la même chose avec une boucle à l'aide de fgetc ou avec une boucle à l'aide de fgets si vous n'avez pas une bibliothèque C disponible (n'oubliez pas de vérifier à la fin du fichier!)

Contrairement aux autres exemples, j'ai appliqué un petit cas concret de l' fscanf exemple parce formaté d'entrée des demandes. Si elle n'est pas concrète, comment pouvez-vous savoir à certains qu'il fonctionne?

La fermeture de notes

Il est mon espoir que cette réponse aide à quelqu'un parce que c'était la seule réponse que j'ai vu qui explique comment le faire avec au moins un exemple de code plutôt que les paragraphes qui ne sont pas nécessairement expliquer tous les "pièges". L'idée était de fournir un guidage de modèle pour illustrer comment lire les données à partir d'un fichier de façon appropriée et en toute sécurité, même si évidemment il n'est pas nécessairement valable pour tous les cas d'utilisation.

while (!feof(fp)) n'est pas toujours mauvais, et avec la façon dont les choses fonctionnent avec fscanf en particulier, le code de l'Exemple 4 effectivement bénéficié de son utilisation en raison de l'ignorance des erreurs a été désiré, mais j'espère qu'aucun de ces vecteurs qui ont été ignorés étaient terriblement critique, tels que les points de un cube pour un 3-D de la carte... Ce qui serait un cube avec seulement 4 points par exemple? ;-)

Évidemment, cette ignorance n'est pas toujours souhaitée, en particulier lors de la lecture du texte ou des données binaires à partir, par exemple, un fichier de configuration, qui est la principale raison les trois premiers exemples ne permettent pas de tenir compte des données malformées; j'aurais utilisé la même technique que dans l'Exemple 2 comme je l'ai fait dans l'Exemple 4 à ignorer un mal formé ligne dans un fichier INI de Windows, mais je ne peux pas le faire avec le mal imbriquées les éléments XML par exemple. Bien sûr il y a des exceptions, mais c'est une question de détail de l'implémentation et de la désirée et les fonctionnalités disponibles.

Espérons que cela aide quelqu'un! :-)

47voto

AProgrammer Points 31212

feof() indique si l'on a essayé de lire au-delà de la fin de fichier. Cela signifie qu'il est peu prédictif de l'effet: si c'est vrai, vous êtes sûr que la prochaine opération d'entrée sera un échec (vous n'êtes pas sûr de la précédente a échoué d'ailleurs), mais si c'est faux, vous n'êtes pas sûr de la prochaine opération d'entrée sera de réussir. De plus, les opérations d'entrée peut échouer pour des raisons autres que la fin du fichier (une erreur de format pour formaté entrée, un pur IO échec-échec de disque, délai d'attente réseau -- pour la saisie de tous les types), de sorte que même si vous pourriez être un facteur prédictif sur la fin du fichier (et quiconque a essayé de mettre en œuvre Ada, qui est un facteur prédictif, vous dira qu'il peut complexe si vous avez besoin de sauter des espaces, et qu'il a des effets indésirables sur les dispositifs interactifs, parfois en forçant l'entrée de la ligne suivante avant de commencer la manipulation de la précédente), vous devez être en mesure de gérer un échec.

Donc la bonne idiome C est de faire une boucle avec le IO opération de succès en tant que 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) */
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X