Il existe plusieurs fonctions standards en C qui permettent de récupérer une chaîne de texte. Hormis la fonction
scanf (trop compliquée pour être étudiée ici), il existe :
- gets : une fonction qui lit toute une chaîne de caractères, mais très dangereuse car elle ne permet pas de contrôler les buffer overflow !
- fgets : l'équivalent de gets mais en version sécurisée, permettant de contrôler le nombre de caractères écrits en mémoire.
Vous l'aurez compris : bien que ce soit une fonction standard du C,
gets est très dangereuse. Tous les programmes qui l'utilisent sont susceptibles d'être victimes de buffer overflow.
Nous allons donc voir comment fonctionne
fgets et comment on peut l'utiliser en pratique dans nos programmes en remplacement de
scanf.
La fonction fgets
Le prototype de la fonction
fgets, situé dans
stdio.h, est le suivant :
Code : C | char *fgets( char *str, int num, FILE *stream );
|
Il est important de bien comprendre ce prototype. 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 tableau str envoyé en premier paramètre.
Notez que si vous avez alloué un tableau de 10 char, 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 pointeur stdin, 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'utiliser fgets pour lire des fichiers, comme on a pu le voir dans le chapitre sur les fichiers.
La fonction
fgets retourne le même pointeur que
str si la fonction s'est déroulée sans erreur, ou
NULL s'il y a eu une erreur. Il suffit donc de tester si la fonction a renvoyé
NULL pour savoir s'il y a eu une erreur.
Testons !
Code : C 1
2
3
4
5
6
7
8
9
10
11
12
13 | #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;
}
|
Code : Console | Quel est votre nom ? Mateo
Ah ! Vous vous appelez donc Mateo
! |
Ça fonctionne très bien, à un détail près : quand vous pressez « Entrée »,
fgets conserve le
\n correspondant à l'appui sur la touche « Entrée ». Cela se voit dans la console car il y a un saut à la ligne après « Mateo » dans mon exemple.
On ne peut rien faire pour empêcher
fgets d'écrire le caractère
\n, la fonction est faite comme ça. En revanche, rien ne nous interdit de créer notre propre fonction de saisie qui va appeler
fgets et supprimer automatiquement à chaque fois les
\n !
Créer sa propre fonction de saisie utilisant fgets
Il n'est pas très difficile de créer sa propre petite fonction de saisie qui va faire quelques corrections à chaque fois pour nous.
Nous appellerons cette fonction
lire. Elle renverra 1 si tout s'est bien passé, 0 s'il y a eu une erreur.
Éliminer le saut de ligne \n
La fonction
lire va appeler
fgets et, si tout s'est bien passé, elle va rechercher le caractère
\n à l'aide de la fonction
strchr que vous devriez déjà connaître. 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 :
Code : C 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 | #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
}
}
|
Vous noterez que je me permets d'appeler la fonction
fgets directement dans un
if. Ça m'évite d'avoir à récupérer la valeur de
fgets dans un pointeur juste pour tester si celui-ci est
NULL ou non.
À partir du premier
if, je sais si
fgets s'est bien déroulée ou s'il y a eu un problème (l'utilisateur a rentré plus de caractères qu'il n'était autorisé).
Si tout s'est bien passé, je peux alors partir à la recherche du
\n avec
strchr et remplacer cet
\n par un
\0 (fig. suivante).
Ce schéma montre que la chaîne écrite par
fgets était «
Mateo\n\0 ». Nous avons remplacé le
\n par un
\0, ce qui a donné au final : «
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à.
Le résultat ? Eh bien ça marche.
Code : C | 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;
}
|
Code : Console | Quel est votre nom ? Mateo
Ah ! Vous vous appelez donc Mateo ! |
Vider 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 !
Code : Console | Quel est votre nom ? Jean Edouard Albert 1er
Ah ! Vous vous appelez donc Jean Edou ! |
La fonction
fgets étant sécurisée, elle s'est arrêtée de lire au bout du 9
e caractère car nous avions alloué un tableau de 10
char (il ne faut pas oublier le caractère de fin de chaîne
\0 qui occupe la 10
e position).
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. Le buffer est une sorte de zone mémoire qui reçoit directement l'entrée clavier et qui sert d'intermédiaire entre le clavier et votre tableau de stockage. En C, on dispose d'un pointeur vers le buffer, c'est ce fameux
stdin dont je vous parlais un peu plus tôt.
Je crois qu'un petit schéma ne sera pas de refus pour mettre les idées au clair (fig. suivante).
Lorsque l'utilisateur tape du texte au clavier, le système d'exploitation (Windows, par exemple) copie directement le texte tapé dans le buffer
stdin. Ce buffer 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 (parce que vous avez alloué un tableau de 10
char seulement), 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 :
Code : C | 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;
}
|
Code : Console | Quel est votre nom ? Jean Edouard Albert 1er
Ah ! Vous vous appelez donc Jean Edou ! |
La fonction
fgets n'a pu copier que les 9 premiers caractères comme prévu. Le problème, c'est que les autres se trouvent toujours dans le buffer (fig. suivante) !
Cela signifie que si vous faites un autre
fgets ensuite, celui-ci va aller récupérer ce qui était resté en mémoire dans le buffer !
Testons ce code :
Code : C 1
2
3
4
5
6
7
8
9
10
11
12 | 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, vous allez voir qu'on ne vous laisse pas taper deux fois votre nom : en effet, 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 !
Code : Console | Quel est votre nom ? Jean Edouard Albert 1er
Ah ! Vous vous appelez donc Jean Edou !
Ah ! Vous vous appelez donc ard Alber ! |
Si l'utilisateur tape trop de caractères, la fonction
fgets nous protège contre le débordement de mémoire, mais il reste toujours des traces du texte en trop dans le buffer. Il faut vider le buffer.
On va donc améliorer notre petite fonction
lire et appeler — si besoin est — une sous-fonction
viderBuffer pour faire en sorte que le buffer soit vidé si on a rentré trop de caractères :
Code : C 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32 | 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.
La fonction
viderBuffer est courte mais dense. Elle lit dans le buffer caractère par caractère grâce à
getchar. Cette fonction renvoie un
int (et non un
char, allez savoir pourquoi, peu importe).
On se contente de récupérer ce
int dans la variable temporaire
c. On boucle tant qu'on n'a pas récupéré le caractère
\n ou le symbole
EOF (fin de fichier), qui signifient tous deux « vous êtes arrivé à la fin du buffer ». On s'arrête donc de boucler dès que l'on tombe sur l'un de ces deux caractères.
C'est un peu compliqué au premier abord et assez technique, mais ça fait son travail. N'hésitez pas à relire ces explications plusieurs fois si nécessaire pour comprendre comment ça fonctionne.