Les chaînes de caractères

Dans ce chapitre, nous allons apprendre à manipuler du texte ou, en langage C, des chaînes de caractères.

Qu'est-ce qu'une chaîne de caractères ?

Dans le chapitre sur les variables, nous avions mentionné le type char. Pour rappel, nous vous avions dit que le type char servait surtout au stockage de caractères, mais que comme ces derniers étaient stockés dans l’ordinateur sous forme de nombres, il était également possible d’utiliser ce type pour mémoriser des nombres.

Le seul problème, c’est qu’une variable de type char ne peut stocker qu’une seule lettre, ce qui est insuffisant pour stocker une phrase ou même un mot. Si nous voulons mémoriser un texte, nous avons besoin d’un outil pour rassembler plusieurs lettres dans un seul objet, manipulable dans notre langage. Cela tombe bien, nous en avons justement découvert un au chapitre précédent : les tableaux.

C’est ainsi que le texte est géré en C : sous forme de tableaux de char appelés chaînes de caractères (strings en anglais).

Représentation en mémoire

Néanmoins, il y a une petite subtilité. Une chaîne de caractères est un peu plus qu’un tableau : c’est un objet à part entière qui doit être manipulable directement. Or, ceci n’est possible que si nous connaissons sa taille.

Avec une taille intégrée

Dans certains langages de programmation, les chaînes de caractères sont stockées sous la forme d’un tableau de char auquel est adjoint un entier pour indiquer sa longueur. Plus précisément, lors de l’allocation du tableau, le compilateur réserve un élément supplémentaire pour conserver la taille de la chaîne. Ainsi, il est aisé de parcourir la chaîne et de savoir quand la fin de celle-ci est atteinte. De telles chaînes de caractères sont souvent appelées des Pascal strings, s’agissant d’une convention apparue avec le langage de programmation Pascal.

Avec une sentinelle

Toutefois, une telle technique limite la taille des chaînes de caractères à la capacité du type entier utilisé pour mémoriser la longueur de la chaîne. Dans la majorité des cas, il s’agit d’un unsigned char, ce qui donne une limite de 255 caractères maximum sur la plupart des machines. Pour ne pas subir cette limitation, les concepteurs du langage C ont adopté une autre solution : la fin de la chaîne de caractères sera indiquée par un caractère spécial, en l’occurrence zéro (noté '\0'). Les chaînes de caractères qui fonctionnent sur ce principe sont appelées null terminated strings, ou encore C strings.

Cette solution a toutefois deux inconvénients :

  • la taille de chaque chaîne doit être calculée en la parcourant jusqu’au caractère nul ;
  • le programmeur doit s’assurer que chaque chaîne qu’il construit se termine bien par un caractère nul.

Définition, initialisation et utilisation

Définition

Définir une chaîne de caractères, c’est avant tout définir un tableau de char, ce que nous avons vu au chapitre précédent. L’exemple ci-dessous définit un tableau de vingt-cinq char.

char tab[25];
Initialisation

Il existe deux méthodes pour initialiser une chaîne de caractères :

  • de la même manière qu’un tableau ;
  • à l’aide d’une chaîne de caractères littérale.
Avec une liste d’initialisation
Initialisation avec une longueur explicite

Comme pour n’importe quel tableau, l’initialisation se réalise à l’aide d’une liste d’initialisation. L’exemple ci-dessous définit donc un tableau de vingt-cinq char et initialise les sept premiers avec la suite de lettres « Bonjour ».

char chaine[25] = { 'B', 'o', 'n', 'j', 'o', 'u', 'r' };

Étant donné que seule une partie des éléments est initialisée, les autres sont implicitement mis à zéro, ce qui nous donne une chaîne de caractères valide puisqu’elle est bien terminée par un caractère nul. Faites cependant attention à ce qu’il y ait toujours de la place pour un caractère nul.

Initialisation avec une longueur implicite

Dans le cas où vous ne spécifiez pas de taille lors de la définition, il vous faudra ajouter le caractère nul à la fin de la liste d’initialisation pour obtenir une chaîne valide.

char chaine[] = { 'B', 'o', 'n', 'j', 'o', 'u', 'r', '\0' };
Avec une chaîne littérale

Bien que tout à fait valide, cette première solution est toutefois assez fastidieuse. Aussi, il en existe une seconde : recourir à une chaîne de caractères littérale pour initialiser un tableau. Une chaîne de caractères littérale est une suite de caractères entourée par le symbole ". Nous en avons déjà utilisé auparavant comme argument des fonctions printf() et scanf().

Techniquement, une chaîne littérale est un tableau de char terminé par un caractère nul. Elles peuvent donc s’utiliser comme n’importe quel autre tableau. Si vous exécutez le code ci-dessous, vous remarquerez que l’opérateur sizeof retourne bien le nombre de caractères composant la chaîne littérale (n’oubliez pas de compter le caractère nul) et que l’opérateur [] peut effectivement leur être appliqué.

#include <stdio.h>


int main(void)
{
    printf("%zu\n", sizeof "Bonjour");
    printf("%c\n", "Bonjour"[3]);
    return 0;
}
Résultat
8
j

Ces chaînes de caractères littérales peuvent également être utilisées à la place des listes d’initialisation. En fait, il s’agit de la troisième et dernière exception à la règle de conversion implicite des tableaux.

Initialisation avec une longueur explicite

Dans le cas où vous spécifiez la taille de votre tableau, faites bien attention à ce que celui-ci dispose de suffisamment de place pour accueillir la chaîne entière, c’est-à-dire les caractères qui la composent et le caractère nul.

char chaine[25] = "Bonjour";
Initialisation avec une longueur implicite

L’utilisation d’une chaîne littérale pour initialiser un tableau dont la taille n’est pas spécifiée vous évite de vous soucier du caractère nul puisque celui-ci fait partie de la chaîne littérale.

char chaine[] = "Bonjour";
Utilisation de pointeurs

Nous vous avons dit que les chaînes littérales n’étaient rien d’autre que des tableaux de char terminés par un caractère nul. Dès lors, comme pour n’importe quel tableau, il vous est loisible de les référencer à l’aide de pointeurs.

char *ptr = "Bonjour";

Néanmoins, les chaînes littérales sont des constantes, il vous est donc impossible de les modifier. L’exemple ci-dessous est donc incorrect.

int main(void)
{
    char *ptr = "bonjour";

    ptr[0] = 'B'; /* Incorrect */
    return 0;
}

Notez bien la différence entre les exemples précédents qui initialisent un tableau avec le contenu d’une chaîne littérale (il y a donc copie de la chaîne littérale) et cet exemple qui initialise un pointeur avec l’adresse du premier élément d’une chaîne littérale.

Utilisation

Pour le reste, une chaîne de caractères s’utilise comme n’importe quel autre tableau. Aussi, pour modifier son contenu, il vous faudra accéder à ses éléments un à un.

#include <stdio.h>


int main(void)
{
    char chaine[25] = "Bonjour";

    printf("%s\n", chaine);
    chaine[0] = 'A';
    chaine[1] = 'u';
    chaine[2] = ' ';
    chaine[3] = 'r';
    chaine[4] = 'e';
    chaine[5] = 'v';
    chaine[6] = 'o';
    chaine[7] = 'i';
    chaine[8] = 'r';
    chaine[9] = '\0'; /* N'oubliez pas le caractère nul ! */
    printf("%s\n", chaine);
    return 0;
}
Résultat
Bonjour
Au revoir

Afficher et récupérer une chaîne de caractères

Les chaînes littérales n’étant rien d’autre que des tableaux de char, il vous est possible d’utiliser des chaînes de caractères là où vous employiez des chaînes littérales. Ainsi, les deux exemples ci-dessous afficheront la même chose.

#include <stdio.h>


int main(void)
{
    char chaine[] = "Bonjour\n";

    printf(chaine);
    return 0;
}
#include <stdio.h>


int main(void)
{
    printf("Bonjour\n");
    return 0;
}
Résultat
Bonjour

Toutefois, les fonctions printf() et scanf() disposent d’un indicateur de conversion vous permettant d’afficher ou de demander une chaîne de caractères : s.

Printf

L’exemple suivant illustre l’utilisation de cet indicateur de conversion avec printf() et affiche la même chose que les deux codes précédents.

#include <stdio.h>


int main(void)
{
    char chaine[] = "Bonjour";

    printf("%s\n", chaine);
    return 0;
}
Résultat
Bonjour

Afin d’éviter de mauvaises surprises et notamment le cas où un utilisateur vous fournirait en entrée une chaîne de caractères comprenant des indicateurs de conversions, préférez toujours afficher une chaine à l’aide de l’indicateur de conversion s et non directement comme premier argument de printf().

Scanf

Le même indicateur de conversion peut être utilisé avec scanf() pour demander à l’utilisateur d’entrer une chaîne de caractères. Cependant, un problème se pose : étant donné que nous devons créer un tableau de taille finie pour accueillir la saisie de l’utilisateur, nous devons impérativement limiter la longueur des données que nous fournit l’utilisateur.

Pour éviter ce problème, il est possible de spécifier une taille maximale à la fonction scanf(). Pour ce faire, il vous suffit de placer un nombre entre le symbole % et l’indicateur de conversion s. L’exemple ci-dessous demande à l’utilisateur d’entrer son prénom (limité à 254 caractères) et affiche ensuite celui-ci.

La fonction scanf() ne décompte pas le caractère nul final de la limite fournie ! Il vous est donc nécessaire de lui indiquer la taille de votre tableau diminuée de un.

#include <stdio.h>
#include <stdlib.h>


int main(void)
{
    char chaine[255];

    printf("Quel est votre prénom ? ");

    if (scanf("%254s", chaine) != 1)
    {
        printf("Erreur lors de la saisie\n");
        return EXIT_FAILURE;
    }

    printf("Bien le bonjour %s !\n", chaine);
    return 0;
}
Résultat
Quel est votre prénom ? Albert
Bien le bonjour Albert !
Chaîne de caractères avec des espaces

Sauf qu’en fait, l’indicateur s signifie : « la plus longue suite de caractère ne comprenant pas d’espaces » (les espaces étant ici entendus comme une suite d’un ou plusieurs des caractères suivants : ' ', '\f', '\n', '\r', '\t', '\v')…

Autrement dit, si vous entrez « Bonjour tout le monde », la fonction scanf() va s’arrêter au mot « Bonjour », ce dernier étant suivi par un espace.

Comment peut-on récupérer une chaîne complète alors ? :euh:

Eh bien, il va falloir nous débrouiller avec l’indicateur c de la fonction scanf() et réaliser nous même une fonction employant ce dernier au sein d’une boucle. Ainsi, nous pouvons par exemple créer une fonction recevant un pointeur sur char et la taille du tableau référencé qui lit un caractère jusqu’à être arrivé à la fin de la ligne ou à la limite du tableau.

Et comment sait-on que la lecture est arrivée à la fin d’une ligne ?

La fin de ligne est indiquée par le caractère \n.
Avec ceci, vous devriez pouvoir construire une fonction adéquate.
Allez, hop, au boulot et faites gaffe aux retours d’erreurs ! :pirate:

#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>


bool lire_ligne(char *chaine, size_t max)
{
    size_t i;

    for (i = 0; i < max - 1; ++i)
    {
        char c;

        if (scanf("%c", &c) != 1)
            return false;
        else if (c == '\n')
            break;

        chaine[i] = c;
    }

    chaine[i] = '\0';
    return true;
}


int main(void)
{
    char chaine[255];

    printf("Quel est votre prénom ? ");

    if (lire_ligne(chaine, sizeof chaine))
        printf("Bien le bonjour %s !\n", chaine);

    return 0;
}
Résultat
Quel est votre prénom ? Charles Henri
Bien le bonjour Charles Henri !

Gardez bien cette fonction sous le coude, nous allons en avoir besoin pour la suite. ;)

Si vous tentez d’utiliser plusieurs fois la fonction lire_ligne() en entrant un nombre de caractères supérieur à la limite fixée, vous constaterez que les caractères excédentaires ne sont pas perdus, mais sont lus lors d’un appel subséquent à lire_ligne(). Ce comportement sera expliqué plus tard lorsque nous verrons les fichiers.

Lire et écrire depuis et dans une chaîne de caractères

S’il est possible d’afficher et récupérer une chaîne de caractères, il est également possible de lire depuis une chaîne et d’écrire dans une chaîne. À cette fin, deux fonctions qui devraient vous sembler familières existent : snprintf() et sscanf().

La fonction snprintf
int snprintf(char *chaine, size_t taille, char *format, ...);

Les trois points à la fin du prototype de la fonction signifient que celle-ci attend un nombre variable d’arguments. Nous verrons ce mécanisme plus en détail dans la troisième partie du cours.

La fonction snprintf() est identique à la fonction printf() mis à part que celle-ci écrit les données produites dans une chaîne de caractères au lieu de les afficher à l’écran. Elle écrit au maximum taille - 1 caractères dans la chaîne chaine et ajoute elle-même un caractère nul à la fin. La fonction retourne le nombre de caractères écrit sans compter le caractère nul final ou bien un nombre négatif en cas d’erreur. Toutefois, dans le cas où la chaîne de destination est trop petite pour accueillir toutes les données à écrire, la fonction retourne le nombre de caractères qui aurait dû être écrit (hors caractère nul, à nouveau) si la limite n’avait pas été atteinte.

Cette fonction peut vous permettre, entre autres, d’écrire un nombre dans une chaîne de caractères.

#include <stdio.h>
#include <stdlib.h>


int main(void)
{
    char chaine[8];
    unsigned long long n = 64;

    if (snprintf(chaine, sizeof chaine, "%llu", n) < 0)
    {
        printf("Erreur lors de la conversion\n");
        return EXIT_FAILURE;
    }

    printf("chaine : %s\n", chaine);
    n = 18446744073709551615LLU;
    int ret = snprintf(chaine, sizeof chaine, "%llu", n);

    if (ret < 0)
    {
        printf("Erreur lors de la conversion\n");
        return EXIT_FAILURE;
    }
    if (ret + 1 > sizeof chaine) // snprintf ne compte pas le caractère nul
        printf("La chaîne est trop petite, elle devrait avoir une taille de : %d\n", ret + 1);

    printf("chaine : %s\n", chaine);
    return 0;
}
Résultat
chaine : 64
La chaîne est trop petite, elle devrait avoir une taille de : 21
chaine : 1844674

Comme vous le voyez, dans le cas où il n’y a pas assez d’espace, la fonction snprintf() retourne bien le nombre de caractères qui auraient dû être écrit (ici 20, caractère nul exclu). Cette propriété peut d’ailleurs être utilisée pour calculer la taille nécessaire d’une chaîne à l’avance en précisant une taille de zéro. Dans un tel cas, aucun caractère ne sera écrit, un pointeur nul peut alors être passé en argument.

#include <stdio.h>
#include <stdlib.h>


int main(void)
{
    unsigned long long n = 18446744073709551615LLU;
    int ret = snprintf(NULL, 0, "%llu", n);

    if (ret < 0)
    {
        printf("Erreur lors de la conversion\n");
        return EXIT_FAILURE;
    }
    else
        printf("Il vous faut une chaîne de %d caractère(s)\n", ret + 1);

    return 0;
}
Résultat
Il vous faut une chaîne de 21 caractère(s)
La fonction sscanf
int sscanf(char *chaine, char *format, ...);

La fonction sscanf() est identique à la fonction scanf() si ce n’est que celle-ci extrait les données depuis une chaîne de caractères plutôt qu’en provenance d’une saisie de l’utilisateur. Cette dernière retourne le nombre de conversions réussies ou un nombre inférieur si elles n’ont pas toutes été réalisées ou enfin un nombre négatif en cas d’erreur.

Voici un exemple d’utilisation.

#include <stdio.h>
#include <stdlib.h>


int main(void)
{
    char chaine[10];
    int n;

    if (sscanf("5 abcd", "%d %9s", &n, chaine) != 2)
    {
        printf("Erreur lors de l'examen de la chaîne\n");
        return EXIT_FAILURE;
    }

    printf("%d %s\n", n, chaine);
    return 0;
}
Résultat
5 abcd

Les classes de caractères

Pour terminer cette partie théorique sur une note un peu plus légère, sachez que la bibliothèque standard fournit un en-tête <ctype.h> qui permet de classifier les caractères. Onze fonctions sont ainsi définies.

int isalnum(int c);
int isalpha(int c);
int isblank(int c);
int iscntrl(int c);
int isdigit(int c);
int isgraph(int c);
int islower(int c);
int isprint(int c);
int ispunct(int c);
int isspace(int c);
int isupper(int c);
int isxdigit(int c);

Chacune d’entre elles attend en argument un caractère et retourne un nombre positif ou zéro suivant que le caractère fourni appartienne ou non à la catégorie déterminée par la fonction.

Fonction Catégorie Description Par défaut
isupper Majuscule Détermine si le caractère entré est une lettre majuscule 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y' ou 'Z'
islower Minuscule Détermine si le caractère entré est une lettre minuscule 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'o', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y' ou 'z'
isdigit Chiffre décimal Détermine si le caractère entré est un chiffre décimal '0', '1', '2', '3', '4', '5', '6', '7', '8' ou '9'
isxdigit Chiffre hexadécimal Détermine si le caractère entré est un chiffre hexadécimal '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'a', 'b', 'c', 'd', 'e' ou 'f'
isblank Espace Détermine si le caractère entré est un espacement blanc ' ' ou '\t'
isspace Espace Détermine si le caractère entré est un espace ' ', '\f', '\n', '\r', '\t' ou '\v'
iscntrl Contrôle Détermine si le caractère est un caractère dit « de contrôle » '\0', '\a', '\b', '\f', '\n', '\r', '\t' ou '\v'
ispunct Ponctuation Détermine si le caractère entré est un signe de ponctuation '!', '"', '#', '%', '&', ''', '(', ')', '*', '+', ',', '-', '.', '/', ':', ';', '<', '=', '>', '?', '[', '\', ']', '^', '_', '{', '<barre droite>', '}' ou '~'
isalpha Alphabétique Détermine si le caractère entré est une lettre alphabétique Les deux ensembles de caractères de islower() et isupper()
isalnum Alphanumérique Détermine si le caractère entré est une lettre alphabétique ou un chiffre décimal Les trois ensembles de caractères de islower(), isupper() et isdigit()
isgraph Graphique Détermine si le caractère est représentable graphiquement Tout sauf l’ensemble de iscntrl() et l’espace (' ')
isprint Affichable Détermine si le caractère est « affichable » Tout sauf l’ensemble de iscntrl()

La suite <barre_droite> symbolise le caractère |.

Le tableau ci-dessus vous présente chacune de ces onze fonctions ainsi que les ensembles de caractères qu’elles décrivent. La colonne « par défaut » vous détaille leur comportement en cas d’utilisation de la locale C. Nous reviendrons sur les locales dans la troisième partie de ce cours ; pour l’heure, considérez que ces fonctions ne retournent un nombre positif que si un des caractères de leur ensemble « par défaut » leur est fourni en argument.

L’exemple ci-dessous utilise la fonction isdigit() pour déterminer si l’utilisateur a bien entré une suite de chiffres.

#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>

/* lire_ligne() */


int main(void)
{
	char suite[255];

	if (!lire_ligne(suite, sizeof suite))
	{
		printf("Erreur lors de la saisie.\n");
		return EXIT_FAILURE;
	}

	for (unsigned i = 0; suite[i] != '\0'; ++i)
		if (!isdigit(suite[i]))
		{
			printf("Veuillez entrer une suite de chiffres.\n");
			return EXIT_FAILURE;
		}

	printf("C'est bien une suite de chiffres.\n");
	return 0;
}
Résultat
122334
C'est bien une suite de chiffres.

5678a
Veuillez entrer une suite de chiffres.

Notez que cet en-tête fournit également deux fonctions : tolower() et toupper() retournant respectivement la version minuscule ou majuscule de la lettre entrée. Dans le cas où un caractère autre qu’une lettre est entrée (ou que celle-ci est déjà en minuscule ou en majuscule), la fonction retourne celui-ci.

Exercices

Palindrome

Un palindrome est un texte identique lorsqu’il est lu de gauche à droite et de droite à gauche. Ainsi, le mot radar est un palindrome, de même que les phrases engage le jeu que je le gagne et élu par cette crapule. Normalement, il n’est pas tenu compte des accents, trémas, cédilles ou des espaces. Toutefois, pour cet exercice, nous nous contenterons de vérifier si un mot donné est un palindrome.

bool palindrome(char *s)
{
    size_t len = 0;

    while (s[len] != '\0')
        ++len;

    for (size_t i = 0; i < len; ++i)
        if (s[i] != s[len - 1 - i])
            return false;

    return true;
}
Compter les parenthèses

Écrivez un programme qui lit une ligne et vérifie que chaque parenthèse ouvrante est bien refermée par la suite.

Entrez une ligne : printf("%zu\n", sizeof(int);
Il manque des parenthèses.
#include <stdio.h>
#include <stdlib.h>

/* lire_ligne() */


int main(void)
{
    char s[255];

    printf("Entrez une ligne : ");

    if (!lire_ligne(s, sizeof s))
    {
        printf("Erreur lors de la saisie.\n");
        return EXIT_FAILURE;
    }

    int n = 0;

    for (char *t = s; *t != '\0'; ++t)
    {
        if (*t == '(')
            ++n;
        else if (*t == ')')
            --n;

        if (n < 0)
            break;
    }

    if (n == 0)
        printf("Le compte est bon.\n");
    else
        printf("Il manque des parenthèses.\n");

    return 0;
}

Le chapitre suivant sera l’occasion de mettre en pratique ce que nous avons vu dans les chapitres précédents à l’aide d’un TP.

En résumé
  1. Une chaîne de caractères est un tableau de char terminé par un élément nul ;
  2. Une chaîne littérale ne peut pas être modifiée ;
  3. L’indicateur de conversion s permet d’afficher ou de récupérer une chaîne de caractères ;
  4. Les fonctions sscanf() et snprintf() permet de lire ou d’écrire depuis et dans une chaîne de caractères ;
  5. Les fonctions de l’en-tête <ctype.h> permettent de classifier les caractères.