La saisie de texte est un des aspects les plus délicats du langage C. Vous connaissez la fonction scanf
, que vous avez vue au début du cours. Vous vous dites : quoi de plus simple et de plus naturel ? Eh bien figurez-vous que non, en fait, c'est tout sauf simple.
Ceux qui vont utiliser votre programme sont des humains. Tout humain qui se respecte fait des erreurs et peut avoir des comportements inattendus. Si vous lui demandez : « Quel âge avez-vous ? », qu'est-ce qui vous garantit qu'il ne va pas vous répondre « Je m'appelle François je vais bien merci » ?
Le but de ce chapitre est de vous faire découvrir les problèmes que l'on peut rencontrer en utilisant la fonction scanf
, et de vous montrer une alternative plus sûre avec la fonction fgets
.
Comprenez les limites de la fonction scanf
scanf()
est une fonction à double tranchant :
elle est facile à utiliser quand on débute (c'est pour ça que je vous l'ai présentée) ;
mais son fonctionnement est complexe, elle peut même être dangereuse dans certains cas.
Je vais vous montrer ses limites par deux exemples concrets.
Exemple 1 : une chaîne de caractères qui contient des espaces
Supposons qu'on demande une chaîne de caractères à l'utilisateur, mais qu'il insère un espace :
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
char nom[20] = {0};
printf("Quel est votre nom ? ");
scanf("%s", nom);
printf("Ah ! Vous vous appelez donc %s !\n\n", nom);
return 0;
}
Quel est votre nom ? Jean Dupont Ah ! Vous vous appelez donc Jean !
Pourquoi le Dupont a disparu ?
La fonction scanf
s'arrête si elle tombe sur un espace, une tabulation ou une entrée.
Vous ne pouvez donc pas récupérer la chaîne si celle-ci comporte un espace.
Exemple 2 : une chaîne de caractères trop longue
Il y a un autre problème, beaucoup plus grave : celui du dépassement de mémoire.
Dans le code que nous venons de voir, il y a la ligne suivante :
char nom[5] = {0};
J'ai alloué 5 cases pour mon tableau de char
appelé nom
, il y a donc la place d'écrire 4 caractères (le dernier étant toujours réservé au caractère de fin de chaîne\0
) :
Que se passe-t-il si on écrit plus de caractères qu'il n'y a d'espace prévu pour les stocker ?
Quel est votre nom ? Patrice Ah ! Vous vous appelez donc Patrice !
On avait alloué 5 cases pour stocker le nom, mais en fait il en fallait 8. La fonction scanf
a donc continué à écrire à la suite en mémoire comme si de rien n'était ! Elle a écrit dans des zones mémoire qui n'étaient pas prévues pour cela.
Les caractères en trop ont écrasé d'autres informations en mémoire :
Pourquoi c'est dangereux ?
Dans ce chapitre, nous allons voir comment sécuriser la saisie de nos données, en empêchant l'utilisateur de faire déborder et de provoquer un "buffer overflow".
Récupérez une chaîne de caractères
Plusieurs fonctions standard en C permettent de récupérer une chaîne de texte :
gets
lit toute une chaîne de caractères mais elle ne contrôle pas les "buffer overflow" !fgets
contrôle le nombre de caractères écrits en mémoire.
Nous allons donc voir comment utiliser fgets
en remplacement de scanf
!
Comprenez le principe de la fonction fgets
Le prototype de la fonction fgets
, situé dans stdio.h
, est le suivant :
char *fgets( char *str, int num, FILE *stream );
Les paramètres sont les suivants :
str
: un pointeur vers un tableau alloué en mémoire où la fonction va pouvoir écrire le texte entré par l'utilisateur.num
: la taille du tableaustr
envoyé en premier paramètre. Si vous avez alloué un tableau de 10char
,fgets
lira 9 caractères au maximum (il réserve toujours un caractère d'espace pour pouvoir écrire le \0 de fin de chaîne).
stream
: un pointeur sur le fichier à lire. Dans notre cas, le fichier à lire est l'entrée standard, c'est-à-dire le clavier. Pour demander à lire l'entrée standard, on enverra le pointeurstdin
, qui est automatiquement défini dans les "headers" de la bibliothèque standard du C pour représenter le clavier. Toutefois, il est aussi possible d'utiliserfgets
pour lire des fichiers, comme on a pu le voir dans le chapitre sur les fichiers.
Il suffit donc de tester si la fonction a renvoyé NULL
pour savoir s'il y a eu une erreur :
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
char nom[10];
printf("Quel est votre nom ? ");
fgets(nom, 10, stdin);
printf("Ah ! Vous vous appelez donc %s !\n\n", nom);
return 0;
}
Quel est votre nom ? Mateo Ah ! Vous vous appelez donc Mateo !
Créez votre propre fonction de saisie utilisant fgets
On peut facilement créer notre propre fonction de saisie pour qu'elle fasse quelques corrections pour nous. Nous l'appellerons : lire
. Elle renverra 1 si tout s'est bien passé, 0 s'il y a eu une erreur.
Éliminez le saut de ligne\n
La fonction lire
va appeler fgets
et , si tout s'est bien passé, va chercher le caractère\n
à l'aide de la fonction strchr
que l'on a déjà vue. Si un \n
est trouvé, elle le remplace par un\0
(fin de chaîne) pour éviter de conserver une "Entrée".
Voici le code, commenté pas à pas :
#include <stdio.h>
#include <stdlib.h>
#include <string.h> // Penser à inclure string.h pour strchr()
int lire(char *chaine, int longueur)
{
char *positionEntree = NULL;
// On lit le texte saisi au clavier
if (fgets(chaine, longueur, stdin) != NULL) // Pas d'erreur de saisie ?
{
positionEntree = strchr(chaine, '\n'); // On recherche l'"Entrée"
if (positionEntree != NULL) // Si on a trouvé le retour à la ligne
{
*positionEntree = '\0'; // On remplace ce caractère par \0
}
return 1; // On renvoie 1 si la fonction s'est déroulée sans erreur
}
else
{
return 0; // On renvoie 0 s'il y a eu une erreur
}
}
À partir du premier if
, je sais si fgets
s'est bien déroulée ou s'il y a eu un problème (le premier paramètre de la fonction (pointeur) est égal à (NULL), le second paramètre est inférieur ou égal à zéro, ou les deux).
Si tout s'est bien passé, je peux alors partir à la recherche du \n
avec strchr
et remplacer cet\n
par un \0
:
La chaîne écrite par fgets
était "Mateo\n\0". Nous avons remplacé le \n
par un \0
, ce qui a donné : "Mateo\0\0". Ce n'est pas grave d'avoir deux \0
d'affilée : l'ordinateur s'arrête au premier \0
qu'il rencontre et considère que la chaîne de caractères s'arrête là :
int main(int argc, char *argv[])
{
char nom[10];
printf("Quel est votre nom ? ");
lire(nom, 10);
printf("Ah ! Vous vous appelez donc %s !\n\n", nom);
return 0;
}
Quel est votre nom ? Mateo Ah ! Vous vous appelez donc Mateo !
Videz le buffer
Nous ne sommes pas encore au bout de nos ennuis. Nous n'avons pas étudié ce qui se passait si l'utilisateur tentait de mettre plus de caractères qu'il n'y avait de place !
Quel est votre nom ? Jean Edouard Albert 1er Ah ! Vous vous appelez donc Jean Edou !
Le problème, c'est que le reste de la chaîne qui n'a pas pu être lu, à savoir "ard Albert 1er", n'a pas disparu : Il est toujours dans le "buffer".
Je crois qu'un petit schéma ne sera pas de refus pour mettre les idées au clair :
Lorsque l'utilisateur tape du texte au clavier, le système d'exploitation copie directement le texte tapé dans le "buffer" stdin
. Il est là pour recevoir temporairement l'entrée du clavier.
Le rôle de la fonction fgets
est justement d'extraire du "buffer" les caractères qui s'y trouvent et de les copier dans la zone mémoire que vous lui indiquez (votre tableau chaine
).
Après avoir effectué son travail de copie, fgets
enlève du "buffer" tout ce qu'elle a pu copier.
Si tout s'est bien passéfgets
a donc pu copier tout le buffer dans votre chaîne, et ainsi le buffer se retrouve vide à la fin de l'exécution de la fonction. Mais si l'utilisateur entre beaucoup de caractères, et que la fonction fgets
ne peut copier qu'une partie d'entre eux, seuls les caractères lus seront supprimés du "buffer". Tous ceux qui n'auront pas été lus y resteront !
Testons avec une longue chaîne :
int main(int argc, char *argv[])
{
char nom[10];
printf("Quel est votre nom ? ");
lire(nom, 10);
printf("Ah ! Vous vous appelez donc %s !\n\n", nom);
return 0;
}
Quel est votre nom ? Jean Edouard Albert 1er Ah ! Vous vous appelez donc Jean Edou !
La fonction fgets
a copié les 9 premiers caractères. Les autres sont toujours dans le buffer :
Testons ce code :
int main(int argc, char *argv[])
{
char nom[10];
printf("Quel est votre nom ? ");
lire(nom, 10);
printf("Ah ! Vous vous appelez donc %s !\n\n", nom);
lire(nom, 10);
printf("Ah ! Vous vous appelez donc %s !\n\n", nom);
return 0;
}
Nous appelons deux fois la fonction lire
. Pourtant, on ne vous laisse pas taper deux fois votre nom : la fonction fgets
ne demande pas à l'utilisateur de taper du texte la seconde fois car elle trouve du texte à récupérer dans le "buffer" !
Quel est votre nom ? Jean Edouard Albert 1er Ah ! Vous vous appelez donc Jean Edou ! Ah ! Vous vous appelez donc ard Alber !
On va donc améliorer notre fonctionlire
et appeler une sous-fonction viderBuffer
pour faire en sorte que le "buffer" soit vidé si on a rentré trop de caractères :
void viderBuffer()
{
int c = 0;
while (c != '\n' && c != EOF)
{
c = getchar();
}
}
int lire(char *chaine, int longueur)
{
char *positionEntree = NULL;
if (fgets(chaine, longueur, stdin) != NULL)
{
positionEntree = strchr(chaine, '\n');
if (positionEntree != NULL)
{
*positionEntree = '\0';
}
else
{
viderBuffer();
}
return 1;
}
else
{
viderBuffer();
return 0;
}
}
La fonction lire
appelle viderBuffer
dans deux cas :
Si la chaîne était trop longue (on le sait parce qu'on n'a pas trouvé de caractère
\n
dans la chaîne copiée).S'il y a eu une erreur (peu importe laquelle), il faut vider là aussi le "buffer" par sécurité pour qu'il n'y ait plus rien.
Convertissez la chaîne en nombre
Notre fonction lire
est maintenant efficace mais elle ne sait lire que du texte.
Mais comment fait-on pour récupérer un nombre ?
Avec fgets
, vous ne pouvez récupérer que du texte, mais il existe d'autres fonctions qui permettent de convertir ensuite un texte en nombre.
Convertissez une chaîne en long
avec strtol
Le prototype de la fonction strtol
est un peu particulier :
long strtol( const char *start, char **end, int base );
La fonction lit la chaîne de caractères que vous lui envoyez :
start
.Elle essaie de la convertir en
long
en utilisant labase
indiquée (généralement, on travaille en base 10 car on utilise 10 chiffres différents de 0 à 9, donc vous mettrez 10).Elle retourne le nombre qu'elle a réussi à lire.
Quant au pointeur de pointeur end
, la fonction s'en sert pour renvoyer la position du premier caractère qu'elle a lu et qui n'était pas un nombre. On ne s'en servira pas, on peut donc lui envoyer NULL
pour lui faire comprendre qu'on ne veut rien récupérer.
Voici quelques exemples d'utilisation pour bien comprendre le principe :
long i;
i = strtol( "148", NULL, 10 ); // i = 148
i = strtol( "148.215", NULL, 10 ); // i = 148
i = strtol( " 148.215", NULL, 10 ); // i = 148
i = strtol( " 148+34", NULL, 10 ); // i = 148
i = strtol( " 148 feuilles mortes", NULL, 10 ); // i = 148
i = strtol( " Il y a 148 feuilles mortes", NULL, 10 ); // i = 0 (erreur : la chaîne ne commence pas par un nombre)
Toutes les chaînes qui commencent par un nombre (ou éventuellement des espaces suivis d'un nombre) seront converties en long
jusqu'à la première lettre ou jusqu'au premier caractère invalide :.
, +
, etc.
La dernière chaîne de la liste ne commençant pas par un nombre, elle ne peut pas être convertie.
La fonction strtol
renverra donc 0.
On peut créer une fonction lireLong
qui va appeler notre première fonction lire
(qui lit du texte), et ensuite convertir le texte saisi en nombre :
long lireLong()
{
char nombreTexte[100] = {0}; // 100 cases devraient suffire
if (lire(nombreTexte, 100))
{
// Si lecture du texte ok, convertir le nombre en long et le retourner
return strtol(nombreTexte, NULL, 10);
}
else
{
// Si problème de lecture, renvoyer 0
return 0;
}
}
Vous pouvez tester dans un main
très simple :
int main(int argc, char *argv[])
{
long age = 0;
printf("Quel est votre age ? ");
age = lireLong();
printf("Ah ! Vous avez donc %d ans !\n\n", age);
return 0;
}
Quel est votre age ? 18 Ah ! Vous avez donc 18 ans !
Convertissez une chaîne en double
avec strtod
La fonction strtod
est identique à strtol
, sauf qu'elle essaie de lire un nombre décimal et renvoie un double
:
double strtod( const char *start, char **end );
Le troisième paramètre base
a disparu ici, mais il y a toujours le pointeur de pointeur end
qui ne nous sert à rien.
Essayez d'écrire la fonction lireDouble
(c'est exactement comme lireLong
, à part que cette fois, on appelle strtod
et on retourne undouble
). Vous devriez alors pouvoir faire ceci :
Combien pesez-vous ? 67.4 Ah ! Vous pesez donc 67.400000 kg !
Ensuite, modifiez votre fonction lireDouble
pour qu'elle accepte aussi le symbole virgule comme séparateur décimal :
Remplacez la virgule par un point dans la chaîne de texte lue (grâce à la fonction de recherche
strchr
).Puis envoyez la chaîne modifiée à
strtod
.
En résumé
La fonction
scanf
a des limites. On ne peut pas, par exemple, écrire plusieurs mots à la fois facilement.Un "buffer overflow" survient lorsqu'on dépasse l'espace réservé en mémoire : si l'utilisateur entre 10 caractères alors qu'on n'avait réservé que 5 cases en mémoire.
L'idéal est de faire appel à la fonction
fgets
pour récupérer du texte saisi par l'utilisateur.Il faut en revanche éviter à tout prix d'utiliser la fonction
gets
qui n'offre pas de protection contre le "buffer overflow".Vous pouvez écrire votre propre fonction de saisie du texte qui fait appel à
fgets
comme on l'a fait, afin d'améliorer son fonctionnement.
J’ai l’impression que vous avez terminé la partie 2 de ce cours. Ne changeons pas les bonnes habitudes, il est temps de faire un quiz afin d’évaluer vos nouvelles connaissances !