Chaînes de caractères

Écrit le 11/11/2004 par Wikibooks
Dernière mise à jour : 02/02/2006

Le langage C offre quelques facilités d'écritures pour simuler les chaînes de caractères à l'aide de tableau. En plus de cela, certaines fonctions de la bibliothèque standard (et les autres) permettent de faciliter leur gestion. À la différence d'un tableau, les chaînes de caractères respectent une convention : se terminer par le caractère '\0' (antislash-zero). Ainsi pour construire une chaîne de caractères « à la main », il ne faut pas oublier ce caractère final sous peine de voir le programme réagir bizarrement.

À noter que les fonctions de la bibliothèque standard sont extrêmement dangeureuses et extrêmement peu pratiques à utiliser. On peut avancer sans trop de risque qu'une des plus grandes lacunes du C provient de sa gestion précaire des chaînes de caractères, pourtant massivement employées dans tout programme digne de ce nom. Pour faire court, on ne saurait trop conseiller de soit reprogrammer les fonctions ci-dessous, d'utiliser une bibliothèque externe ou de faire preuve de paranoïa avant leur utilisation.

Les fonctions permettant de manipuler les chaînes de caractères se trouvent dans le fichier d'entête string.h, ainsi pour les utiliser il faut ajouter la commande préprocesseur :

#include <string.h>

Comparaison de chaînes

int strcmp(const char * chaine1, const char * chaine2);
int strncmp(const char * chaine1, const char * chaine2, size_t longueur);

Compare les chaînes chaine1 et chaine2 et renvoie un entier :
* négatif, si chaine1 est inférieure à chaine2 ;
* nul, si chaine1 est égale à chaine2 ;
* positif, si chaine1 est supérieur à chaine2.

première remarque : lorsque les deux chaînes sont égales, strcmp renvoie 0, qui a la valeur de vérité faux. Pour tester l'égalité entre deux chaînes, il faut donc écrire soit if (strcmp(chaine1, chaine2) == 0) ..., soit if (!strcmp(chaine1, chaine2)) ... mais surtout pas if (strcmp(chaine1, chaine2)) ... qui teste si deux chaînes sont différentes !

seconde remarque : l'opérateur ==, dans le cas de pointeurs, teste si les adresses sont égales. Noter chaine1 == chaine2, si chaine1 et chaine2 sont des char * revient à tester si les deux chaînes pointent sur la même zone mémoire et non pas à tester l'égalité de leur contenu.

À noter l'existence de deux fonctions de comparaisons de chaîne, insensibles à la casse des caractères et s'adaptant à la localisation en cours, fonctionnant sur le même principe que strcmp() et strncmp(), mais dont l'origine provient des systèmes BSD :

int strcasecmp(const char * chaine1, const char * chaine2);
int strncasecmp(const char * chaine1, const char * chaine2, size_t longueur);

Longueur d'une chaîne

size_t strlen(const char * chaine);

Renvoie la longueur de la chaine sans compter le caractère '\0'.

exemple : strlen("coincoin") renvoie 8.

Concaténation et copie de chaînes

char * strcpy (char *destination, const char *source);
char * strncpy (char *destination, const char *source, size_t n);
char * strcat (char *destination, const char *source);
char * strncat (char *destination, const char *source, size_t n);
char * strdup (const char *source);

strcpy copie le contenu de source à l'adresse mémoire pointé par destination, incluant le caractère nul final. strcat concatène la chaîne source à la fin de la chaîne destination, en y rajoutant aussi le caractère nul final.

Toutes ces fonctions renvoient le pointeur sur la chaîne destination.

Il faut être très prudent lors des copies ou des concaténations de chaînes, car les problèmes pouvant survenir peuvent être très pénible à diagnostiquer. L'erreur archi-classique est de faire une copie dans une zone mémoire non réservée, comme dans l'exemple suivant, à priori anodin :

/* Ce code contient une erreur grossière et volontaire */
char * copie_chaine( const char * source )
{
       char * chaine = malloc( strlen(source) );

       if( chaine != NULL )
       return strcpy( chaine, source );
       else
       return NULL;
}

Ce code a priori correct provoque pourtant l'écriture d'un octet dans une zone non allouée. Les conséquences de ce genre d'action sont totalement imprévisibles, pouvant au mieux passer inaperçue, ou au pire écraser des structures de données critiques dans la gestion des blocs mémoires et engendrer des accès mémoire illégaux lors des prochaines tentatives d'allocation ou de libération de bloc. Le cauchemar de tout programmeur. Dans cet exemple il aurait bien évidemment fallu écrire :

       char * chaine = malloc( strlen(source) + 1 );

C'est aussi ce que fait la fonction strdup(), dont la valeur de retour, si elle est différente de NULL, peut être libérée avec free().

Une autre erreur, beaucoup plus fréquente hélas, est de copier une chaîne dans une zone mémoire locale (allouée sur la pile), sans se soucier de savoir si cette zone est capable d'accueillir la nouvelle chaîne. Les conséquences peuvent ici être beaucoup plus graves qu'un simple accès illégal à une zone mémoire globale (allocation dynamique ou variable globale/statique, en général cela met directement fin à l'exécution du programme).

Un écrasement mémoire (buffer overflow) est considéré comme un défaut de sécurité relativement grave, puisqu'un attaquant bien renseigné sur la structure du programme peut effectivement lui faire exécuter du code arbitraire. Ce problème vient de la manière dont les variables locales aux fonctions sont stockées en mémoire, qui sont en fait empilées vers le bas (adresses décroissantes) sur la plupart des systèmes d'exploitation.

Or, une chaîne de caractères (comme tout tableau C) est ordonnée selon les adresses croissantes (c'est à dire que &tableau[i] < &tableau[i+1]). Comme le C permet d'accéder à un tableau en dehors de ses limites, on pourrait donc théoriquement accéder aux valeurs stockées au-delà des déclarations de variables locales. En fait c'est à cet endroit, que le compilateur place une valeur extrêmement critique : l'adresse de retour de la fonction. En s'y prenant bien, on peut donc écraser cette valeur, la faire pointer vers un petit bout de code qui donnerait l'ordre... d'effacer le disque dur (Cette adresse est en fait assez facile à écraser dans la mesure où elle est alignée sur au moins 4 octets) ! Si en plus le programme possède des privilèges, les résultats peuvent être assez catastrophiques.

Voici un exemple très classique, où ce genre d' exploit peut arriver :

/* Ce code contient une erreur grossière et volontaire */
int traite_chaine( const char * ma_chaine )
{
    char tmp[512];

    strcpy( tmp, ma_chaine );

    /* ... */
}

Ce code, hélas plus fréquent qu'on ne le pense, est à banir. Si une copie de la chaîne doit être fait dans une zone mémoire, mieux vaut utiliser la fonction strdup().

On peut aussi s'en sortir en ne copiant qu'un certain nombre des premiers octets de la chaîne, avec la fonction strncpy :

        strncpy( tmp, ma_chaine, sizeof tmp );
    tmp[ sizeof tmp - 1 ] = 0;

On notera l'ajout explicite du caractère nul, si ma_chaine est plus grande que la zone mémoire tmp. La fonction strncpy ne rajoutant hélas pas, dans ce cas, de caractère nul. C'est un problème tellement classique, que toute application C reprogramme en général la fonction strncpy pour prendre en compte ce cas de figure.

La gestion correcte des chaînes de caractères est un problème à ne pas sous-estimer en C. Le langage n'offrant que très peu d'aide, c'est sans doute une des plus grandes lacunes qui a fait haïr le C à beaucoup de gens.

Recherche dans une chaîne

char * strchr(const char * chaine, int caractère);
char * strrchr(const char * chaine, int caractère);

Recherche le caractère dans la chaine et renvoie la position de la première occurence dans le cas de strchr et la position de la dernière occurence dans le cas de strrchr.

char * strstr(const char * meule_de_foin, const char * aiguille);

Recherche l'aiguille dans la meule de foin et renvoie la position de la première occurence.

Traitement des blocs mémoire

La bibliothèque string.h contient encore quelques fonctions pour la manipulation de zone brute de mémoire :
void memcpy( void * destination, const void * source, size_t longueur ); : Copie 'longueur' octet de la zone mémoire 'source' dans 'destination'. Vous devez bien-sûr vous assurer que la chaine destination ait suffisament de place, ce qui est en général plus simple dans la mesure où l'on connait la longueur.<br />Attention au récouvrement des zones : si destination + longueur < source alors source >= destination.

void memmove( void * destination, const void * source, size_t longueur ); : Identique à la fonction memcpy(), mais permet de s'affranchir totalement de la limitation de recouvrement.

void memset( void * memoire, int caractere, size_t longueur ); : Initialise les 'longueur' premiers octets du bloc 'memoire', par la valeur convertie en type char de 'caractere'. Cette fonction est souvent employée pour mettre à zéro tous les champs d'une structure ou d'un tableau :

struct MonType_t mem;

memset( &mem, 0, sizeof mem );

int memcmp( const void * mem1, const void * mem2, size_t longueur ); : Compare les 'longueur' premiers octets des blocs 'mem1' et 'mem2'. Renvoie les codes suivants :

void * memchr( const void * memoire, int caractere, size_t longueur ); : Recherche dans les 'longueur' premiers octets du bloc 'memoire', la valeur convertie en type char de 'caractere'. Renvoie un pointeur sur l'emplacement où le caractère a été trouvé, ou NULL si rien n'a été trouvé.

void * memrchr( const void * memoire, int caractere, size_t longueur ); : Pareil que memchr(), mais commence par la fin du bloc 'memoire'.

Cet article provient de Wikibooks et est sous licence GNU Free Documentation License. Il a été écrit par plusieurs personnes et est constamment mis à jour. Cet article est la version du 28 avril 2005 à 17:26. L'article d'origine se trouve à http://fr.wikibooks.org/wiki/Programmation_C_Chaînes_de_caractères.