Les chaines de caractères en C

Nicolas Joseph

Après un rappel sur la structure des chaines de caractères en C et un aperçu des fonctions de la bibliothèque standard servant à manipuler ces chaines, je vous propose de créer notre propre bibliothèque de manipulation des chaines de caractères.

Commentez ♪

- Titre : Les chaines de caractères en C

- Auteur : Nicolas Joseph

- Parution : 14 novembre 2005

- Mise à jour : 16 décembre 2008

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2005-2008 Nicolas Joseph. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Les chaines de caractères en C

- II. Les fonctions de la bibliothèque standard

- II-A. strcpy

- II-B. strncpy

- II-C. strcat

- II-D. strncat

- II-E. strcmp

- II-F. strncmp

- II-G. strlen

- II-H. strchr

- II-I. strrchr

- II-J. strstr

- II-K. strspn

- II-L. strcspn

- II-M. strpbrk

- II-N. strtok

- III. Création de fonctions avancées

- III-A. Modifier la case d'une chaine de caractères

- III-B. Connaître l'index d'une sous-chaine

- III-C. Extraire une sous-chaine

- III-D. Découper une chaine

- III-E. Fusionner plusieurs chaines de caractères

- III-F. Remplacer une partie d'une chaine

- III-G. Éliminer les espaces superflus

- IV. Remerciements

- V. Code source complet

I - Les chaines de caractères en C

En C, il n'existe pas de type de variable pour les chaines de caractères comme il en existe pour les entiers (int) ou pour les caractères (char). Les chaines de caractères sont en fait stockées dans un tableau de char dont la fin est marquée par un caractère nul, de valeur 0 et représenté par le caractère '\0' ou '\x0' ou la valeur 0 directement(1). En mémoire la chaine « Bonjour » est représentée ainsi :

B

o

n

j

o

u

r

\0

Tout ce qui suit le caractère '\0' sera ignoré :

char s[14] = "Hello\0World!";
printf ("%s\n", s);

Affichera seulement « Hello ». Il ne faut donc pas oublier de réserver une place supplémentaire pour le caractère de fin de chaine sinon on obtient un simple tableau de caractères et dans ce cas, son utilisation en tant que chaine de caractères mène à un comportement indéfini.
Il existe plusieurs manières de déclarer et d'initialiser une chaine de caractères :

char s1[11] = "Developpez";
char s2[] = "Developpez";
char *s3 = "Developpez";

La première méthode réserve une zone de 11 bytes et la chaine « Developpez » y est stockée. La seconde méthode laisse le compilateur calculer la taille appropriée (11 bytes dans notre cas). La dernière méthode ne réserve pas de mémoire, elle se contente de stocker l'adresse d'une chaine de caractères qui peut se trouver dans un endroit de la mémoire non modifiable par le programme, dans ce cas toute modification de la chaine s3 se conclura par un comportement indéterminé. il est donc conseillé d'écrire :

const char *s3 = "Developpez";

Déclarer une chaine de caractères constante est aussi une bonne habitude à prendre lors du passage d'une chaine comme paramètre d'une fonction qui ne modifie pas cette dernière.
Si vous souhaitez utiliser une chaine de taille variable, il faut utiliser les mécanismes d'allocation dynamique de la mémoire :

char *s = malloc (sizeof (*s) * 256);
/*
* Utilisation d'une chaine de 255 caractères maximum
*/
free (s);

II - Les fonctions de la bibliothèque standard

Voici une courte description des fonctions de la bibliothèque standard destinée à la manipulation des chaines de caractères. Il ne faut pas oublier d'inclure string.h avant de les utiliser.

II-A - strcpy

char *strcpy (char *s, const char *ct);

Copie la chaine ct dans s (y compris le caractère de fin de chaine), retourne s.

II-B - strncpy

char *strncpy (char *dst, const char *src, size_t n);

Identique à strcpy en se limitant à n caractères.

Les fonctions limitant le nombre de caractères sont une bonne habitude à prendre pour éviter, dans le cas de , de copier une chaine trop grande et entraîner un comportement indéfini. Cependant, il faut tout de même faire attention puisque, toujours avec , si l'on veut copier une chaine trop grande, le caractère de fin de chaine de ne sera pas copié : on obtient une chaine invalide. Pour faire une utilisation sécurisée de ces fonctions, il faut ajouter nous-mêmes un caractère de fin de chaine à la fin du tableau.

II-C - strcat

char *strcat (char *s, const char *ct);

Concatène la chaine ct à la suite de s et retourne s

II-D - strncat

char *strncat (char *s, const char *ct, size_t n);

Identique à en se limitant à n caractères.

II-E - strcmp

int strcmp (const char *cs, const char *ct);

Compare les chaines cs et ct et renvoie :

- une valeur négative si, lexicalement, cs < ct ;

- une valeur nulle si cs == ct ;

- Une valeur positive si cs > ct.

II-F - strncmp

int strncmp (const char *cs, const char* ct, size_t n);

Identique à en se limitant à n caractères.

II-G - strlen

size_t strlen (const char *cs);

Retourne le nombre de caractères de cs sans tenir compte du caractère de fin de chaine.

II-H - strchr

char *strchr (const char *cs, int c);

Retourne l'adresse de la première occurrence du caractère c dans la chaine cs en partant du début de la chaine.

II-I - strrchr

char *strrchr (const char *cs, int c);

Identique à strchr en commençant par la fin de cs.

II-J - strstr

char *strstr (const char *cs, const char *ct);

Identique à sauf qu'il s'agit de la première occurrence de la sous-chaine ct.

II-K - strspn

size_t strspn (const char *cs, const char *ct);

Cette fonction retourne le nombre de caractères du début de cs appartenant à ct.

II-L - strcspn

size_t strcspn (const char *cs, const char *ct);

C'est l'inverse de la fonction, cette fois, les caractères ne font pas partie de l'ensemble ct.

II-M - strpbrk

char *strpbrk (const char *cs, const char *ct);

Identique à sauf que cette fois, c'est l'ensemble des caractères de ct qui est recherché.

II-N - strtok

char *strtok (char *s, const char *t);

Cette fonction décompose la chaine s en sous-chaines délimitées par un caractère appartenant à ct. Un appel ultérieur à strtok avec s égale à NULL, retourne la sous-chaine suivante ; ct peut être différent à chaque appel.
La chaine passée en argument est modifiée par la fonction : elle ajoute un caractère de fin de chaine à la place du séparateur.

Le fichier string.h contient le prototype d'autres fonctions, mais qui ne sont pas utiles pour la manipulation des chaines de caractères.

III - Création de fonctions avancées

Notre but étant de créer une bibliothèque, les fonctions publiques devront commencer par un préfixe pour éviter les conflits de noms. Le préfixe str suivie d'une lettre est réservé pour les fonctions de la bibliothèque standard, nous utiliserons donc str_. Nous ne devons pas faire de suppositions hasardeuses sur les paramètres fournis par l'utilisateur de notre bibliothèque, aussi vérifier la validité de ceux-ci est nécessaire pour éviter tout arrêt brutal du programme.

III-A - Modifier la case d'une chaine de caractères

Le fichier d'en tête ctype.h propose les fonctions tolower et toupper pour mettre un caractère respectivement en minuscule et en majuscule, il est intéressant de proposer la même chose, mais pour une chaine de caractères :

char *str_tolower (const char *ct)
{
char *s = NULL;

if (ct != NULL)
{
int i;

/* (1) */
s = malloc (sizeof (*s) * (strlen (ct) + 1));
if (s != NULL)
{
/* (2) */
for (i = 0; ct[i]; i++)
{
s[i] = tolower (ct[i]);
}
s[i] = '\0';
}
}
return s;
}

char *str_toupper (const char *ct)
{
char *s = NULL;

if (ct != NULL)
{
int i;

/* (1) */
s = malloc (sizeof (*s) * (strlen (ct) + 1));
if (s != NULL)
{
/* (2) */
for (i = 0; ct[i]; i++)
{
s[i] = toupper (ct[i]);
}
s[i] = '\0';
}
}
return s;
}

Rien de bien compliqué, on commence par allouer une nouvelle chaine de même taille (1) puis on recopie chaque caractère un par un en modifiant sa case (2).

Dans cet exemple, pour parcourir la chaine de caractères j'ai utilisé une boucle for pour des questions de lisibilité, mais on rencontre souvent ce genre de code :

while (*s++)
{
/* ... */
}

La notation *s est équivalente à s[0] donc permet de retourner le premier caractère d'une chaine. Le parcours de la chaine se fait simplement en incrémentant l'adresse du tableau. La boucle s'arrête lorsque l'on atteint le caractère de fin de chaine qui vaut 0 (donc faux). Attention, avec cette méthode, on perd l'adresse du premier élément (en cas d'allocation dynamique, impossible de libérer la mémoire) cependant en cas de restriction d'espace mémoire, cela permet d'économiser une variable.

III-B - Connaître l'index d'une sous-chaine

Il existe la fonction qui permet de trouver l'adresse d'une sous-chaine, mais je trouve plus intéressant de connaître l'index de celle-ci dans le tableau :

int str_istr (const char *cs, const char *ct)
{
int index = -1;

if (cs != NULL && ct != NULL)
{
char *ptr_pos = NULL;

ptr_pos = strstr (cs, ct);
if (ptr_pos != NULL)
{
index = ptr_pos - cs;
}
}
return index;
}

Il faut vérifier le retour de la fonction, car si la sous-chaine n'est pas trouvée, l'indice vaut -1 ce qui provoquera un comportement indéfini en cas d'utilisation de l'indice dans un tableau.

III-C - Extraire une sous-chaine

Cette fonction permet d'extraire une sous-chaine de s comprise entre l'indice start et end.

char *str_sub (const char *s, unsigned int start, unsigned int end)
{
char *new_s = NULL;

if (s != NULL && start < end)
{
/* (1)*/
new_s = malloc (sizeof (*new_s) * (end - start + 2));
if (new_s != NULL)
{
int i;

/* (2) */
for (i = start; i <= end; i++)
{
/* (3) */
new_s[i-start] = s[i];
}
new_s[i-start] = '\0';
}
else
{
fprintf (stderr, "Memoire insuffisante\n");
exit (EXIT_FAILURE);
}
}
return new_s;
}

Grâce aux indices, on calcule la taille de la sous-chaine (1), ensuite on parcourt s entre les deux indices (2) pour copier chaque caractère dans la sous-chaine (3).

III-D - Découper une chaine

J'ai découvert cette fonction en programmant en Perl, elle permet de découper une chaine de caractère suivant un délimiteur et de placer chaque sous-chaine dans un tableau terminé par NULL.

char **str_split (char *s, const char *ct)
{
char **tab = NULL;

if (s != NULL && ct != NULL)
{
int i;
char *cs = NULL;
size_t size = 1;

/* (1) */
for (i = 0; (cs = strtok (s, ct)); i++)
{
if (size <= i + 1)
{
void *tmp = NULL;

/* (2) */
size <<= 1;
tmp = realloc (tab, sizeof (*tab) * size);
if (tmp != NULL)
{
tab = tmp;
}
else
{
fprintf (stderr, "Memoire insuffisante\n");
free (tab);
tab = NULL;
exit (EXIT_FAILURE);
}
}
/* (3) */
tab[i] = cs;
s = NULL;
}
tab[i] = NULL;
}
return tab;
}

Tant que strtok nous renvoi une adresse non nulle (1), on augmente la taille du tableau d'une case (2) dans laquelle on stocke l'adresse retournée par (3).

La variable s passée en paramètre est modifiée par la fonction . De plus le tableau de pointeurs renvoyé par notre fonction fait référence à la chaine passée en paramètre, par conséquent, elle ne doit pas être modifiée ni détruite si vous utilisez le tableau de sous-chaines.

III-E - Fusionner plusieurs chaines de caractères

Encore une fonction inspirée de Perl ! Cette fois-ci, il s'agit de faire l'inverse de la fonction en réunissant des chaines de caractères grâce à un séparateur.

char *str_join (char *cs, ...)
{
va_list va;
const char *ct;
char *s = NULL;
size_t size = 0;

va_start (va, cs);
/* (1) */
while ((ct = va_arg (va, char *)) != NULL)
{
void *tmp = NULL;

/* (2) */
size += strlen (ct) + strlen (cs);
tmp = realloc (s, sizeof (*s) * (size + 1));
if (tmp != NULL)
{
if (s == NULL)
{
/* (3) */
s = tmp;
strcpy (s, ct);
}
else
{
/* (4) */
s = tmp;
strcat (s, cs);
strcat (s, ct);
}
}
else
{
fprintf (stderr, "Memoire insuffisante\n");
free (s);
s = NULL;
exit (EXIT_FAILURE);
}
}
return s;
}

Pour pouvoir gérer un nombre variable d'arguments, la liste doit être terminée par la valeur NULL (1). Ensuite pour chaque argument, on calcule la nouvelle taille de la chaine finale sans oublier d'ajouter la taille du séparateur (2). Pour la première chaine, on la copie simplement (3) et pour les autres (4), on commence par concaténer le séparateur puis la nouvelle chaine.

III-F - Remplacer une partie d'une chaine

Cette fonction permet de remplacer length caractères de la chaine s à partir de l'indice start par une nouvelle chaine ct. Une nouvelle chaine est créée, il ne faut pas oublier de libérer la mémoire.

char *str_remplace (const char *s, unsigned int start, unsigned int lenght, const char *ct)
{
char *new_s = NULL;

if (s != NULL && ct != NULL && start >= 0 && lenght > 0)
{
size_t size = strlen (s);

/* (1) */
new_s = malloc (sizeof (*new_s) * (size - lenght + strlen (ct) + 1));
if (new_s != NULL)
{
/* (2) */
memcpy (new_s, s, start);
/* (3) */
memcpy (&new_s[start], ct, strlen (ct));
/* (4) */
memcpy (&new_s[start + strlen (ct)], &s[start + lenght], size - lenght - start + 1);
}
}
else
{
fprintf (stderr, "Memoire insuffisante\n");
exit (EXIT_FAILURE);
}
return new_s;
}

1. On commence par créer un nouvel emplacement mémoire pour stocker la chaine modifiée.

2. Ensuite, la nouvelle chaine est créée par copier/coller du début de s.

3. Puis à l'indice start, on ajoute la chaine de remplacement ct.

4. Pour finir par la fin de la chaine de départ s (sans oublier de copier le caractère de fin de chaine).

III-G - Éliminer les espaces superflus

Pour finir notre bibliothèque, je vous propose une fonction qui supprime les espaces superflus dans une chaine de caractères.

char *str_strip (const char *string)
{
char *strip = NULL;

if (string != NULL)
{
strip = malloc (sizeof (*strip) * (strlen (string) + 1));
if (strip != NULL)
{
int i, j;
int ps = 0;

for (i = 0, j = 0; string[i]; i++)
{
if (string[i] == ' ')
{
if (ps == 0)
{
strip[j] = string[i];
ps = 1;
j++;
}
}
else
{
strip[j] = string[i];
ps = 0;
j++;
}
}
strip[j] = '\0';
}
else
{
fprintf (stderr, "Memoire insuffisante\n");
exit (EXIT_FAILURE);
}
}
return strip;
}

Pour supprimer les espaces, il suffit de recopier la chaine de caractères initiale et lorsque l'on rencontre un espace, on regarde s'il s'agit du premier (si l'on a déjà rencontré un espace, la variable ps vaut 1), si c'est le cas, on le copie comme n'importe quel caractère et l'on met ps à 1, par contre si ce n'est pas le premier, on ne le copie pas. Bien sûr, si l'on copie autre chose qu'un espace, il ne faut pas oublier de mettre ps à zéro pour que le prochain espace soit copié.

IV - Remerciements

Merci à khayyam90 pour la relecture attentive de cet article.

V - Code source complet

En plus des fonctions vues dans la partie précédente, le fichier str.h redéfini les fonctions de la bibliothèque standard, ceci peut nous permettre par la suite de réécrire ses fonctions (pour ajouter la vérification de la validité des paramètres par exemple).

str.h

#ifndef H_STRING
#define H_STRING

#include <string.h>

/* Redefinition des fonctions standards pour un usage futur, par exemple verifier
la validite des arguments. */
#define str_cpy  strcpy
#define str_ncpy strncpy
#define str_cat  strcat
#define str_ncat strncat
#define str_cmp  strcmp
#define str_ncmp strncmp
#define str_len  strlen
#define str_chr  strchr
#define str_rchr strrchr
#define str_str  strstr
#define str_spn  strspn
#define str_pbrk strpbrk
#define str_tok  strtok

char *str_tolower (const char *);
char *str_toupper (const char *);
int str_istr (const char *, const char *);
char *str_sub (const char *, unsigned int, unsigned int);
char **str_split (char *, const char *);
char *str_join (char *, ...);
char *str_remplace (const char *, unsigned int, unsigned int, const char *);
char *str_strip (const char *);

#endif /* not H_STRING */

str.c

#include <ctype.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include "str.h"

char *str_tolower (const char *ct)
{
char *s = NULL;

if (ct != NULL)
{
int i;

s = malloc (sizeof (*s) * (strlen (ct) + 1));
if (s != NULL)
{
for (i = 0; ct[i]; i++)
{
s[i] = tolower (ct[i]);
}
s[i] = '\0';
}
}
return s;
}

char *str_toupper (const char *ct)
{
char *s = NULL;

if (ct != NULL)
{
int i;

s = malloc (sizeof (*s) * (strlen (ct) + 1));
if (s != NULL)
{
for (i = 0; ct[i]; i++)
{
s[i] = toupper (ct[i]);
}
s[i] = '\0';
}
}
return s;
}

int str_istr (const char *cs, const char *ct)
{
int index = -1;

if (cs != NULL && ct != NULL)
{
char *ptr_pos = NULL;

ptr_pos = strstr (cs, ct);
if (ptr_pos != NULL)
{
index = ptr_pos - cs;
}
}
return index;
}

char *str_sub (const char *s, unsigned int start, unsigned int end)
{
char *new_s = NULL;

if (s != NULL && start < end)
{
new_s = malloc (sizeof (*new_s) * (end - start + 2));
if (new_s != NULL)
{
int i;

for (i = start; i <= end; i++)
{
new_s[i-start] = s[i];
}
new_s[i-start] = '\0';
}
else
{
fprintf (stderr, "Memoire insuffisante\n");
exit (EXIT_FAILURE);
}
}
return new_s;
}

char **str_split (char *s, const char *ct)
{
char **tab = NULL;

if (s != NULL && ct != NULL)
{
int i;
char *cs = NULL;
size_t size = 1;

for (i = 0; (cs = strtok (s, ct)); i++)
{
if (size <= i + 1)
{
void *tmp = NULL;

size <<= 1;
tmp = realloc (tab, sizeof (*tab) * size);
if (tmp != NULL)
{
tab = tmp;
}
else
{
fprintf (stderr, "Memoire insuffisante\n");
free (tab);
tab = NULL;
exit (EXIT_FAILURE);
}
}
tab[i] = cs;
s = NULL;
}
tab[i] = NULL;
}
return tab;
}

char *str_join (char *cs, ...)
{
va_list va;
const char *ct;
char *s = NULL;
size_t size = 0;

va_start (va, cs);
while ((ct = va_arg (va, char *)) != NULL)
{
void *tmp = NULL;

size += strlen (ct) + strlen (cs);
tmp = realloc (s, sizeof (*s) * (size + 1));
if (tmp != NULL)
{
if (s == NULL)
{
s = tmp;
strcpy (s, ct);
}
else
{
s = tmp;
strcat (s, cs);
strcat (s, ct);
}
}
else
{
fprintf (stderr, "Memoire insuffisante\n");
free (s);
s = NULL;
exit (EXIT_FAILURE);
}
}
return s;
}

char *str_remplace (const char *s, unsigned int start, unsigned int lenght, const char *ct)
{
char *new_s = NULL;

if (s != NULL && ct != NULL && start >= 0 && lenght > 0)
{
size_t size = strlen (s);

new_s = malloc (sizeof (*new_s) * (size - lenght + strlen (ct) + 1));
if (new_s != NULL)
{
memcpy (new_s, s, start);
memcpy (&new_s[start], ct, strlen (ct));
memcpy (&new_s[start + strlen (ct)], &s[start + lenght], size - lenght - start + 1);
}
}
else
{
fprintf (stderr, "Memoire insuffisante\n");
exit (EXIT_FAILURE);
}
return new_s;
}

char *str_strip (const char *string)
{
char *strip = NULL;

if (string != NULL)
{
strip = malloc (sizeof (*strip) * (strlen (string) + 1));
if (strip != NULL)
{
int i, j;
int ps = 0;

for (i = 0, j = 0; string[i]; i++)
{
if (string[i] == ' ')
{
if (ps == 0)
{
strip[j] = string[i];
ps = 1;
j++;
}
}
else
{
strip[j] = string[i];
ps = 0;
j++;
}
}
strng[j] = '\0';
}
else
{
fprintf (stderr, "Memoire insuffisante\n");
exit (EXIT_FAILURE);
}
}
return strip;
}

(1)Le chiffre 0 (zéro) est le caractère '0'. Sa valeur dépend du charset utilisé (en ASCII, 48 décimal).