Scanf démythifiée

Présentation des erreurs courantes induites par l'utilisation de scanf et des moyens de s'en prévenir.

Article lu   fois.

L'auteur

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

Avant-Propos

Avant toute chose, l'utilisation de la fonction scanf suppose une bonne connaissance de la mécanique des pointeurs, sur lesquels elle repose. Je ne reviendrai pas sur cette mécanique, de nombreux cours (notamment sur l'Internet) le faisant de manière approfondie.

Ces prérequis constituent un des points pour lesquels la fonction scanf ne doit pas être présentée trop tôt aux débutants, qui maîtrisent mal les pointeurs, sources de "segmentation fault" bien connus des développeurs C... ;-)

Introduction

L'article suivant a été écrit dans le but de démythifier la fonction scanf (et, à travers elle, les fonctions *scanf).
Cette fonction, simple à appréhender dans ses services rendus, est en effet une des premières fonctions de saisie présentée aux débutants.
Malheureusement, si son concept est trivial, le comportement sous-jacent l'est beaucoup moins, provoquant nombre d'erreurs dans les programmes de ces mêmes débutants.
D'autre part, l'usage sécurisé de cette fonction entrainant une surcharge non négligeable du code, les personnes expérimentées lui préfèrent le plus souvent fgets() et les fonctions de conversions adéquates, et finissent par négliger, voire oublier l'existence des fonctions *scanf() .

Description de la fonction scanf

Etudions les paramètres utilisés par scanf lors de son appel :

 
Sélectionnez

#include <stdio.h>
      
int scanf ( const char *format, ...);
      

Cette fonction de la librairie standard prend en paramètre une chaîne de caractères décrivant le format du flux entrant "attendu", ainsi qu'une liste (vide ou non), de pointeurs valides (c'est à dire pointant sur une zone mémoire allouée, et de taille à contenir le résultat de la conversion).

Note :
le 'f' de scanf indique que l'entrée analysée est supposée formatée, c'est à dire générée par des fonctions comme *printf (ou par d'autres moyens, mais avec un comportement similaire).
C'est de ce 'f' que viennent tous les problèmes que l'on puisse rencontrer avec cette fonction : en effet, on oublie trop souvent, lorsqu'on l'utilise, qu'elle est éprouvée par un utilisateur lambda, et que rien ne l'empêche de faire des saisies "non formatées", provoquant l'échec de la fonction scanf. Nous reviendrons plus tard sur ce point.

La chaîne "format" peut contenir, des caractères alphanumériques propres à l'utilisation du développeur (comme "path_file", "device", etc...) : pratique quand on cherche à scanner un fichier de configuration, avec des entrées bien précises ( avec fscanf ). Elle peut de même contenir (c'est le plus intéressant !) des spécificateurs de format permettant de prélever sur l'entrée standard, et selon un format défini à l'avance, des éléments susceptibles d'influer sur l'exécution du programme.

Exemple :
Utilisation de fscanf (qui a exactement le même comportement que la fonction scanf) pour l'analyse d'un fichier de configuration. On voit que l'on peut mélanger des caractères "banals" et des spécificateurs (pour par exemple récupérer le numéro d'un device) :

 
Sélectionnez

int i;
fscanf(config_file, "device_number = %d", &i);
      



Ces spécificateurs sont précédés du signe '%'. Ils sont par ailleurs constitués de plusieurs drapeaux (certains optionnels), affinant les spécifications sur l'analyse du flux entrant.

Note :
Si le développeur souhaite intégrer dans sa chaîne le signe '%' sans pour autant indiquer une conversion, il suffit de le doubler pour qu'il perde sa signification.
Ainsi, dans la chaîne "hello%%world", le second '%' ne sera pas interprété comme annonçant le début d'une conversion, puisqu'il est inhibé par le premier '%'.

La liste suivante des drapeaux constituant les spécificateurs de format n'a pas pour but d'être exhaustive, mais d'en présenter les principaux, avec leur définition et leur utilisation. Pour plus d'informations, le meilleur document (plus accessible que la norme ISO/IEC 9899:1990):


man scanf


Un spécificateur se compose d'entêtes :

% Le signe '%', introduisant une conversion
* Le signe '*', précisant que si la conversion est réussie, l'objet de cette conversion sera ignoré : il est inutile d'associer un pointeur au résultat de cette conversion.
De manière optionnelle, on peut aussi préciser (entre le signe '%' et la conversion) un champ indiquant la longueur de la saisie attendue. C'est un entier décimal. Sa valeur par défaut est "l'infini", c'est a dire que scanf cherchera a scanner le plus possible d'éléments remplissant la condition voulue.


Ainsi qu'une combinaison des drapeaux suivants :

d entier décimal signé. Le pointeur associé doit être un pointeur sur "int"
i entier signé. Le pointeur associé doit être un pointeur sur "int".
L'entier est lu en base 16 s'il commence par '0x' ou '0X' ; s'il commence par un '0', il est lu en base 8 ; dans tous les autres cas, il est lu en base 10
o entier octal. Le pointeur associé doit être un pointeur sur "unsigned int"
u entier décimal, à ceci près que le pointeur associé doit être un pointeur sur un "unsigned int"
x,X entier hexadécimal signé. Pointeur sur "unsigned int" associé


Note :
Pour les conversions "dioux", si l'on précède les formateurs d'un 'l', alors le pointeur associé pointe sur un "long int" ; un 'h' indique un pointeur sur "short int" ; un "hh" sur "char" ; et enfin, "ll" un pointeur sur "long long int".

e,f,g Ces formateurs permettent de récupérer des nombres flottants. Leur format est le même que celui utilisé (et validé) par la fonction strtod, dont je me permet de citer le manuel quant à la représentation attendue (Attention, la source étant le manuel Unix, il pourrait y avoir certains détails d'implantation propre à ce système. Il s'agit là d'une indication, pour plus de détails, je vous invite à consulter la norme ISO/IEC 9899:1990 (C90) ) :

La (portion initiale de la) chaîne attendue est de la forme suivante :
Des espaces éventuels en tête, ou des caractères interprétés comme tels par isspace(3)
Un éventuel signe plus (``+'') ou moins (``-'')
Un nombre décimal ou hexadécimal, ou un infini, ou encore un non-nombre NAN (not-a-number).
Un nombre décimal consiste en une séquence non-vide de chiffres décimaux, pouvant contenir un point décimal (dépendant de la localisa- tion, mais habituellement ``.''), suivi éventuellement d'un exposant décimal. Ce dernier est constitué d'un ``E'' ou ``e'' suivi éventuellement du signe plus ou du signe moins, puis une séquence non- vide de de chiffres décimaux, indiquant la multiplication par une puis- sance de 10.
Un nombre hexadécimal commence ``0x'' ou ``0X'', suivi d'une séquence non-vide de chiffres hexadécimaux, pouvant contenir un point décimal suivi éventuellement d'un exposant binaire. Ce dernier est constitué d'un ``P'' ou ``p'' suivi éventuellement du signe plus ou du signe moins, puis une séquence non-vide de de chiffres décimaux, indiquant la multiplication par une puissance de 2. Il ne peut y avoir plus d'un point ou d'un exposant binaire.
Un infini est soit ``INF'', soit ``INFINITY'', sans tenir compte des différences entre majuscules et minuscules.
Un non-nombre est ``NAN'' (sans différences majuscules/minuscules) suivi éventuellement par `(', une séquence de caractères, et `)'. La chaîne de caractères indique le type de non-nombre de manière dépendant de l'implémentation.


A noter que le pointeur associé doit être un pointeur sur "float" (sauf si on positionne le drapeau 'l' (indiquant un double), ou 'L' (indiquant un 'long double')
s séquence de caractères différents d'un caractère d'espacement.
Le pointeur associé doit être un pointeur sur char.
L'espace alloué à la réception de la chaîne saisie doit être assez grand pour contenir à la fois la séquence et le caractère de terminaison de chaîne ('\0', qui est ajouté automatiquement)
La séquence s'arrête lorsque scanf rencontre un caractère d'espacement, ou bien lorsque la longueur précisée (champ de taille vu plus haut) a été atteinte
Note :
Un caractère est considéré comme d'espacement s'il fait partie de l'ensemble suivant :
' ' (espace)
'\t' (tabulation)
'\n' (fin de ligne)
....
c séquence de caractères. Par défaut, 1 seul caractère, à moins qu'un champ taille n'ait été précisé. Le pointeur associé est un pointeur sur char.
Dans le cas d'une séquence de plusieurs caractères, l'espace alloué doit être suffisant pour contenir tous les caractères.
Contrairement au formateur 's', aucun caractère de terminaison de chaîne n'est ajouté
[ permet d'introduire des expressions rationnelles.
Nous les verrons plus en détail par la suite. Le pointeur associe est un pointeur sur "char".
L'espace alloué doit être suffisant pour contenir de surcroît le caractère de terminaison de chaîne (ajouté automatiquement).
Est associé au caractère ']' fermant le set d'expression rationnelle


Note :
Le caractère 'virgule' utilisé par les formateurs de flottants est défini suivant les locales du système.
Ainsi, cela peut être ',' ou '.' ou autre... Attention à la portabilité !

Problèmes à l'utilisation, et solutions apportées

Comportement de la fonction scanf (particulièrement en cas d'erreur)

La fonction applique le filtre décrivant le format (la chaîne de caractère), sur le flux entrant.
Pour chaque conversion réussie, scanf place l'objet converti dans l'espace pointé par le pointeur associé. Aucune vérification n'est faite quant à la validité de ces pointeurs, de même que, dans le cas de saisie de chaîne de caractères (ou de séquence de caractères), aucun contrôle sur le rapport entre les quantités "espace alloué" / "taille de la saisie" : si le programmeur ne fait pas ces contrôles lui-même, les risques de buffer overflow sont élevés.

C'est là, je crois, le point le plus important à comprendre dans le fonctionnement de la fonction scanf. Si au cours de l'analyse de la chaîne de format une des conversions échoue, alors la fonction scanf s'arrête brutalement : les conversions suivantes ne sont pas effectuées !! On peut donc avoir une partie des résultats attendus, mais pas tous !

Il est donc impératif de verifier la valeur retournée par scanf. Cette valeur n'est pas innocente, elle indique précisément le nombre d'affectations correctes. Il va sans dire que continuer l'exécution d'un programme en ayant une partie des variables non initialisées (celles non "remplies" par scanf suite a un échec de conversion) provoque un "undefined behaviour" (comportement indéfini) qui file de l'urticaire à n'importe quel développeur sérieux...

Comportement vis à vis des espaces

C'est là aussi une des subtilités du fonctionnement de scanf qu'il faut connaître, surtout si l'on souhaite en faire une utilisation poussée (i.e. sécurisée).

Tout d'abord, le cas particulier dont je vais vous parler concerne uniquement le cas du formateur '%c' (ainsi que celui des expressions rationnelles, mais le fonctionnement par rapport à ce cas particulier est le même...)

Par défaut, scanf supprime les espaces précédant ce qui peut convenir au formateur. Mais dans le cas du '%c', elle ne le fait pas, ce qui fait que les espaces peuvent "compter réellement" lorsque l'on a précisé une taille de saisie par exemple...
Le concept est peut être un peu nébuleux, mais en pratique, c'est très simple... ;-)

Imaginons que je veuille récupérer 10 caractères sur l'entrée standard.

Je déclare un tableau de taille suffisante (i.e. permettant de mettre un terminateur de chaîne "manuellement" : on a vu que dans le cas du '%c', un caractère '\0' final n'était pas ajouté par scanf, au contraire du '%s')

 
Sélectionnez

char tab[11] = {0}; /* en l'initialisant à zero, je met le
				'\0' final directement...*/
      

Je récupère les 10 caractères naïvement... Et j'affiche le résultat.

 
Sélectionnez

scanf("%10c", tab); /* recuperation */
printf("lu : %s", tab);
      

Et je fais la saisie suivante (entre guillemets qui ne font, eux, pas partie de la saisie)

 
Sélectionnez

      "azertyuiop" puis <Enter>
      

On obtient :

 
Sélectionnez

      "lu : azertyuiop"
      


Jusque là, tout va bien...
Mais imaginons un utilisateur farceur...

 
Sélectionnez

      "     azertyuiop"  puis Enter
      

Il y a cinq espaces avant la saisie qui nous intéresse..
Et on obtient :

 
Sélectionnez

      "lu : '     azert'"
      

Les blancs n'ont pas été enlevés. Et c'est gênant.

Remarque :
On voit bien, une fois de plus, que les formateurs ne sont pas à employer à la légère : leur utilisation génère des résultats complètement différents.
On ne peut donc intervertir '%c' et '%s' sans qu'il y ait de conséquences.



Rappel :

%s [*] s'arrête au premier caractère "blanc" (sauf si l'on a précisé une longueur de champ, et que celle-ci est atteinte avant un blanc.)
[*] supprime les espaces lors de l'analyse
[*] ajoute un terminateur de chaîne (caractère '\0') de façon automatique.
%c [*] lit par défaut un seul caractère
[*] ne supprime pas les espaces "non utiles" en tête de saisie: ils comptent alors contre le champ taille que l'on a pu préciser.
[*] n'ajoute pas de caractère final : il faut le faire manuellement si l'on souhaite travailler sur une chaîne valide.


Malgré cela, que faire pour passer outre ce comportement qui peut fausser les exécutions des programmes. Il "suffit" de faire précéder le formateur '%10c' d'un espace, pour dire explicitement à scanf de réduire les espaces en tête.
On doit donc mettre ' %10c'.
Vérifions :

 
Sélectionnez

scanf(" %10c", tab);
      

Saisie :

 
Sélectionnez

      '     azertyuiop'  (cinq espaces précédant la saisie éffective)
      

Donne :

 
Sélectionnez

      'lu : azertyuiop'
      

Les espaces ont biens été supprimés.

Note :
On voit souvent les débutants utiliser à tort et à travers les espaces dans les chaînes de formatage. On voit bien à travers cet exemple que leur présence n'est pas innocente, et peut influer sur le comportement de la fonction.
Vigilance donc dans l'utilisation abusive d'espaces, que ce soit dans la chaîne de formatage, ou lors de la prise en compte des saisies utilisateur !!

Utilisation courante de scanf

Saisie sécurisée d'une chaîne de caractère

Tout d'abord, il va sans dire que le paragraphe précédent n'a pas été écrit inutilement, et il faut bien avoir compris les subtilités d'espaces avec scanf. Tout peut être fait selon nos désirs, mais encore faut-il savoir clairement ce que l'on veut. Je considère donc tout ce qui a été dit précédemment comme acquis.

Importance du contrôle de la taille mémoire

En effet, le principal défaut d'une fonction comme scanf est de ranger directement en mémoire ce qui est prélevé sur le flux entrant là où le programmeur l'a décidé.
Mais cela pose problème en cas de saisie trop longue : c'est le fameux buffer overflow, qui peut être exploité à des fins nuisibles, que ce soit dirigé contre votre programme, ou pire, contre votre système (shellcodes, etc...)

Il devient donc impératif de contrôler la taille de cette saisie, de façon à éviter ces comportements litigieux.

Si vous avez été attentifs, vous avez sûrement noté quel outil va nous permettre d'assurer ces contrôles. Il s'agit bien sûr du flag "taille" que l'on peut glisser entre le signe '%' et le formateur indiquant la conversion.

Ainsi, lorsque vous allouez de la place en mémoire pour recevoir la chaîne de caractères (que ce soit statiquement ou dynamiquement), il vous faut conserver cette taille afin de la réutiliser dans ce champ taille, et ainsi sécuriser la saisie.

On peut procéder de plusieurs façons pour positionner la taille dans la chaîne de formatage.
Si vous avez déclaré un tableau de caractère de manière statique :

 
Sélectionnez

char tab[80];
	  

La meilleure façon de faire dans ce cas là est de définir, via une macro, la longueur du tableau.
Ainsi, cela devient :

 
Sélectionnez

#define TAILLE 80
<...>
char tab[TAILLE+1] = {0}; /* +1 pour le caractere terminal */ 
	  

Puis, on utilise une astuce du préprocesseur (il suffit de lire la doc de CPP pour la connaître ;-)

 
Sélectionnez

#define xstr(s) str(s)
#define str(s) #s
	  

Ces macros nous permettent de transformer en chaîne de caractère des macros en les expansant d'abord.
On peut donc l'utiliser ainsi :

 
Sélectionnez

scanf("%"xstr(TAILLE)"s", tab);
	  

Si on passe uniquement le préprocesseur sur ce morceau de code, on observe que cpp a transormé cette notation en :

 
Sélectionnez

scanf("%""80""s", tab);
          

Ce qui équivaut à :

 
Sélectionnez

scanf("%80s", tab);
          

On a donc fait une saisie de chaîne de caractère déjouant le piège du buffer overflow. La même astuce peut être utilisée pour le formateur '%c', ainsi que pour tout autre nécessitant un contrôle.

Cette façon de faire fonctionne bien en cas de définition statique de la taille de l'espace nécessaire au stockage de la chaîne de caractere. De façon dynamique, on peut aussi le faire, mais la mise en oeuvre est plus lourde.

Imaginons que la taille soit définie en cours d'exécution par le résultat d'une opération quelconque.

On dispose de la taille :

 
Sélectionnez

size_t taille;
<...>
taille = 80;
	  

On alloue la place nécessaire pour la chaîne (j'utilise calloc de préférence à malloc, car calloc initialise la zone mémoire réservée à zero, ce qui permet de positionner directement le caractère terminal de chaîne) :

 
Sélectionnez

char *tab = calloc(taille, sizeof *tab);
/* ne pas oublier de tester le retour de la fonction calloc... */  
	  

On construit maintenant dynamiquement la chaîne de formatage :

 
Sélectionnez

int t_format = 81;
char *format = calloc(t_format, sizeof *format);
/* tester le retour de calloc... */

snprintf(format, t_format, "%%%ds", taille);
	  

On a donc la chaîne format qui ressemble à "%80s". (Les deux premiers '%' servent a imprimer le caractere '%' dans la chaîne ; puis vient '%d' permettant d'imprimer la taille dans la chaîne ; puis enfin 's' sera imprimé, indiquant le format qui sera utilisé par scanf...)

Remarque :
snprintf n'est pas ANSI, mais est conforme à la norme C99. Il est préférable de l'utiliser car elle permet de contrôler la taille de ce qui sera imprimé, de manière à éviter un débordement de buffer... Attention tout de même, elle n'imprime que taille-1 caractères, de manière à mettre automatiquement le '\0' final.

Récuperation d'une ligne entière

On peut réaliser le pendant de la fonction fgets avec scanf, mais cela nécessite l'utilisation des expressions rationnelles.

Je vais brièvement vous introduire celles-ci, de manière à ce que vous en compreniez les tenants et les aboutissants nécessaires à la compréhension de certaines astuces...

Tout d'abord, les expressions rationnelles (notées ER dans la suite de ce document), sont exprimées entre les caractères '[' et ']' .
Entre ces deux bornes, on peut définir un ensemble non vide de caractères quelconques, qui serviront de "filtre", appliqué sur le flux entrant, de manière à faire une sélection plus "fine" qu'avec les formateurs standards.

Rappel :
Pour le formateur '[' (i.e. les ER), l'espace alloué pour recevoir la chaîne doit etre suffisamment grand pour contenir la chaîne ET un caractere terminal '\0' (ajouté automatiquement par scanf).
Par ailleurs, les espaces précédant la saisie "effective" ne sont pas supprimés par défaut, revoir le paragraphe concernant ce comportement (avec '%c') pour plus de détails.

Ainsi, on a vu que %s prenait les caractères différents d'un caractere d'espacement. On pourrait avoir envie d'affiner, et ne valider que les séquences contenant uniquement des lettres.
On aurait donc :

 
Sélectionnez

char tab[81] = {0};
scanf(" %80[abcdefghijklmnopqrstuvwxyz]", tab);
	  

Si vous avez bien tout suivi jusqu'à maintenant, vous aurez remarqué le formatage que j'ai utilisé :

un espace avant le '\%' pour supprimer les espaces précédant la saisie.
un champ taille de 80 caractères, de manière à effectuer un contrôle sur la taille de la saisie en cours.
entre crochets, les lettres représentant les critères de validation d'une saisie : ainsi, toute saisie contenant autre chose que ce qui est présent dans l'ensemble de caractère sera considérée comme fausse, et scanf terminera en erreur.


On remarque que l'ensemble précédent (les lettres) est particulièrement fastidieux à taper. Il existe la possibilité d'utiliser le caractère '-' (tiret) pour les ensembles "courants". On peut de plus préciser plusieurs de ces ensembles entre les crochets de l'ER :

 
Sélectionnez

scanf(" %80[a-zA-Z]", tab);
	  

L'ensemble décrit ci-dessus concerne les lettres de l'alphabet, qu'elles soient en majuscule ou en minuscule. Ainsi, une saisie comme suit sera valide :

 
Sélectionnez

	  'HellOWorLd'
	  

Toute saisie contenant d'autres caractères sera rejetée.

Maintenant, venons-en à la recuperation d'une ligne complète. (bha oui, c'était le but du chapitre ;-)

Une ligne est définie par une suite de caractères et terminée par un retour chariot '\n'.
Si on veut récupérer une ligne entière, il suffit donc de prendre tous les caractères différents de '\n'.
On ne va pas entrer tous les caractères imaginables satisfaisant cette condition. Il existe un autre caractere spécial, le '^' (circonflexe) permettant de dire : toute saisie ne contenant pas les caractères suivants est considérée comme valide.

Etudions le code suivant :

 
Sélectionnez

char tab[81] = {0};
char c;

if (scanf("%80[^\n]", tab) == 1) {
	/* saisie reussie */
	if ( (c = getchar()) != '\n') {
		/* saisie trop longue */

	}

}
else{
	/* echec de la saisie */
	
}
	  

On remarque :

un champ taille (80)
un ensemble précisant : tout sauf '\n'
le test du retour de scanf :

[*] s'il vaut 1, alors la saisie a réussie : cela veut dire que tab a été correctement rempli. Par contre, on ne sait pas s'il reste des caractères disponibles, à cause du champ taille. En effet, comme on a précisé celle-ci, il se peut que la fonction scanf se soit arrêtée alors que le flux entrant contient encore des caractere différents de '\n'.

On fait un 'getchar' pour récupérer le prochain caractère (il y en a forcement au moins un si scanf a réussi)
-- s'il est égal à '\n', alors c'était le dernier du flux.
-- s'il en est différent, c'est qu'il reste des caractères en trop. On peut choisir de vider le flux de manière standard, en utilisant getchar, ou bien faire un autre traitement, tout dépend de ce que l'on souhaite faire.


[*] s'il est différent de 1, c'est que la saisie a échoué. Dans l'exemple pris plus haut, ce cas peut arriver uniquement en cas de saisie d'une ligne vide (ne contenant qu'un '\n').
Il faut donc vider le flux entrant. En effet, la fonction scanf s'est arrêtée avant le '\n' : ce caractère reste donc présent sur le flux ! (un simple getchar fait l'affaire :-)


Rappel :
La manière standard de vider le flux entrant est la suivante :

 
Sélectionnez

int c;
while ( ((c = getchar()) != '\n') && c != EOF)
{
}; 
	  

Utilisation de scanf dans une boucle

On l'a vu dans le paragraphe précédent, l'échec (ou non !) de scanf lors d'une saisie est susceptible de laisser des caractères sur le flux entrant. Ce comportement est particulièrement gênant en cas d'utilisation de scanf à l'intérieur d'un boucle.

Il existe une expression rationnelle que l'on peut rajouter à la fin de celle(s) qui nous intéresse(nt). On va utiliser le spécificateur '*' pour indiquer que l'on ignore le résultat.

Ainsi, vider le flux reviendra à dire : "je valide tout caractère différent de '\n' (avec '^'), et je me fiche du résultat (avec '*')". Cela donne :

 
Sélectionnez

scanf("%*[^\n]");
	  

On ne précise pas de pointeur, puisque l'on "jette" le résultat. Par contre, il reste encore et toujours le même caractere dans le flux entrant : ce satané '\n'. On lui règle son compte avec un 'getchar' bien senti.
Vider le flux entrant avec scanf revient au final à :

 
Sélectionnez

scanf("%*[^\n]");
getchar();
	  

Lors de l'utilisation dans une boucle, si, par exemple, on souhaite que l'utilisateur entre un nombre, et que l'on veuille le lui redemander tant que ce n'est pas le cas, le code serait le suivant :

 
Sélectionnez

#include <stdio.h>

int main(void){

  int nombre = 0;
  int ok = 0;
  int retour;

  printf("saisissez un nombre :\n");
  while (!ok){

    retour = scanf("%d%*[^\n]", &nombre);
    printf("retour : %d\n", retour);
    if ( !retour ){
      /* erreur de saisie, on vide le flux */
      int c;
      while ( ((c = getchar()) != '\n') && c != EOF);

      printf("on vous a demande de saisir un nombre\n");
      printf("veuillez recommencer :\n");
    }
    else {
      /* reussite de la saisie */
      getchar(); /* on enleve le '\n' restant */

      printf("saisie acceptee\n");
      ok = 1;  /* sort de la boucle */
    }

  }

  return 0;
}
	  

En fait, si la saisie échoue (valeur de retour de scanf égale à zero dans le cas présent), il faut vider explicitement stdin (avec getchar par exemple, mais on pourrait utiliser scanf comme vu plus haut ;-).
Par contre, en cas de reussite, l'ER suivante : '%*[^\n]' permet de supprimer tout caractere restant. C'est le cas si, par exemple, on a une saisie du genre: '1234azertyuiop poiuytreza' Le nombre 1234 sera récupéré, et le reste du flux purgé.

Par contre, en cas de reussite, il ne faut pas oublier d'enlever le '\n' restant...

Vérification du retour de scanf, et outils de contrôle

Scanf retourne le nombre d'éléments assignés correctement. Ainsi, un retour de valeur zero signifie qu'aucun élément n'a été assigné ; c'est le cas lorsque l'on entre une lettre alors que le formateur est '%d'.

Il faut noter le cas particulier que constitue l'utilisation de l'étoile '*', signifiant que l'on ignore le résultat. En effet, même si la conversion a réussie, comme rien n'a été affecté, elle ne sera pas comptabilisée.

Dans le cas suivant, si tout se passe bien, la valeur de retour de scanf sera 1 (et non 2) :

 
Sélectionnez

int a;
scanf("%d%*[^\n]", &a);
	

Imaginons maintenant que nous voulions récupérer certains éléments d'une saisie, et en passer d'autres.

Exemple :
" 123azerty456uiop789 "
Et on ne veut récupérer que les nombres aux extrémités, tout en vérifiant que la ligne suit le pattern suivant :

 
Sélectionnez

[nombre]~[lettres]~[nombre]~[lettres]~[nombre]
	  

On peut imaginer de faire comme suit :

 
Sélectionnez

int a, b;
scanf("%d%*[a-z]%*[0-9]%*[a-z]%d", &a, &b);
	  
  • On récupère le premier entier
  • On vérifie sans assignement qu'il existe bien une suite de lettres (minuscules)
  • On vérifie sans assignement qu'il existe bien une suite de chiffres
  • On vérifie sans assignement qu'il existe bien une suite de lettres (minuscules)
  • On récupère le second entier

On a vu que si scanf réussi, on aura comme valeur de retour 2 (puisqu'on assigne deux éléments).
Mais en cas d'échec, la valeur ne pourra être que 0 (rien d'assigné) ou 1 (premier entier).

Si l'on veut faire une gestion fine des erreurs, voire reprendre sur ces mêmes erreurs, on ne dispose pas assez d'informations pour savoir ou exactement scanf a echouée :
Est-ce à cause

  • Des symboles ou des majuscules dans la première séquence de lettres ?
  • pas de seconde séquence de chiffre ?
  • Des symboles ou des majuscules dans la seconde séquence de lettres ?

Nous allons donc aborder le dernier formateur utile de scanf : le formateur '%n'.
Ce formateur nous permet de connaître le nombre de caractères lus par scanf au moment de l'assignement. Le pointeur associé est un pointeur sur int.

Note :
Le nombre de caractères lus est différent du nombre d'assignations réalisées.

Reprenons donc l'exemple ci-avant. Si l'on veut connaître où peut avoir echoué scanf, il suffit de déclarer quelques variables supplémentaires.
Ainsi :

 
Sélectionnez

int a = 0;
int  b = 0;
int seq_1_lettres = 0;
int seq_chiffre = 0;

scanf("%d%*[a-z]%n%*[0-9]%n%*[a-z]%d", &a, &seq_1_lettres, &seq_chiffre, &b);
	  

A propos du formateur %n. La norme précise que la valeur assignée n'est pas comptabilisée dans le retour de scanf : c'est comme si l'on avait précisé le drapeau '*'. Cependant, il est à noter que certaines implantations ne respectent pas ce principe, et il est donc fortement conseillé de ne plus tenir pour absolument exact le retour de scanf lorsque l'on utilise ce formateur.

On peut donc tester le retour de scanf de la façon suivante :

 
Sélectionnez

if(!b) {
  /* b n'a pas ete assigne, on en deduit un echec de scanf */
  if(!a){
    /* echec complet de scanf */
          ...
  } else {
    /* au moins a est correct */
    if (!seq_1_lettres){
      /* c'est la sequence de lettre qui n'est pas passee */
         ....
    } else if (!seq_chiffre) {
      /* la sequence de chiffre a plante... */
      ....
    }
  }

} else {
  /* b a ete correctement rempli, c'est donc que scanf s'est correctement deroule jusqu'au bout*/
    ....
}

.....
	  

Conclusion

Pour conclure, si l'on souhaite utiliser la fonction scanf, il est important de connaître plusieurs choses.
Tout d'abord, il faut bien comprendre les mécanismes que mettent en jeu les formateurs "simples" de la fonction( d, i, o, u, x, e, f, g).
Autre chose, il faut toujours avoir à l'esprit les subtilités inhérentes à la présence des espaces dans cette même chaîne de formatage...
Par ailleurs, si l'on veux faire de la saisie correcte et sécurisée, il est très important de contrôler la taille de la saisie utilisateur via les champs associés, ce afin d'éviter tout débordement de buffer.
De même, il faut se rappeler que certains formateurs (s, et '[' ) ajoutent automatiquement un '\0' à la saisie utilisateur, ce qu'il faut prendre en compte lorsqu'on alloue l'espace de stockage.
Enfin, ne pas oublier que lors de l'utilisation répétée de fonctions de saisie (que ce soit de manière séquentielle ou via une boucle), il est important de contrôler le retour de ces fonctions, de manière à savoir s'il faut vider le buffer d'entrée (pour éviter tout comportement indéfini).

Une fois ces points bien compris, vous êtes prêts ! :-)

Remerciements

Je tiens à remercier tout particulièrement Emmanuel Delahaye et Pedro204 pour leurs relectures attentives, ainsi qu'Anomaly et toute l'équipe de Developpez.com pour m'avoir permis de publier ce tutoriel. :-)

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2005 Xavier Renault. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.