while (!feof) en C : pourquoi la dernière ligne se répète

Comprends pourquoi while (!feof) répète la dernière ligne en C. Suis fgets, corrige la boucle et teste le fichier vide ou sans retour à la ligne final.

Par
Publié le

Ton programme lit un fichier, puis affiche sa dernière ligne deux fois. La boucle semble pourtant vérifier la fin avec while (!feof(stdin)). Le problème vient de ce que cette vérification raconte : une lecture passée, pas la prochaine.

En bref

En C, feof teste l'indicateur de fin du flux ; il ne prédit pas si la prochaine lecture réussira. Après une ligne terminée par un retour à la ligne, cet indicateur peut encore être nul, même si aucun octet ne reste. Utilise fgets(3) dans la condition de boucle pour traiter seulement les lectures réussies, puis vérifie ferror(stream) à la sortie. Tester seulement feof après la lecture peut aussi supprimer une dernière ligne sans retour à la ligne final.

Pourquoi la dernière ligne s'affiche-t-elle deux fois ?

La dernière tentative échoue, mais ton code réutilise le contenu laissé dans le tableau. Voici une version instrumentée : read vaut 1 quand fgets(3) renvoie le tableau, 0 quand il renvoie NULL. Les deux autres nombres montrent les indicateurs du flux, ramenés à 0 ou 1 avec !!.

bad.cÀ éviter
#include <stdio.h>#include <string.h>
int main(void){    char line[8] = "";
    while (!feof(stdin)) {        char *result = fgets(line, sizeof line, stdin);        if (ferror(stdin))            return 1;        line[strcspn(line, "\n")] = '\0';        printf("read=%d eof=%d error=%d buffer=[%s]\n",               result != NULL, !!feof(stdin), !!ferror(stdin), line);    }    return 0;}
Terminal
$ gcc -std=c17 -Wall -Wextra -Werror bad.c -o bad$ printf 'A\nB\n' | ./badread=1 eof=0 error=0 buffer=[A]read=1 eof=0 error=0 buffer=[B]read=0 eof=1 error=0 buffer=[B]

Le printf(3) du shell fournit deux lignes sur l'entrée standard. Pour lire un fichier ouvert avec fopen, le raisonnement serait le même en remplaçant stdin par ce flux, après avoir vérifié l'ouverture.

La deuxième lecture s'arrête sur le \n de B. La troisième essaie de lire encore, rencontre la fin sans obtenir de caractère et renvoie NULL. Dans ce cas précis, fgets(3) laisse le tableau inchangé : B est affiché une deuxième fois, sans avoir été relu.

strcspn retire le retour à la ligne du tableau uniquement pour rendre l'affichage lisible. Ce retrait ne modifie pas le flux. Le test d'erreur empêche ici une boucle infinie sur une erreur de lecture, mais ne corrige pas la mauvaise utilisation du résultat.

Que se passe-t-il lors des trois lectures ?

Avec l'entrée A, retour à la ligne, B, retour à la ligne, le troisième appel ne produit aucune nouvelle donnée. Les instantanés sont pris après chaque appel à fgets(3), avant le retrait du retour à la ligne.

A puis B : lire les données, puis constater la fin

Étape 1 sur 3

Lecture de A

read
1
feof
0
ferror
0
  1. AA + \nlu
  2. BB + \nà lire
  3. résultattableaulecture réussie

fgets a obtenu A et son retour à la ligne. Le flux contient encore B et son retour à la ligne.

Lire toutes les étapes
  1. 1. Lecture de A

    read
    1
    feof
    0
    ferror
    0
    1. AA + \nlu
    2. BB + \nà lire
    3. résultattableaulecture réussie

    fgets a obtenu A et son retour à la ligne. Le flux contient encore B et son retour à la ligne.

  2. 2. Lecture de B

    read
    1
    feof
    0
    ferror
    0
    1. AA + \nlu
    2. BB + \nlu
    3. résultattableaulecture réussie

    fgets s'arrête sur le retour à la ligne de B. Aucun octet ne reste, mais feof vaut encore 0.

  3. 3. Tentative suivante

    read
    0
    feof
    1
    ferror
    0
    1. AA + \nlu
    2. BB + \nlu
    3. résultatNULLne pas traiter

    fgets rencontre la fin avant tout caractère et renvoie NULL. Réutiliser le tableau afficherait encore B.

La fin n'est pas une case supplémentaire du fichier contenant un caractère EOF. C'est un état que la bibliothèque constate pendant une opération de lecture.

Pourquoi tester feof après fgets peut-il perdre une ligne ?

Parce qu'un appel peut obtenir des caractères, puis rencontrer la fin avant un \n. Il renvoie alors le tableau contenant ces caractères, tout en positionnant l'indicateur de fin.

Terminal
$ printf 'A\nB' | ./badread=1 eof=0 error=0 buffer=[A]read=1 eof=1 error=0 buffer=[B]

La dernière lecture a réussi : read=1. Pourtant, eof=1. Un test qui quitterait dès que feof(stdin) devient vrai, avant de traiter B, jetterait cette donnée valide. Ici, la boucle fautive ne répète pas B : le comportement change avec le retour à la ligne final. C'est précisément pourquoi un seul fichier d'essai peut cacher le défaut.

Quelle boucle faut-il écrire avec fgets ?

Mets le résultat de fgets(3) dans la condition, puis traite le tableau dans le corps. Après NULL, examine l'indicateur d'erreur pour distinguer une fin ordinaire d'une lecture interrompue.

good.cCorrect
#include <stdio.h>#include <string.h>
int main(void){    char line[8];    size_t count = 0;
    while (fgets(line, sizeof line, stdin) != NULL) {        line[strcspn(line, "\n")] = '\0';        printf("chunk %zu: [%s]\n", ++count, line);    }    printf("chunks=%zu eof=%d error=%d\n",           count, !!feof(stdin), !!ferror(stdin));    return ferror(stdin) ? 1 : 0;}
Terminal
$ gcc -std=c17 -Wall -Wextra -Werror good.c -o good$ printf 'A\nB\n' | ./goodchunk 1: [A]chunk 2: [B]chunks=2 eof=1 error=0$ printf 'A\nB' | ./goodchunk 1: [A]chunk 2: [B]chunks=2 eof=1 error=0$ ./good < /dev/nullchunks=0 eof=1 error=0

Les deux fichiers donnent deux fragments traités. Le fichier vide n'en donne aucun. Aucun contenu non initialisé n'est utilisé, puisque le corps n'est jamais exécuté après une lecture échouée.

Le nom chunk est volontaire : avec huit cases, fgets(3) peut stocker au plus sept caractères puis le zéro terminal. Une longue ligne arrive en plusieurs fragments. Il faut les assembler si ton application attend une ligne complète.

Terminal
$ printf 'abcdefghij\n' | ./goodchunk 1: [abcdefg]chunk 2: [hij]chunks=2 eof=1 error=0

Ce programme de démonstration attend du texte sans octet nul interne. Pour traiter un fichier binaire, choisis une interface qui fournit aussi le nombre d'octets lus. La fiche fgets détaille son contrat de chaîne ; le problème du fgets après scanf concerne un retour à la ligne encore présent dans le flux, une autre cause d'affichage surprenant.

Comment vérifier qu'une erreur ne passe pas pour une fin normale ?

Vérifie ferror après la boucle et renvoie un échec s'il est positionné. Sous Linux avec glibc, rediriger un répertoire vers notre programme provoque une erreur de lecture :

Terminal
$ ./good < .; echo "status=$?"chunks=0 eof=0 error=1status=1

Cette commande est un test Linux, pas une méthode portable pour fabriquer une erreur sur tous les systèmes. Elle montre pourquoi attendre uniquement feof est insuffisant : une lecture peut échouer alors que cet indicateur reste à zéro.

Dans une application, ajoute un message d'erreur adapté à l'opération. Ne continue pas à traiter le tableau après l'échec ; son contenu n'est pas garanti en cas d'erreur de lecture.

C

mini_scanf

64-bit

Dans mini_scanf, implémente les conversions %s, %d et %c. Il faut distinguer les conversions réussies de la fin de l'entrée : EOF est réservé à une fin d'entrée avant toute conversion possible. C'est un exercice avancé sur les valeurs de retour, pas une demande de recopier la boucle fgets.

Ouvrir l'exercice

Questions fréquentes

feof renvoie-t-il EOF ?

Non. Il renvoie zéro si l'indicateur de fin n'est pas positionné, une valeur non nulle sinon. EOF est une constante entière employée par d'autres fonctions, comme fgetc. fgets renvoie un pointeur ou NULL.

Faut-il remettre le tableau à zéro à chaque tour ?

Cela ne répare pas la condition. Tu traiterais encore une tentative qui n'a fourni aucune donnée. Vérifie le retour de fgets avant d'utiliser le tableau.

Comment adapter la règle à fscanf ?

Teste le nombre de conversions attendues, par exemple 1 pour une seule conversion. Un retour nul peut signaler un échec de correspondance sans fin de fichier. Contrôle aussi les valeurs et leurs limites ; pour un entier saisi en texte, la conversion avec strtol permet une validation explicite.

clearerr permet-il de recommencer ?

Il efface les indicateurs, mais ne répare ni la source ni la logique de lecture. L'utiliser dans cette boucle sans résoudre la cause peut simplement faire recommencer les mêmes échecs.

Sources

À toi d'écrire le code.