Scanf démythifiéeDate de publication : 30/11/2005 , Date de mise à jour : 30/11/2005
Par
Xavier Renault (aka Pouic) Présentation des erreurs courantes induites par l'utilisation de scanf et des moyens de s'en prévenir. Avant-Propos Introduction Description de la fonction scanf Problèmes à l'utilisation, et solutions apportées Comportement de la fonction scanf (particulièrement en cas d'erreur) Comportement vis à vis des espaces Utilisation courante de scanf Saisie sécurisée d'une chaîne de caractère Importance du contrôle de la taille mémoire Récuperation d'une ligne entière Utilisation de scanf dans une boucle Vérification du retour de scanf, et outils de contrôle Conclusion Remerciements 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 :
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) :
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 :
Ainsi qu'une combinaison des drapeaux suivants :
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".
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éesComportement 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.
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')
Je récupère les 10 caractères naïvement... Et j'affiche le résultat.
Et je fais la saisie suivante (entre guillemets qui ne
font, eux, pas partie de la saisie)
On obtient :
Jusque là, tout va bien... Mais imaginons un utilisateur farceur...
Il y a cinq espaces avant la saisie qui nous intéresse..
Et on obtient :
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 :
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 :
Saisie :
Donne :
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 scanfSaisie 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 :
La meilleure façon de faire dans ce cas là est de
définir, via une macro, la longueur du tableau.
Ainsi, cela devient :
Puis, on utilise une astuce du préprocesseur (il
suffit de lire la doc de CPP pour la connaître ;-)
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 :
Si on passe uniquement le préprocesseur sur ce
morceau de code, on observe que cpp a transormé cette
notation en :
Ce qui équivaut à :
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 :
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) :
On construit maintenant dynamiquement la chaîne de
formatage :
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 :
Si vous avez bien tout suivi jusqu'à maintenant,
vous aurez remarqué le formatage que j'ai utilisé :
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 :
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
:
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 :
On remarque :
Rappel : La manière standard de vider le flux entrant est la suivante :
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 :
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 à :
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 :
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) :
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 :
On peut imaginer de faire comme suit :
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
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 :
On peut donc tester le retour de scanf de la façon
suivante :
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. :-)
|
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. Cette page est déposée.
Copyright © 2000-2012 - www.developpez.com