Skip to content

Saisie d’une chaîne de caractères en C

11 décembre 2012

Introduction

Le C est un langage qui a été créé par Dennis Ritchie pour écrire un système d’exploitation. Les premiers programmes en C ont été ceux qui composent le système Unix. Celui-ci est connu pour être très modulaire, constitué de petits programmes qui font une seule tâche précise et communiquent entre eux à travers des fichiers, des tubes, des sockets (non, pas des chaussettes) etc.

Cette introduction peut vous paraître hors-sujet, mais je veux seulement dire par là que la conversation avec l’utilisateur n’est pas ce qui a de plus simple en C.

Problématique

En faisant des petits programmes comme ceux de nos TP, vous avez sûrement remarqué que pour demander à l’utilisateur un texte contenant des mots séparés par des espaces, cela pouvait poser des problèmes.

En effet, quand nous débutons en C, on nous dit souvent que pour lire une chaîne de caractères, on utilise scanf avec le code format %s. C’est bien mais quand on essaie, on se rend compte que ça marche pour un mot, mais quand on en met deux avec une espace entre eux, le deuxième est ignoré par scanf. Pire, si on a un deuxième scanf, le deuxième mot va être récupéré par lui !

Et puis quand on grandit, on nous dit que scanf c’est mal, non seulement pour la raison ci-dessus mais aussi parce que si l’utilisateur donne n’importe quoi, on perd le contrôle de notre programme.

Exemple :

char a[20], b[20] ;
printf("Donnez un mot : ");
scanf("%s",a);
printf("Un autre : ");
scanf("%s",b);
printf("1er : %d lettres, 2e : %d lettres\n",strlen(a), strlen(b)");

Si l’utilisateur tape : bonjour anticonstitutionnellement pour le premier mot, non seulement le programme n’attend pas le deuxième mot après avoir affiché « Un autre », mais, ce qui est encore plus grave, les lettres qui débordent de la chaîne b (qui n’est pas assez grande) vont provoquer un dépassement de tampon.

Si vous avez de la chance, vous avez un compilateur récent (GCC > 4.0) qui peut parfois vous avertir du débordement (mais ça ne marche pas toujours) :

Donnez un mot : bonjour anticonstitutionnellement
Un autre : 1er : 7 lettres, 2e : 25 lettres
*** stack smashing detected ***: ./programme terminated
Aborted (core dumped)

Si vous avec de la malchance, vous pouvez faire planter votre PC…

Cherchons une solution

scanf avec %s

Une manière d’éviter ce genre de désagréments est de préciser la longueur maximale au code format %s. Par exemple, scanf("%20s",... indique à scanf de ne prendre que les 20 premiers caractères de la chaîne introduite. Cela évite les débordements mais ne règle pas le problème des espaces, qui stoppent toujours la lecture de la chaîne.

scanf avec %c

On pourrait aussi utiliser le code format %c. Quand on l’utilise seul, il ne permet de lire qu’un seul caractère, mais on peut lui préciser le nombre de caractères à lire. Par exemple, scanf("%20c",... lit tous les caractères, même les espaces et s’arrête au 20e. Un inconvénient de ce code format est que contrairement à %s, il n’ajoute pas tout seul le caractère nul de fin de chaîne, il faut le faire soi-même. Mais le plus gros problème c’est qu’il lit aussi les retours à la ligne générés par la touche Entrée, et ne s’y arrête pas, ce qui fait que dans l’exemple l’utilisateur est obligé de taper 20 caractères…

gets

La bibliothèque standard fournit des fonctions spécialement faites pour les chaînes de caractères. La plus simple est gets. Simple oui, mais boguée ! Enfin, d’après le manuel. En fait, gets a le même « bug » que scanf utilisé avec "%s" : si l’utilisateur donne une chaîne trop longue pour la variable, on obtient un dépassement de tampon.

fgets

Une fonction plus intéressante est fgets, qui a parmi ses paramètres la taille de la chaîne à lire, ce qui permet, comme dans l’exemple avec scanf("%20s",..., de limiter le nombre caractères lus. Comme scanf("%c"... mais contrairement à scanf("%s"..., fgets lit les espaces et continue aux mots qui les suivent, et lit le retour à la ligne donné par la touche Entrée. Cependant, fgets arrête la lecture après le retour à la ligne et ajoute un caractère nul de fin à la chaîne.

Exemple :

char chaine[81]; // 80 caractères + '\0' terminal
printf("Donnez une phrase (pas plus de 80 car.) : ");
fgets(chaine, 81, stdin);
chaine[strlen(chaine)-1]='\0'; //enlève le '\n'

Remarques :
fgets est faite pour lire une chaîne depuis un fichier, mais en C la lecture d’une chaîne saisie au clavier revient à lire l’entrée standard stdin, qui est vue par le programme comme un fichier.
Nous avons mis 81 pour la taille de la chaîne en paramètre de fgets. C’est pour pouvoir lire 80 caractères, car fgets réserve toujours le dernier pour le caractère nul de fin.
La dernière ligne permet de supprimer le caractère de retour à la ligne qui a été lu par fgets. Il y aura deux caractères nuls après le dernier caractère de la phrase lue mais de toute façon, avec les chaînes tout ce qui se trouve après un caractère nul est toujours ignoré.

Cette fonction est assez satisfaisante mais il reste un inconvénient : la taille de la variable qui recevra la chaîne de caractères doit être fixée à l’avance. On ne sait jamais vraiment combien réserver pour optimiser la mémoire utilisée.

Allocation dynamique

Dans le cas où l’on veut demander plusieurs chaînes à l’utilisateur, un moyen simple d’économiser de la mémoire consiste à suivre les étapes suivantes :

  • lire ces chaînes dans une variable commune « tampon » (appelons-là buffer), qui va resservir pour chaque chaîne ;
  • une fois la chaîne lue, nous pourrons savoir quelle taille lui réserver en mémoire, en utilisant une fonction d’allocation dynamique ;
  • après avoir transféré la chaîne dans l’espace alloué dynamiquement, buffer peut être utilisé pour la chaîne suivante, on répète donc l’opération pour toutes les chaînes.

Exemple :

for (int i=0; i<n; i++) {
    printf("Nom %d : ",i+1);
    char buffer[TAILLE_BUFFER];
    fgets(buffer, TAILLE_BUFFER, stdin);
    buffer[strlen(buffer)-1]='\0';  // enlève le '\n' à la fin
    nom[i]=malloc(strlen(buffer)+1);
    if (nom[i] == NULL) {
        fputs(stderr,"Erreur d'allocation mémoire");
        exit(EXIT_FAILURE);
    }
    strcpy(nom[i], buffer);
}

Nous avons appelé malloc() sans utiliser sizeof, car sizeof(char) vaut toujours 1 par définition. Mais on aurait aussi bien pu faire :

    nom[i]=calloc(strlen(buffer)+1,sizeof(char));

N’oubliez pas de réserver un caractère pour le caractère nul de fin de chaîne, car strlen() ne le compte pas.

N’oubliez pas de considérer le cas où malloc() n’arrive pas à allouer assez de mémoire. Ça peut toujours arriver et si vous ne le prévoyez pas, vous risquez de gros ennuis.
Ici, nous avons choisi dans ce cas d’afficher (fputs) un message sur la sortie d’erreurs standard (stderr). Puis de terminer le programme (exit()) en renvoyant un code de sortie qui indique qu’une erreur s’est produite (EXIT_FAILURE). Vous pourriez réfléchir à un traitement des erreurs plus sophistiqué.

N’oubliez pas non plus de faire le ménage quand vous en aurez terminé avec ces chaînes de caractères : vous leur avez réservé de l’espace, il faudra le libérer vous-même, en appelant free() autant de fois que vous avez appelé malloc() (un appel pour chacune des chaînes du tableau, dans une boucle for par exemple).

Cette solution est assez simple mais n’est pas la meilleure : on se pose toujours la question de déterminer la bonne valeur pour la constante TAILLE_BUFFER.

Solution « fait-main »

Nous allons construire une fonction qui permet de lire une chaîne en résolvant les problèmes que l’on vient de voir.
Puisque l’on veut optimiser la taille de la mémoire utilisée, on doit bien sûr allouer la chaîne dynamiquement.
Ne sachant pas quelle sera la taille de la chaîne, nous allons utiliser realloc() pour l’agrandir au fur et à mesure.

Mais attention, j’ai vu un étudiant faire une boucle qui lisait chaque caractère de la chaîne et à chaque itération appelait realloc() en incrémentant la taille allouée de 1 octet. C’est un très bon raisonnement mais c’était quand même une mauvaise idée : realloc() est une fonction lourde qui ralentit l’exécution, et l’appeler de façon répétée ainsi n’est pas très malin si l’on veut optimiser notre code.

La solution adoptée par de nombreux programmeurs consiste à faire croître la taille du buffer non pas de façon arithmétique, mais de manière géométrique, en la faisant tout simplement doubler à chaque fois qu’il est plein.

/* lire_chaine : affiche un message et lit une chaine de
 *       caractères (avec allocation dynamique)
 * Paramètre :
 *  -  message : invite à afficher
 * Résultat : retourne l'adresse de la chaîne créée
 *   dynamiquement contenant le texte lu ou NULL en cas
 *   d'échec.
 */
char * lire_chaine(char * message){
  printf("%s", message);
  size_t taillbuff = MIN_BUFFER;
  char *buffer = malloc(taillbuff);
  if (buffer == NULL) return NULL; // échec de malloc()
  char *p;
  for(p=buffer ; (*p=getchar()) != '\n' && *p!=EOF ; ++p)
    if (p - buffer == taillbuff - 1) {   // buffer plein,
      p = realloc(buffer, taillbuff *= 2); //on le double
      if (p == NULL) {     // échec de realloc()
        free(buffer);
        return NULL;
      } else buffer = p; //bloc réalloué != buffer
      p += taillbuff/2 - 1; // p reprend sa place dans
    }                       // la nouvelle zone
  *p = 0;
  p = realloc(buffer, p - buffer + 1); //réajustement
  if (p == NULL) {          // échec de realloc
    free(buffer);
    return NULL;
  } else return p;
}

Remarques :

  • La taille initiale MIN_BUFFER peut être choisie à 7 ou 8 par exemple.
  • La fonction getchar() lit un caractère à la fois.
  • realloc() est une fonction à utiliser avec précaution : pour pouvoir changer la taille de la zone allouée, il lui arrive souvent de déplacer cette zone vers un autre endroit de la mémoire, c’est pourquoi elle renvoie en résultat la nouvelle adresse. Non seulement l’ancienne adresse qui lui a été transmise en paramètre n’est plus valide, mais s’il reste des pointeurs qui pointaient vers une partie de l’ancienne zone, il faut les mettre à jour pour qu’ils pointent sur la nouvelle zone.

Dans une première version de cette fonction, j’ai omis ce détail et cela ma coûté quelques soucis.

  • Puisque cette fonction fait une allocation dynamique, il ne faudra pas oublier de libérer avec free() la mémoire pointée par sa valeur de retour une fois que la chaîne lue n’est plus utilisée.

Références

Méthodologie de la programmation en C Norme C 99 – API POSIX Achille Braquelaire Collection: Sciences Sup, Dunod 2005 – 4ème édition EAN13 : 9782100490189. Disponible aux bibliothèques de l’EPST (Bel-Horizon) et de la faculté des Sciences (Chetouane).
Pages de manuel : scanf(3), gets(3), malloc(3). Disponibles en tapant la commande man 3 <fonction>.

Publicités

From → C, Programmation

Laisser un commentaire

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s

%d blogueurs aiment cette page :