Récupérer une chaîne de caractères
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 :
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 !
#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 ! |
Ç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 :
#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.
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 ! |
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 !
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 9e 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 10e 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 :
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 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 :
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 !
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 :
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.
Créé avec HelpNDoc Personal Edition: Produire des livres EPub gratuitement