scanf en C : pourquoi fgets saute une ligne

Ton fgets semble ignoré après scanf ? Suis le retour à la ligne dans stdin, corrige la lecture et valide un entier avec fgets et strtol.

Par
Publié le

Tu saisis un âge, tu appuies sur Entrée, puis ton programme affiche un nom vide sans attendre la suite. Le fgets placé après scanf semble avoir été ignoré. Il a pourtant lu quelque chose.

En bref

Après scanf("%d", &age), le retour à la ligne qui suit le nombre reste dans stdin. Le fgets suivant le lit et renvoie la chaîne "\n", sans attendre le nom. Pour corriger, consomme le reste de la ligne après scanf, ou lis chaque réponse avec fgets et convertis ensuite le nombre avec strtol.

Pourquoi fgets lit-il un nom vide après scanf ?

Les deux fonctions partagent le même flux d'entrée, mais elles ne s'arrêtent pas au même endroit. Voici le programme minimal qui rend le problème visible :

mixed.c
#include <stdio.h>#include <string.h>
int main(void){    int age;    char name[64];
    if (scanf("%d", &age) != 1)        return 1;    if (fgets(name, sizeof name, stdin) == NULL)        return 1;    name[strcspn(name, "\n")] = '\0';    printf("age=%d, nom=[%s]\n", age, name);    return 0;}
Terminal
$ gcc -std=c17 -Wall -Wextra -Werror mixed.c -o mixed$ printf '21\nAda\n' | ./mixedage=21, nom=[]

La commande printf envoie deux lignes au programme, exactement les octets 21\nAda\n. Ici, le printf du shell fournit l'entrée ; celui du programme C affiche le résultat.

scanf lit les chiffres 2 et 1, puis s'arrête devant le premier caractère qui ne fait plus partie de l'entier. Le \n attend donc encore dans stdin. fgets le récupère et s'arrête aussitôt. Ensuite, strcspn repère ce \n et le programme le remplace par \0 : le nom affiché entre crochets est vide. « Ada » n'a pas été lu.

  1. 21\nAda\nLes deux lignes en entrée
  2. \nAda\nscanf a consommé 21
  3. Ada\nfgets a consommé le premier retour à la ligne
Deux lectures dans le même flux

Le test scanf(...) != 1 reste nécessaire. Si tu saisis abc, aucune conversion n'a lieu : utiliser age sans vérifier le résultat reviendrait ici à lire une variable non initialisée.

Comment corriger un programme qui utilise déjà scanf ?

Après une conversion réussie, lis jusqu'au prochain \n ou jusqu'à EOF, puis seulement appelle fgets. Cette solution convient si ton format impose un âge sur une ligne et un nom sur la suivante.

discard.c
#include <stdio.h>#include <string.h>
int main(void){    int age;    char name[64];
    if (scanf("%d", &age) != 1)        return 1;    int c;    while ((c = getchar()) != '\n' && c != EOF)        ;    if (c == EOF)        return 1;    if (fgets(name, sizeof name, stdin) == NULL)        return 1;    name[strcspn(name, "\n")] = '\0';    printf("age=%d, nom=[%s]\n", age, name);    return 0;}
Terminal
$ gcc -std=c17 -Wall -Wextra -Werror discard.c -o discard$ printf '21   \nAda Lovelace\n' | ./discardage=21, nom=[Ada Lovelace]

Pourquoi une boucle plutôt qu'un seul getchar() ? Parce que l'utilisateur peut laisser des espaces après le nombre. Un seul appel enlèverait le premier espace de 21 \n et laisserait le reste à fgets.

c est un int, pour pouvoir représenter les valeurs des octets lus et la valeur spéciale EOF. Si tu oublies le test de fin d'entrée, une boucle qui attend \n peut tourner sans fin sur un fichier terminé.

Cette correction jette volontairement tout ce qui suit l'entier sur sa ligne. Avec 21abc, elle conserve donc l'âge 21 et abandonne abc. Si tu veux refuser cette saisie, il faut valider la ligne entière. Autre limite : %d suppose que le nombre tient dans un int ; un dépassement à la conversion a un comportement indéfini. Pour une entrée non contrôlée, passe à la version fgets et strtol ci-dessous.

Comment lire chaque réponse avec fgets et vérifier l'entier ?

Lis d'abord une ligne complète, puis donne sa chaîne à strtol(3). Tu peux alors refuser abc, un nombre trop grand ou 21abc avant de passer à la saisie du nom.

Le programme suivant accepte un âge compris entre 0 et INT_MAX. Il autorise les blancs autour du nombre. Pour garder un format d'entrée explicite, chaque ligne doit se terminer par \n et contenir au plus 62 octets avant ce retour à la ligne ; une ligne plus longue ou incomplète arrête le programme.

read.c
#include <ctype.h>#include <errno.h>#include <limits.h>#include <stdio.h>#include <stdlib.h>#include <string.h>
static int read_line(char line[64]){    if (fgets(line, 64, stdin) == NULL)        return 0;    char *newline = strchr(line, '\n');    if (newline == NULL)        return 0;    *newline = '\0';    return 1;}
int main(void){    char line[64];    char name[64];    char *end;
    if (!read_line(line)) {        puts("Ligne age invalide");        return 1;    }    errno = 0;    long age = strtol(line, &end, 10);    if (end == line || errno == ERANGE || age < 0 || age > INT_MAX) {        puts("Age invalide");        return 1;    }    while (isspace((unsigned char)*end))        end++;    if (*end != '\0') {        puts("Age invalide");        return 1;    }    if (!read_line(name)) {        puts("Ligne nom invalide");        return 1;    }    printf("age=%ld, nom=[%s]\n", age, name);    return 0;}
Terminal
$ gcc -std=c17 -Wall -Wextra -Werror read.c -o read$ printf '21\nAda Lovelace\n' | ./readage=21, nom=[Ada Lovelace]$ printf '21abc\nAda\n' | ./readAge invalide$ printf 'abc\nAda\n' | ./readAge invalide$ printf '999999999999999999999999\nAda\n' | ./readAge invalide$ printf '21\nAda' | ./readLigne nom invalide

Les échecs couvrent des cas différents. end == line signifie qu'aucun chiffre n'a été converti. errno == ERANGE détecte un résultat qui ne tient pas dans un long ; la comparaison avec INT_MAX applique ensuite la limite choisie pour l'âge. Enfin, *end permet de refuser les caractères qui restent après les blancs.

Le cast (unsigned char) protège l'appel à isspace si char est signé et que la chaîne contient un octet supérieur à 127. Il ne faut pas passer une valeur négative autre que EOF à cette fonction.

Le helper read_line donne la même règle aux deux lectures. Si fgets ne trouve pas de \n dans le tableau, le programme s'arrête au lieu de prendre un fragment de ligne pour le nom suivant. Une application qui redemande la saisie doit aussi consommer le reste de la ligne refusée avant de recommencer. Ici, quitter suffit.

Le nom peut contenir des espaces, comme « Ada Lovelace ». Une chaîne vide reste acceptée : refuser un nom vide serait une règle supplémentaire à ajouter selon ton application. Ces exemples attendent du texte, sans octet nul dans l'entrée.

C

atoi

Écris gm_atoi, ta conversion d'une chaîne en entier. Observe où ta lecture s'arrête : pour une saisie utilisateur, ce point d'arrêt est précisément l'information que strtol te fournit et qu'atoi ne te donne pas.

Ouvrir l'exercice

Pourquoi ne pas ajouter un espace ou un retour à la ligne dans scanf ?

Un blanc dans le format de scanf consomme une suite de blancs jusqu'au prochain caractère non blanc ou jusqu'à la fin de l'entrée. scanf("%d\n", &age) peut donc attendre une nouvelle saisie après Entrée, au lieu de consommer exactement un retour à la ligne.

scanf(" %c", &letter) répond à un autre besoin : l'espace avant %c saute les blancs qui précèdent le caractère. C'est adapté à un choix de menu quand les blancs ne sont pas des réponses valides. Cela ne répare pas la lecture d'un nom complet par fgets.

Pour choisir sans tâtonner, décide d'abord si ton entrée est une suite de valeurs séparées par des blancs ou une suite de lignes. Dans notre exemple, une ligne représente une réponse. Employer fgets pour les deux réponses rend cette séparation visible dans le code.

Questions fréquentes

fgets renvoie-t-il NULL quand il lit seulement Entrée ?

Non. S'il a lu le retour à la ligne, il renvoie ton tableau, qui contient "\n". NULL signale une erreur de lecture ou une fin d'entrée avant tout caractère. Le programme retire lui-même le \n ensuite.

Est-ce que scanf("%s", name) permet de lire un nom complet ?

%s s'arrête au premier blanc : il ne garde que Ada dans Ada Lovelace. Avec un tableau de 64 caractères, %63s limite la lecture, mais les espaces du nom restent un problème. Garde fgets pour cette saisie.

Pourquoi deux scanf avec %d semblent-ils fonctionner ?

%d saute les blancs avant le prochain nombre. Le deuxième appel peut donc passer le retour à la ligne laissé par le premier. fgets, lui, conserve ce retour à la ligne et s'arrête dessus.

Faut-il utiliser atoi après fgets ?

Pour une saisie à contrôler, préfère strtol. atoi ne fournit pas de pointeur de fin ni de mécanisme fiable pour signaler un dépassement. La fiche atoi détaille ces limites.

Sources

À toi d'écrire le code.