Les Chaînes de caractères La puissance des pointeurs : les arbres Cours suivant

 Fichier : strings.cpp 

C'est vrai, j'en suis conscient. Pour l'instant, nous avons abordé des points importants, mais nous n'avons toujours rien fait qui produise réellement des résultats amusants. Un type de données que nous n'avons toujours pas abordé, et qui pourtant est essentiel en programmation est la chaîne de caractères. Certains langages en ont fait un type interne (au même rang que les int ou les double), d'autres on adopté des conventions pour représenter les chaînes de caractères, mais ne les a pas inclus dans le langage (c'est le cas du C). Enfin, d'autres langages sont conçus de telle manière que l'ajout d'un type "chaîne de caractère" se fait de manière très élégante... j'ai nommé le C++.

C'est donc au chaînes de caractères que ce cours est dédié. Ces chaînes seront abordées sous deux angles différents. En effet, le monde du C++ est encore fortement imprégné par les reflexes et les outils du C. Nous allons donc voir dans un premier temps comment on gère les chaînes de caractères en C, qui ne propose qu'un certain nombre de fonctions pour les maniupler, mais qui ne les a pas intégré comme un type à part entière. Ces manipulations sont parfois peu pratiques, et ont maintes fois montré leurs limites en provocant d'innombrables erreurs de segmentation et parfois même des brêches de sécurité dans des systèmes (vous êtes curieux de savoir comment?). Mais elles sont encore largement utilisées, et ce site ne serait pas complet sans en avoir parlé au moins une fois.

Et puis nous verrons l'approche qu'à choisi le C++. Cette approche fait appel à des concepts que nous n'avons pas encore abordé, en particulier les concepts objets, mais ce sera un parfait premier exemple de manipulations d'objets en C++.

 
   

Avant de commencer, je me dois de parler de quelque chose que nous avons complètement ignoré jusqu'ici : la nouvelle norme C++. Jusqu'ici, nous n'avons utilisé que des principes du C++ qui en faisaient parti quasiment depuis le début. Mais le C++ est un langage encore jeune, et les détails de son fonctionnement ne sont pas encore tous au point (quoiqu'il ne devrait plus y avoir de changements majeurs à partir de maintenant). Actuellement, le C++ en est à sa troisième version. Ne confondez pas ce numéro de version avec celui de votre compilateur : ils n'ont rien à voir. En gros, cela signifie qu'après sa création, il y a eu deux grands changements dans le langage (principalement des ajouts), qui l'ont séparé toujours un peu plus de son ancêtre direct, le C. Lors de la naissance de la troisième version, les concepteurs du C++ y ont ajouté toute une série d'outils qui permettent au programmeur de ne plus réinventer la roue à chaque fois qu'ils commencent un programme. La manipulation des chaînes de caractères fut bien sûr l'une des priorités des concepteurs, mais ils ont également pensé à de nombreux autres outils intéressants. Cette collection d'outils est ce qu'on appelle communément la STL (Standard Template Library). Historiquement, STL était une librairie séparée du C++ (mais écrite dans ce langage), et qui contenait un grand nombre de fonctions générales pour aider les programmeurs. Cette STL a été adoptée et modifiée par le commité de conception du C++, alors qu'elle existe toujours de manière autonome. Par abus de langage, on parle cependant de la STL pour désigner la librairie qui vient en standard avec la nouvelle version du C++. Les chaînes de caractères que nous allons voir en C++ viennent de cette STL. Enfin, en plus de cette librairie, le C++ introduit de nouvelles syntaxes et de nouveaux mots-clés, mais il s'agit de notions relativement avancées, et nous n'en parlerons que très peu dans le cadre de ce site.

C'est pourquoi, il vous faudra un compilateur relativement récent pour travailler, faute de quoi votre programme ne compilera pas.

Un test simple, pour savoir si votre compilateur supporte la nouvelle norme est de tenter de compiler le programme suivant (en prenant en compte chaque instruction telle qu'elle est écrite ici) :

 

#include <iostream>
using namespace std;

int main(void)
{
   cout << "La new norme, c'est plus better!" << endl;
}

Nous reparlerons des changements induits par la nouvelle norme en abordant the C++ way of chaînes de caractères.

 

    Les chaînes de caractères en C 


Commençons par plonger les mains dans le cambouis, et revenons à la définition que nous avons donné d'une chaîne de caractères. Il s'agit ni plus ni moins que d'un tableau de char, chaque case contenant une lettre (un caractère ASCII), et la dernière lettre de la chaîne étant suivie d'une case contenant la valeur 0 (ou plus exactement le caractère nul, '\0'). Cette convention n'est - comme son nom l'indique - qu'une convention, mais c'est celle qu'a été choisie par les concepteurs du C. Ce système a ses avantages et ses inconvénients : il faut par exemple parcourir toute la chaîne pour en connaître la longueur. D'un autre côté, c'est un format assez léger qui ne prend qu'un octet de plus que ce qu'il ne faut pour contenir la chaîne.

Pour l'instant, cette définition des chaînes de caractères vous paraît peut-être un peu statique. Voici pour l'instant ce que nous savons faire avec des chaines de caracatères :

char chaine[] = "Le C++, c'est pas mal!"
 
cout << "Avant : " << chaine << endl; 
chaine[3] = 'D'
cout << "Apres : " << chaine << endl; 

C'est bien, mais c'est peu. Nous aimerions bien pouvoir manipuler avec un peu plus de possibilités les chaînes de caractères, et effectuer des copies, des concaténations, des comparaisons ou même créer des chaînes de caractères contenant des nombres et des données qu'on n'a pas forcément lors de l'écriture du problème.

Le but de ce cours n'est pas de vous donner tous les détails des fonctions de manipulation des chaînes de caractères en C. La librairie string.h contenant ces fonctions est vaste, et il existe de nombreuses très bonnes documentations à ce sujet. Si vous travaillez sous Unix/Linux, vous pouvez toujours faire un "man fonction" dans un terminal. Si vous travaillez sous Windows, votre compilateur vient sûrement avec une documentation complète de ces fonctions. Ce que nous allons faire ici, c'est de voir en gros le panorama des fonctions classiques. Il ne vous sera pas difficile d'aller par vous-même plus en détail par la suite.

    Langage C - Copier des chaînes de caractères 

La première opération, qui est également l'une des plus simples, est la copie de chaînes de caractères. Elle se fait grâce à la fonction strcpy(char* destination, char* source). Copier une chaîne de caractères est utile lorsqu'on veut travailler sur une chaîne dans une fonction, mais sans modifier l'original. Voici un exemple :

#include <iostream.h> 
#include <string.h> 
 
void fonction(char* chaine) 

   char chaine2[100]; 
   int longueur = strlen(chaine); 
    
   if(longueur > 99
   { 
       cout << "La chaine parametre est trop longue!" << endl; 
       return
   } 
 
   strcpy(chaine2, chaine); 
   chaine2[longueur - 1] = '2'
   cout << chaine2 << endl; 

 
void main() 

   fonction("Voici la chaine numero 1"); 
}

La fonction intéressante est bien sûr la fonction fonction(). Elle prend en paramètre une chaîne de caractères, nommée chaine. Voici ensuite les étapes de la fonction :

Pour ce qui est de l'affichage de cet exemple, il n'y a aucune surprise :

  Voici la chaîne numero 2

 

Remarque

Contrairement à ce qu'on aurait envie de croire intuitivement, c'est le second paramètre de la fonction strcpy() qui est la source, et le premier paramètre qui est la destination. La confusion est fréquente lorsqu'on n'utilise pas souvent cette fonction.

 

Avant de passer à la concaténation (coller une chaîne à une autre), je voudrais revenir sur un point important. La fonction strcpy() copie une chaîne d'un endroit à un autre de la mémoire. Elle ne peut donc travailler correctement que si l'endroit qui va recevoir la copie a été alloué avec une taille suffisante. Le petit exemple ci-dessous va illustrer deux erreurs classiques : oublier d'allouer de la place, et ne pas allouer assez de place. Dans cet exemple, on utilisera l'allocation dynamique, car c'est une opération que vous serez souvent ammené à faire si vous travaillez à la manière C :

#include <string.h> 
 
void main() 

   char* chaine1 = "Chaine de caracteres"
   char* chaine2; 
   char* chaine3; 
   char* chaine4; 
    
   // Oups! Pas d'allocation préalable! 
   // -> ERREUR 
   strcpy(chaine2, chaine1); 
    
   // Oups! Il manque un octet! 
   // -> ERREUR 
   chaine3 = new char[strlen(chaine1)]; 
   strcpy(chaine3, chaine1); 
    
   // OK, tout est bien fait : 
   // -> tout va bien 
   chaine4 = new char[strlen(chaine1) + 1]; 
   strcpy(chaine4, chaine1); 
}

Notez bien que ces erreurs ne vous sont pas signalées par le compilateur : il ne sait pas comment travaille strcpy(), il sait juste qu'elle attends deux pointeurs de type char*. Restez donc toujours vigilants lorsque vous travaillez avec ces fonctions issues de la librairie C.

Voici enfin une petite image pour ceux qui sentent qu'ils n'ont pas encore parfaitement compris le coup de l'allocation de la chaîne :

Remarque

Imaginez que la mémoire est une table, et qu'un pointeur indique un endroit de cette table. Sur cette table, vous avez un verre plein d'eau : votre chaîne de caractères. Le verre représente un espace alloué, et l'eau représente les octets qu'il y a dans cet espace ; donc vous "fabriquez" votre verre avec l'instruction new, et vous le "remplissez" avec la fonction strcpy(). Le but du jeu est de ne pas avoir d'eau sur la table.

Voici ce qui se passe lorsque vous appelez la fonction strcpy() : vous demandez au C de verser à un endroit A de la table autant d'eau qu'il y en a à un endroit B (le verre déjà rempli). Le C va joyeusement s'exécuter, et va verser à l'endroit A exactement autant d'eau qu'il y a à B... mais sans s'occuper de savoir s'il y a un verre en A ou non.

Si vous n'avez pas vous-même mis un verre à cet endroit-là, il va - sans vergogne - verser de l'eau directement sur la table. Bravo, vous venez de planter votre PC. Notez que vous aurez également de l'eau sur la table si vous ne créez pas un verre assez grand.

 

 

    Langage C - Concaténer des chaînes de caractères 

Maintenant que nous savons copier des chaînes de caractères, nous pouvons presque les concaténer : après tout, une concaténation de chaînes, ce n'est ni plus ni moins que copier une chaîne à la suite d'une autre.

La fonction de la librairie string.h qui s'occupe de concaténer deux chaînes est la fonction strcat(char* str1, char* str2) : elle copie la seconde chaîne à la suite de la première. Tout comme pour la copie de chaînes, il faut s'assurer qu'il y a bien la place de copier la seconde à la suite de la première - il faut pour cela avoir prévu un tableau assez grand. Il est intéressant de tenter de faire la concaténation par nous-même : nous allons donc voir un petit exemple qui procède de deux façons différentes.

#include <iostream.h> 
#include <string.h> 
 
void main() 

   char* fin = "le C++"
   char debut1[100]; 
   char debut2[100]; 
   strcpy(debut1, "On aime bien "); 
   strcpy(debut2, "On n'aime pas "); 
    
   // Première façon : à la main 
   strcpy(debut1 + strlen(debut1), fin); 
    
   // Seconde façon : avec strcat() 
   strcat(debut2, fin); 
    
   cout << debut1 << endl; 
   cout << debut2 << endl; 
}

Les mêmes règles que pour strcpy() s'appliquent ici aussi : il faut toujours prévoir une taille de destination assez grande, et toujours vérifier si c'est le cas. Dans cet exemple, nous ne le faisons pas car ça fait beaucoup de code pour peu de choses, mais souvenez-vous que c'est important.

La première façon de procéder est la façon dont on ferait naturellement la concaténation : on copie la seconde chaîne à l'endroit du zéro terminal de la première chaîne. Ainsi, on a créé une nouvelle chaîne qui est la réunion des deux, mais sans zéro au milieu de la chaîne. Dans la pratique, on a effectué un calcul sur les pointeurs : adresse de la première case de debut1, puis décalage de strlen(debut1) octets pour se retrouver à la position du zéro terminal de debut1.

La seconde façon de procéder utilise la fonction strcat(), et sera notre façon de procéder tant que nous resterons dans le langage C.

Je vais ici marquer une petite pause pour revenir sur le problème de la taille des zones mémoires qui vont recevoir le résultat des opérations de chaînes. En effet, vous aurez constaté qu'il faut toujours prévoir assez de place, et que si par malheur le programme n'en avait pas assez, il plante. Il s'agit là d'une constante source d'erreurs en C, et les concepteurs de la librairie string.h ont mis en place un certain nombre de fonctions qui permettent de réduire les risques d'écriture à l'extérieur des zones prévues. Il s'agit des mêmes fonctions que précédemment, mais prenant un paramètre supplémentaire : un nombre maximal d'octets qu'il faut copier/ajouter. Les équivalents des fonctions strcpy() et strcat() se nomment (attention, c'est très subtil) strncpy() et strncat().

Il est souvent recommandé d'utiliser ces fonctions (il existe d'autres fonctions qui sont déclinées en deux versions sur le même modèle) dans des programmes pouvant constituer des failles de sécurités dans les systèmes. Beaucoup d'attaques informatiques utilisent une technique qui consiste justement à remplir des tableaux de char au-delà de leur capacité prévue, ce qui a pour effet de modifier directement le code du programme! Il serait donc bon de prendre pour habitude de systématiquement utiliser les fonctions strn* plutôt que les str*, même si l'utilisation en est un peu plus lourde.

 

    Langage C - Formatage de chaînes de caractères 

Maintenant que nous savons recopier à tout va des chaînes de caractères, il nous faudrait pouvoir les créer. Certes, il y a l'initialisation lors de la déclaration, mais c'est bien maigre. Comment créer une chaîne qui contienne par exemple le nom que l'utilisateur a entré au clavier lors de l'exécution? Ou encore un nombre? Vous vous en doutez, les concepteurs du C ne vous ont pas laissé en plan devant ce cas. Vous n'aurez pas a écrire les fonctions vous-même, mais vous aurez à apprendre une fonction un peu particulière, sprintf(), qui est un des membres de la familles des très fameuses fonctions *printf() du langage C. Pour vous qui connaissez ces fonctions, ou du moins la version de base, printf(), vous n'aurez aucune difficulté pour vous familiariser avec sprintf(). Pour les autres, c'est une autre pair de manches.

Le programmeur C connaît la fonction printf() depuis sa plus tendre et verte enfance. C'est une fonction très puissante qui permet, dans un langage comme le C, de faire des affichages complexes, en mélangeant des chaînes statiques (connues lors de la compilation) à des données dynamiques. Revenons à ce que nous savons faire en C++ :

cout << "Merci, M. " << nomDuClient << ", de votre visite.\n";

Ceci nous permet d'afficher un message qui change avec la valeur de nomDuClient. Notons au passage un point important : a priori, nomDuClient est une chaîne de caractères, cela semble logique, mais il pourrait très bien s'agir d'un int, ou d'un autre type fondamental que nous connaissons. La syntaxe ci-dessus est possible grâce à certaines caractéristiques du langage C++, que le langage C ne possède pas. Plus exactement, les concepteurs du "cout" (nous verrons de quoi il s'agit un peu plus tard) ont fait en sorte que l'opérateur "<<" puisse servir à concaténer des chaînes de caractères et d'autres données pouvant être représentées sous cette forme, afin de les afficher les unes à la suite des autres. Le C++ permet en effet de surdéfinir le sens des opérateurs... mais nous reparlerons de ça plus tard. Pour ce qui est du C, ce dernier n'offre pas cette possibilité, et use donc d'un autre stratagème pour obtenir des résultats similaires. Voici, en avant-première, et parce que je vous aime bien, la version C de l'instruction ci-dessus :

printf("Merci, M. %s, de votre visite.\n", nomDuClient);

C'est vrai, c'est moins joli. Nous sommes ici revenu à l'utilisation d'une fonction, au lieu d'utiliser des opérateurs mutants. Cependant, le résultat est le même à l'affichage. Vous aurez peut-être déjà deviné que la fonction printf() va remplacer, lors de l'affichage, le petit "%s" par ce qu'il y a dans nomDuClient.

La fonction printf() est par bien des points particulière. D'une part, c'est certainement une fonction très complexe. Elle est capable d'afficher (c'est-à-dire traduire) des nombres et des chaînes de caractères, avec plus ou moins de chiffres avant et après la virgule, avec une marge à droite ou a gauche, avec ou sans signe... De ce point de vue, elle est très complète. Autre particularité, elle prend un nombre variable d'arguments. En effet, dès qu'on souhaite inclure plusieurs variables ou valeurs dans l'affichage, il devient fastidieux d'écrire plusieurs instructions printf() les unes à la suite des autres. Le C, comme le C++, offre la possibilité d'écrire des fonctions prenant un nombre variable de paramètres. Il s'agit d'une fonctionnalité complexe, qu'il n'est pas forcément nécessaire de connaître, mais qui est appliquée très judicieusement dans ce cas-ci. Voyons un second exemple de l'utilisation de printf() :

#include <stdio.h>    // Contient printf() 
main() 

   char* nomDuClient = "Robert"
   int nombreDeVoitures = 2
   printf("M. %s possède %d voitures.\n", nomDuClient, nombreDeVoitures); 
}

Cette fois-ci, la fonction prend trois arguments, au lieu de deux auparavant. Comme avant, le premier argument est ce qu'on appelle la chaîne de contrôle. C'est le "squelette" de la chaîne qui va être affichée à l'écran. Les arguments suivants sont des variables ou des valeurs immédiates qui sont destinées à être affichées. Vous aurez tout de suite (j'en suis sûr) noté l'apparition d'un autre luron, "%d". Il est temps d'expliquer un peu le fonctionnement de la chaîne de contrôle.

Contrairement au "cout", la fonction printf() ne peut pas s'affranchir du type des données à afficher. Il lui faut un indice. Cet indice est donné par les caractères de contrôle que sont les "%s", "%d" et autres "%p" qu'on utilise avec cette fonction. Ils indiquent non seulement la position à laquelle doit être affichée la variable correspondante, mais aussi le type de celle-ci. En ce qui concerne la position, elle est relativement intuitive : les caractères de contrôle s'appliquent dans le même ordre que les variables données par la suite. Dans ce cas, le "%s" s'applique à nomDuClient, et le "%d" s'applique à nombreDeVoitures.

Voyons maintenant l'indication de type pour les données de la fonction printf(). Jusqu'ici, nous avons constaté que %s désignait une chaîne de caractères, et, dans l'exemple précédent, %d désigne un entier. Voici un petit tableau plus complet des types reconnus pas printf() :

      %d, %i       Entier signé décimal
      %u   Entier non signé décimal
      %o   Entier non signé octal
      %x, %X   Entier non signé hexadécimal (resp. a-f et A-F)
      %f   Réel décimal notation classique ("1.58")
      %e, %E   Réel décimal notation scientifique ("8.8e3" ou "8.8E3" resp.)
      %c   Charactère ASCII
      %s   Chaîne de caractères
      %p   Adresse mémoire (pointeur), en hexadécimal

Il existe d'autres caractères de contrôle reconnus par printf(), mais cette liste n'est donnée qu'à titre élémentaire : si vous souhaitez réellement connaître toutes les options de printf(), reportez-vous à sa documentation. Sans plus attendre, voyons tout de suite quelques exemples d'utilisation :

#include <stdio.h> 
 
main() 

   char* texte = "Chaine!"
   int valeur = 12
    
   printf("10 + 15 = %d\n"10+15); 
   printf("10 * %f = %e\n"3.14159310*3.141593); 
   printf("%d s'écrit %x (ou %X) en hexa!\n", valeur, valeur, valeur); 
   printf("Voici une %s\n", texte); 
   printf("la variable 'valeur' se situe à %p\n", &valeur); 
}

L'affichage produit est le suivant :

  10 + 15 = 25
10 * 3.141593 = 3.141593e+001
12 s'écrit c (ou C) en hexa!
Voici une Chaine!
la variable 'valeur' se situe à 0065FDF0

Vous devriez maintenant avoir une petite idée de comment fonctionne la fonction printf(). Il serait alors temps de revenir à notre but de départ : la fonction sprintf() qui va nous permettre de formatter des chaînes de caractères. Le "f" de printf() signifie "format". Rajoutez à cela un "s" pour "string", et vous savez ce que fait sprintf()!

La fonction sprintf(), également définie dans stdio.h, fonctionne de la même façon que printf(), mis à part qu'au lieu d'envoyer le résultat à l'affichage, elle le place dans une chaîne donnée en premier paramètre :

char chaine[100]; 
sprintf(chaine, "Bonjour M. %s", nomDuClient);

Encore une fois, comme avec les fonctions précédentes, il faut s'assurer que la taille de chaine est bien suffisante pour accueillir le résultat de sprintf(). Nous pouvons maintenant formater des chaînes de caractères en fonction de données que nous n'aurons pas forcément lors de la compilation. Ceci permet de facilement convertir des nombres en chaînes de caractères, d'obtenir la traduction en hexadécimal d'un entier... tant de tâches si difficiles à faire à la main, et pourtant si utiles.

Nous venons de passer quelques minutes à expliquer le fonctionnement de la fonction printf() - vous apprécierez au passage la complexité d'une tâche qu'on pensait si simple : l'affichage de valeurs à l'écran (il faut surdéfinir des opérateurs, ou alors recourir à des fonctions au nombre de paramètres variable, etc...). Certes, il ne s'agit pas de C++ mais de C. Sachez cependant qu'il est rare de ne jamais avoir besoin de connaître le C lorsqu'on fait du C++, et que vous croiserez certainement ces fonctions plus d'une fois dans votre longue et brillante carrière de développeur. Il n'est donc pas complètement inutile de les connaître. Cependant, dans un programme C++, il n'est pas conseillé de mélanger les instructions cout et printf(), car elles utilisent des mécanismes différents qui peuvent mener à de subtils bugs d'affichage dans vos programmes. Pensez-y!

 

    Langage C - Comparaison de chaînes de caractères 

Une opération courante, et pas seulement dans l'informatique, est de classer des mots par ordre alphabétique. Pour cela, il faut d'abord pouvoir les comparer, afin de dire qu'une chaîne est placée avant ou après une autre avec ce classement. La bibliothèque string.h met à notre disposition quelques fonctions effectuant des comparaisons entre chaînes de caractères. Il s'agit des fonctions strcmp(), strncmp(), stricmp() et strnicmp(). Ces quatre fonctions prennent deux chaînes de caractères en argument, et renvoient un entier signé en fonction de la comparaison : si la première chaîne est "avant" la seconde dans l'ordre alphabétique, le résultat est strictement négatif. Si c'est l'inverse, le résultat est strictement positif. Si les deux chaînes sont égales, les fonctions renvoient 0.

La fonction strcmp(char* str1, char* str2) compare les deux chaînes entières, et tient compte des différences majuscules/minuscules. Notez que sur un ordinateur classique, les majuscules sont placées avant les minuscules dans l'ordre alphabétique. La fonction strncmp(char* str1, char* str2, int n) effectue le même travail, mais ne compare que les n premiers caractères des deux mots.

Les fonctions strcmp(char* str1, char* str2) et strnicmp(char* str1, char* str2, int n) se comportent comme strcmp() et strncmp() respectivement, mais ne tiennent pas compte des différences majuscules/minuscules.

#include <iostream.h> 
#include <string.h> 
 
int main() 

   char* mot1 = "malin"
   char* mot2 = "Piano"
   int res = strcmp(mot1, mot2); 
   cout << "Comparaison avec casse : " << res << endl; 
   res = stricmp(mot1, mot2); 
   cout << "Comparaison sans casse : " << res << endl; 
    
   return 0
}

Vous constaterez que la valeur renvoyée par str*cmp() ne vaut pas forcément -1, 0 ou 1 comme beaucoup de gens le pensent. En réalité, cela dépend de votre implémentation.

 

    Langage C - Recherche dans une chaîne de caractères 

Finalement, il nous reste une chose à voir : la recherche d'un caractère ou d'une chaîne de caractères dans une autre chaîne de caractères. Il ne serait pas difficile d'écrire une telle fonction soi-même, en particulier pour la recherche d'un caractère, mais si il existe plusieurs façons de rechercher des caractères dans une chaîne, dépendamment du cas dans lequel vous vous situez. La bibliothèque string.h nous offre des fonctions capables d'effectuer des recherches un peu plus avancées, et méritent d'être un peu explorées.

La fonction strchr(char* str, char c) recherche la première occurence du caractère c dans la chaîne str. Cette fonction considère le zéro terminal comme faisant partie de la chaîne. Sa consoeur, strrchr(char* str, char c) effectue le même type de recherche, mais dans l'autre sens (en commençant par la fin). Les deux fonctions renvoient un pointeur vers la position à laquelle le caractère recherché à été trouvé en premier.

Si on veut rechercher un caractère parmi plusieurs possibles (par exemple tous les opérateurs dans une expression arithmétique), il existe la fonction strpbrk(char* str, char* symboles). Elle renvoie un pointeur vers la position du premier caractère trouvé dans str faisant partie de symboles.

strpbrk("2 + 4 = x""+-*/%");

Si on souhaite rechercher une chaîne à l'intérieur d'une autre, on utilise la fonction strstr(char* str1, char* str2), qui renvoie la position de la première occurence de str1 dans str2.

Vous voici en possession de la connaissance ultime (ou presque) en termes de manipulation de chaînes de caractères en C. Encore une fois, si nous avons fait un détour par le bon vieux C - alors que vous êtes tous impatients de découvrir the C++ way of trituring the chains of characters - c'est parce que vous allez probablement rencontrer assez souvent ces fonctions. Et puis faire un peu de C n'a jamais fait de mal à personne, hein?

Plus sérieusement, cette petite excursion nous a permis de mettre en lumière la difficulté de la manipulation de chaînes de caractères en C, alors qu'il s'agit d'un type qu'on aurait envie de classer comme fondamental. On a ici clairement atteint une des limites du C : sur ces chaînes de caractères, les bugs ont connu leur heure de gloire, avec des tableaux trop petits, et autres problèmes d'allocation. Cette histoire de taille de tableau est une véritable plaie, d'autant plus que le C n'offre pas la possibilité de simplement les agrandir dynamiquement. Il faut alors passer par des réallocations de mémoire, des recopies, etc... Le C++ arrive alors sur son beau destrier blanc pour sauver les programmeurs de la calvitie qui provient plus souvent de l'arrachage intempestif des cheuveux que d'un simple signe de l'âge. Comme nous l'avons dit précédamment, le C++ va permettre d'implémenter simplement et élégamment un type de données comme la chaîne de caractères.

Il faut savoir que dans un premier temps, ce "type" n'existait pas officiellement. Ainsi, Borland et autres Microsoft proposaient à leurs petits développeurs des String et des CString et des BString pour pallier à ce manque... résultat : les programmes les plus simples ne pouvaient même plus passer d'un compilateur à un autre, pour peu qu'ils utilisent l'une ou l'autre de ces versions propriétaires de chaînes de caractères. Les concepteurs du C++ ont pris le problème à bras le corps et ont décidé d'offrir, avec la version 3 du langage, un type string prédéfini, afin de supprimer une bonne fois pour toutes cette partie du travail du programmeur.

Nous allons frôler ici quelques concepts avancés du C++ que je garde pour le chapitre suivant. Ces concepts, je vais les laisser de côté pour l'instant. Mon seul but ici est de vous montrer comment faire en C++ standard ce que nous avons fait en C un peu plus tôt. Nous n'entrerons donc malheureusement pas dans les détails pour savoir comment les choses sont faites. En tout cas, même si les concepts sous-jacents sont complexes, vous allez constater avec quelle simplicité le C++ nous laisse traiter les chaînes de caractères.

 

    Les chaînes de caractères en C++ 

Avant de nous lancer tête baissée dans les exemples, nous allons rapidement reparler de la nouvelle norme (introduite dans un petit paragraphe au début de ce cours). Vous allez noter certains changements, principalement dans les directives #include : nous allons nous affranchir de l'extension ".h" des fichiers d'en-tête. Nous allons également rajouter une instruction qu'il ne faudra pas oublier par la suite si vous choisissez de rester dans la nouvelle norme. Voyons tout de suite un petit exemple pour voir de quoi je parle :

#include <iostream> 
#include <string> 
 
using namespace std; 
 
int main() 

   string nomDuClient = "Robert"
    
   string message = "Bonjour, M. " + nomDuClient; 
   message += '.'
    
   cout << message << endl;     
}

Les trois premières lignes peuvent paraître légèrement nouvelles, certes. Voyons d'abord pourquoi nous avons enlevé l'extension du nom du fichier d'en-tête. Si vous relisez le petit paragraphe du début de ce cours, vous vous souviendrez que le C++ a introduit une nouvelle norme qui a changé un certain nombre de choses. L'un de ces changements fut un changement majeur dans l'organisation des noms (noms de variables, de fonctions, de structs, de classes, etc...), avec l'aparition des espaces de noms. Souvenez-vous : lorsque nous avons parlé des structures, nous avons dit qu'il était possible d'avoir un même nom de variable au sein de structures différentes ; cela ne posait aucun problème de collision de noms. Par contre, en dehors des structures, il est impossible d'avoir des noms de variables, de constantes ou de fonctions qui soient identiques. Ceci posait de nombreux problèmes à chaque fois qu'un développeur proposait par exemple une librairie de fonctions qui portaient parfois le même nom que celles d'une librairie d'un autre développeur. Il était alors impossible d'utiliser les deux librairies dans un même programme. Les espaces de noms constituent un moyen d'isoler les fonctions et les noms de variables dans une même unité de nommage.

Prenons un exemple concret. Dans la nouvelle norme, toutes les fonctions et tous les objets des librairies standard du C++ ont été placées dans l'espace de noms std. Dans cet espace de noms, on retrouve en particulier string et cout. En réalité, leur nom pleinement qualifié est maintenant std::string et std::cout (nous reviendrons sur les espaces de noms dans un autre cours). Mais vous sentez comme moi qu'il est fastidieux d'écrire ce "std::" à chaque fois qu'on veut effectuer un affichage. Etant donné que le C++ - dans sa nouvelle norme - nous oblige à préciser l'espace de noms de chaque objet (sauf pour ceux qui n'en ont pas, mais ce n'est plus le cas des objets de la librairie standard), il met à notre disposition un raccourci, qui est justement cette instruction using namespace std. En la plaçant au début de votre programme, vous précisez au compilateur que vous n'indiquerez pas l'espace de noms pour les objets situés dans l'espace de noms std.

En faisant passer tous les objets de la librairie standard dans l'espace de noms std, les concepteurs de cette nouvelle norme auraient cassé la compatibilité des programmes C++ antérieurs à ce changement. Ils ont donc conservé les anciennes librairies telles qu'elles étaient, et ont placé les nouvelles dans d'autres fichiers. En n'utilisant pas l'extension ".h" pour le fichier d'en-tête, vous précisez que vous souhaitez utiliser les librairies de la nouvelle norme.

Mais que se passe-t-il pour la librairie string.h? La librairie C et la librairie C++ portent pourtant le même nom? Là encore, il a fallu prévoir le coup : dans les règles de la nouvelle norme, les anciennes librairies C ont également perdu leur extension, mais pour éviter tout conflit comme celui-ci, elles ont gagné un "c" devant leur nom. Résumons :

Je ne rentrerai pas plus dans le détail des espaces de noms pour le moment, mais il me semblait important d'expliquer ce point fondamental avant de continuer.

Pour en revenir à nos chaînes de caractères, vous constaterez dans le petit exemple ci-dessus que leur manipulation semble à priori très intuitive. La déclaration ne pose aucun problème :

string nomDuClient = "Robert";

De cet aspect-là, rien n'a changé du C (mis à part le type, qui est maintenant string au lieu de char*). Vous noterez au passage que mon petit programme de coloration syntaxique n'a pas mis en gras le type string comme il le fait si bien pour les types fondamentaux int, char, etc... C'est tout simplement, comme nous l'avons déjà dit, parce que string n'est pas un type fondamental, mais bien un objet fabriqué de toutes pièces grâce aux outils fournis par le C++.

Ensuite, notre exemple crée un message qui est la concaténation de trois morceaux de chaînes de caractères :

string message = "Bonjour, M. " + nomDuClient; 
message += '.';

Soyons même plus précis : il s'agit d'une première concaténation entre une chaîne de caractères constante de type char* ("Bonjour, M. "), d'une string (nomDuClient), suivie d'une seconde concaténation entre une string (message) et un caractère simple ('.'). Toutes ces concaténations se font de manière très simple en utilisant l'opérateur d'addition +, ce qui est bien plus pratique et intuitif pour nous programmeurs que d'utiliser des fonctions de concaténations lourdes et redondantes.

Sur ce point-là, le C++ a mis à notre disposition un outils réellement plus pratique que les tableaux de type char*. Bien sûr, la copie de chaînes de caractères est tout aussi simple :

string str1 = "Coucou"
string str2 = str1; 
cout << str2 << endl;

Ici encore, un simple opérateur d'affectation suffit pour effectuer la copie.

 

    Langage C++ - Conversions depuis d'autres types

Les concepteurs du C++ ont décidé que la classe string ne contiendrait que des fonctions destinées à la création et à la manipulation de chaînes de caractères ; ils ont entre autres complètement mis de côté la conversion de valeurs numériques et autres en chaînes de caractères. Afin de faire cela, on doit donc passer par un copain de la classe string qui s'appelle strstream et qui habite dans le fichier d'en-tête du même nom.

Les objets strstream s'utilisent exactement de la même façon que l'objet cout que nous avons déjà vu tant de fois. Nous avons pu constater qu'il était aussi aisé de sortir des chaînes de caractères que des valeurs numériques. Il suffit simplement d'écrire cout << truc pour que, (quasiment) quelque soit le type de truc, sa valeur soit affichée correctement.

Or, il se trouve que les objets strstream sont des cousins des ostream dont fait partie cout. En fait, ils en sont même des déscendants. De se lien de parenté, ils tirent l'immense avantage d'avoir déjà à leur disposition les opérateurs << utilisés par les ostream (et d'ailleurs, il peuvent faire quasiment tout ce dont est capable cout, dans les limites du raisonnable : il s'agit ici d'objets en mémoire, par de flux de sortie à l'écran). Ainsi, dès qu'un type a été rajouté à la liste des objets pouvant être affichés par cout (car ce n'est pas automatique, il faut un peu de code pour réussir cela - mais pas beaucoup), il peut également être inscrit dans un strstream. Voici une illustration :

 
#include <strstream>  
 
strstreams;  
s << "J'ai " << 20 << " ans";

On utilise donc le strstream exactement comme le cout. Il est ensuite possible de convertir un strstream en string, grâce à la fonction s.str(). Cette opération est d'ailleurs nécessaire si on souhaite afficher le strstream en question. Je m'illustre plus clairement :

#include <strstream>  
 
strstream s;  
s << "J'ai " << 20 << " ans";  
string s2 = s.str(); // conversion en string  
cout << s.str(); // affichage

De ce point de vue, le formatage d'une chaîne de caractères passe par un objet du type strstream. Ceci n'est pas tellement un défaut ou une limitation, plus qu'un choix d'outil : un objet strstream peut parfois être plus adapté à l'utilisation que vous souhaitez en faire qu'un objet string. Pensez à utiliser les deux, vous serez plus performant!

 

    Langage C++ - Comparaison de chaînes de caractères

Tout comme en C, on trouve en C++ des outils pour effectuer des comparaisons entre deux chaînes, avec quelque réserve toutefois... mais on ne peut avoir que des avantages, et l'apport du C++ est important : la simplicité.

 
string a = ...; 
string b = ...; 
 
cout << (a < b) << endl; 
cout << (a <= b) << endl; 
cout << (a == b) << endl; 
 
cout << (a < "hello") << endl; 
cout << ("hello" < b) << endl; 

Vous le constatez : la comparaison de strings est immédiate de simplicité (ça se dit, ça?). Notez au passage que vous pouvez parfaitement comparer des strings avec des chaînes de caractères classiques du C. De ce point de vue, ces braves petites strings sont nos amies. Par contre, voici le contre-coup : ce type de comparaison est sensible à la casse des caractères, un point c'est tout. Si vous souhaitez effectuer des comparaisons non sensibles, vous devrez passer par les fonctions du C (c'est encore ce qu'il y a de plus simple).

Et vous de vous écrier : les fonctions du C prennent des char* en paramètres, alors que nous disposons de strings. Et moi de vous répondre : pas de problème! Il existe bien sûr une petite fonction magique qui s'occupe de régler ce problème...

 
#include <iostream> 
#include <string> 
#include <cstring> 
 
int main() 

   string a = "bonjour"
   char b[100]; 
 
   strcpy(b, a.c_str()); 
   a = "au revoir"
   cout << "a = " << a << endl; 
   cout << "b = " << b << endl; 
}

J'ai bien sûr nommé la fonction c_str(), qui s'applique à un objet du type string. Cette fonction renvoie un pointeur vers la chaîne de caractère interne de l'objet. Attention toutefois : n'allez pas modifier ce qu'il y a à cet endroit-là de la mémoire, c'est n'est pas votre rôle. La string s'occupe très bien de tout cela, et il ne faut surtout pas se mêler de ses affaires personnelles. Afin de s'assurer que vous n'alliez pas changer le contenu de la mémoire à cet endroit-là, la fonction renvoie un pointeur constant, et le compilateur vous empâchera de faire les modifications ou d'appeler des fonctions qui peuvent le faire. Je ne me priverais pas, au passage, de vous faire remarquer que la bibliothèque dans laquelle est définie la fonction strcpy() s'appelle maintenant csting, correspondant à la bibliothèque string.h de l'ancienne norme.

 
Remarque

Si vous devez utiliser la fonction c_str(), assurez-vous de recopier le contenu de la mémoire dans un tableau qui vous appartient avant de commencer les modifications.

 

 
    Langage C++ - Recherche dans une chaîne de caractères 


Enfin, un dernier point à aborder pour conclure ce cours de manipulation de chaînes de caractères : la recherche dans les chaînes de caractères. Ici aussi, nous allons pouvoir faire un parallèle entre le C et le C++, et utiliser des fonctions dites d'instance, c'est-a-dire qui s'appliquent à une instance d'un objet, exactement comme les fonctions c_str() ou str() vues précédemment.

Plus particulièrement, nous allons commencer par voir la fonction find(), qui prend comme argument la chaîne de caractères à rechercher et qui renvoie la position de la première occurence trouvée, un peu comme le fait la fonction C strstr() équivalente. Voici un exemple :

string a = "bonjour"
int p = a.find("jour"); 
cout << p << endl;

Cet exemple provoque l'affichage de l'entier 3. En effet, le début de la chaîne "jour" dans la chaîne "bonjour" correspond bien à la position 3 (quatrième caractère). Notez bien que vous pouvez ici utiliser une variable de type string comme argument de la fonction find(), au lieu d'utiliser une chaîne de caractères classique, comme c'est le cas ici. Si la chaîne à rechercher n'est pas trouvée, la fonction renvoie -1. Cela nous convient bien, puisqu'il n'y a aucun risque d'ambiguité avec une position valide.

Si vous souhaitez commencer la recherche un peu plus loin dans la fonction, vous pouvez rajouter un second paramètre donnant la position à laquelle la recherche doit commencer. Voici une illustration, avec un morceau de code affichant toutes les positions à laquelle la chaîne est trouvée :

#include <iostream> 
#include <string> 
#include <cstring> 
 
int main() 

   string a = "bonjour, comment allez-vous?"
   int i = a.find('o'); 
 
   while(i != -1
   { 
       cout << i << endl; 
       i = a.find('o', i+1); 
   } 

Le programme recherche la première occurence de la lettre 'o' dans la chaîne "bonjour, comment allez-vous?" (notez encore une fois que la fonction find() connaît plusieurs types pour le premier paramètre, puisqu'ici nous utilisons un simple char). Si elle en trouve une, elle l'affiche, et recherche l'occurence suivante en commençant à la position suivant la position de la dernière occurence (vous avez suivi?). Ainsi, nous passons à travers toute la chaîne sans retrouver plusieurs fois la même occurence.

Nous pouvons maintenant effectuer la recherche dans le sens inverse, grâce à la fonction rfind() : le travail est le même, mis à part qu'on part de la fin de la chaîne.

    The End of The Calvaire

Nous n'irons pas plus loin dans la manipulation de chaînes de caractères pour ce cours. Vous savez maintenant tout ce qu'il vous faut savoir pour faire des manipulations élémentaires et classiques, et je vous invite, par la suite, à vous plonger dans la documentation de votre C++ afin de savoir tout ce qu'il y a savoir sur les objets de type string. Vous découvririez alors qu'il est possible d'effectuer des modifications de la chaîne, et des opérations un peu plus complexes (et donc plus amusantes) sur les chaînes de caractères. Mais ce n'est pas nécessaire de savoir tout cela.

Pour ce cours, il n'y a pas d'exercices prédéfinis : vous aurez souvent à manipuler des chaînes de caractères, alors vous apprendrez en faisant. Si vous souhaitez jouer avec les strings, tout ce que je peux dire est : lancez-vous! Plus vous en ferez, mieux vous les manierez. Et surtout, n'hésitez pas à vous plonger dans la documentation. Vous pourrez trouver la documentation sur les strings sur le site de Microsoft msdn.microsoft.com, dans la section C++ Language Reference, à défaut d'autre chose.

Voici cependant quelques suggestions :

Questions

  • Reprenez un algorithme de tri que nous avons déjà vu et appliquez-le a des chaînes de caractères, à la fois en C et en C++
  • Essayez de reprogrammer les ftonctions strcpy() et strcat(). Pour les plus hardis, essayez aussi strcmp().
  • Sauriez-vous faire une fonction prenant une chaîne en paramètre et renvoyant 1 (vrai) si cette chaîne contient des balises HTML?
  • Beaucoup mieux - et beaucoup plus difficile : sauriez-vous écrire un programme qui lise une expression mathématique, du style 4+7 et qui en calcule le résultat? Commencez par des nombres à 1 chiffre, avec peu de vérification syntaxique (pas la pein de s'inquiéter d'erreurs comme 4++7 pour l'instant, on supposera que toutes les expressions sont correctes) et les 4 opérateurs de base + - * et /. Une fois que vous avez fait ça, vous pouvez étendre en utilisant les parenthèses, les priorités opératoires et tout ce que vous voudrez.

Je vous invite aussi à regarder le fichier source associé à ce cours, qui contient un récapitulatif des fonctions et techniques utilisées dans ce cours.

Voir aussi  

Les tableaux - La gestion dynamique de la mémoire


Retour au sommaire Haut de la page La puissance des pointeurs : les arbres Cours suivant