Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Chaînes de caractères en C

Se termine par \0

En C, une chaîne de caractères est une suite de char (octets) qui termine par un caractère/octet spécial : le caractère 0 de code 0.

"chouette"

chouette\0

Chaîne de caractères = pointeur sur char

On peut initialiser

char* s = "chouette";

En mémoire, s est un pointeur, une adresse mémoire, vers une zone de la mémoire avec 9 octets alloués où on a :

chouette\0



s

QCM

  • Que pensez-vous de char s[3] = "abc"; ? Il manque une case pour ‘\0’.

  • Que pensez-vous de char s[10] = "abc"; ? Pas de soucis, le compilateur complète avec des 0.

  • Que pensez-vous de char s[] = "abc"; ? Pas de soucis, le compilateur comprend qu’il faut allouer 4 octets, et d’ailleurs il n’alloue que 4 octets.

  • Comment accéder à la i-ème lettre ? a[i].

  • Que pensez-vous du programme suivant ?

        char s[] = "abca";
        s[2] = 'b';

Il modifie la 2e lettre est on a la chaîne “abba”.

  • Que pensez-vous du programme suivant ?
char* s = "abca";
s[2] = 'b';

Ca ne fonctionne pas car s est un pointeur vers une chaîne constante “abca” qui se trouve dans le segment de données en lecture seule. Il y a le segment de données normal qui est lecture/écriture, mais une zone de segment de données en lecture seule (rodata, read-only data).

  • Est-ce que ce programme est correct bien que le tableau ne finissent pas par 0 ?
char A[3];
A[0] = 'a';
A[1] = 'a';
A[2] = 'a';

Oui, c’est un tableau classique de caractères. Par contre, il ne faut pas le donner aux fonctions que l’on va voir plus tard, de <string.h> qui gèrent les chaînes de caractères ! En effet, A[3] (qui sort de la zone allouée) n’est peut-être pas 0 et donc les fonctions vont continuer à lire des parties de la mémoire indéterminée jusqu’à trouver un 0.

  • Est-ce que ce programme est correct ?
char* A = malloc(2);
A[0] = 'a';
A[1] = 'a';
A[2] = 'a';

Clairement non car A[2] n’est pas alloué.

  • Est-ce que ce programme est correct ?
char* A = malloc(3);
A[0] = 'a';
A[1] = 'a';
A[2] = 'a';

Le malloc peut ne pas réussir ce n’est pas bien. Mais à part ça, ça fonctionne. Par contre, A n’est pas une chaîne de caractères, c’est juste un pointeur vers un tableau avec 3 caractères.

char* A = malloc(4);
A[0] = 'a';
A[1] = 'a';
A[2] = 'a';
A[3] = `\0`;

Le malloc peut ne pas réussir ce n’est pas bien. Mais à part ça, ça fonctionne et A est une chaîne de caractères car elle termine par 0.

char* A = malloc(10);
A[0] = 'a';
A[1] = 'a';
A[2] = 'a';
A[3] = `\0`;

Le malloc peut ne pas réussir ce n’est pas bien. Mais à part ça, ça fonctionne et A est une chaîne de caractères car elle termine par 0. Il y a des cases non utilisées mais ce n’est pas grave.

Le C c’est vraiment du bas niveau !

Considérons deux chaînes de caractères

char s[10], t[10];
  • Peut-on écrire s = "abc"; ? Non, car les tableaux ne sont pas assignables.

  • Peut-on écrire t = s; ? Pareil, non. On ne peut pas faire d’affectation de tableaux (c’est parce que ce sont des tableaux, on peut par contre faire des assignations de pointeurs, et donc de char *).

  • Peut-on initialiser char u[10] = "abc"; ? Oui, à l’initialisation c’est bon. C’est équivalent à char u[10] = {'a', 'b', 'c', '\0'};

  • Peut-on tester s == t ? Oui, on peut toujours tester l’égalité d’adresse mémoire de tableaux ou de pointeurs :).

  • Que teste s == t ? Que l’adresse mémoire s est égale à l’adresse mémoire t.

  • Que pensez-vous du programme suivant ?

char* donnerSalutation() {
    char s[50] = "Bonjour";
    return s;
}

Le code compile mais c’est très grave. On déclare un tableau de 50 cases mémoire sur la pile. On y écrit "Bonjour" puis on renvoie l’adresse (s) alors que la pile va être écrasée !

  • Que pensez-vous du programme suivant ?
char* donnerSalutation() {
    char* s = "Bonjour";
    return s;
}

On déclare un pointeur qui pointe vers la zone de données en lecture seule où il y a écrit "Bonjour". On ne peut pas modifier la chaîne de caractères. Il ne faut pas appeler free sinon erreur (on ne peut pas libérer une zone de données en lecture seule !).

  • Que pensez-vous du programme suivant ?
char* donnerSalutation() {
    char* s = malloc(10*sizeof *s);
    s[0] = 'h';
    s[1] = 'i';
    s[2] = '!';
    s[3] = '\0';
    return s;
}

On déclare un pointeur qui pointe vers une zone du tas et on y écrit la chaîne "hi!" (attention cependant à la constante volante 10). On peut modifier la chaîne de caractères. Il ne faut pas oublier de faire free quelque part.

Longueur d’une chaîne de caractères

La bibliothèque string.h propose la fonction suivante :

size_t strlen(const char *s);

Elle prend une chaîne de caractères et renvoie sa longueur, i.e. le nombre d’octets de caractères. Attention, il faut que la chaîne finisse par 0, sinon gare à vous ! Et ce caractère 0 n’est pas comptabilisé.

Ecrire soi-même une fonction int len(const char* s) qui renvoie la longueur de la chaîne s.

size_t len(const char *str) {
    const char *ptr = str;
    while (*ptr != '\0') ptr++;
    return ptr - str;
}

La fonction utilise l’arithmétique des pointeurs.

Parcours d’une chaîne de caractères

Ecrire une fonction qui teste d’appartenance d’un caractère à une chaîne.

Ecrire une fonction qui transforme une chaîne de caractères en sa version en minuscule.

Considérons :

char* s = "Bonjour tout le monde";
  • Que vaut strlen(s) ? 21 car il y a 21 caractères dans "Bonjour tout le monde".

  • Que vaut sizeof(s) ? 8 car il faut 8 octets pour stocker une adresse mémoire.

Considérons :

char[] s = "Bonjour tout le monde";
  • Que vaut strlen(s) ? 21 car il y a 21 caractères dans "Bonjour tout le monde".

  • Que vaut sizeof(s) ? 22 car il faut 22 octets pour mettre la chaîne de caractères "Bonjour tout le monde" de 21 caractères, puis 0.

Comparaison de chaînes

La bibliothèque string.h propose une fonction de comparaison de deux chaînes de caractères :

int strcmp(const char *s1, const char *s2)
strcmp("abricot", "chat")strcmp("chat", "chat")strcmp("chat", "abricot")
< 00> 0

Copie de chaînes

strdup

char * strdup( const char * source);

La fonction strup duplique une chaîne de caractère. Elle renvoie une copie de la chaîne de caractères source :

  • Elle alloue une nouvelle zone mémoire avec un malloc (caché dans l’appel strdup) de la même taille que source
  • En cas de succès, elle copie source vers cette nouvelle zone mémoire et renvoie un pointeur vers cette zone
  • En cas d’échec (hé oui, le malloc peut échouer), elle renvoie un pointeur nul.

⚠ Attention à libérer la mémoire avec free de la copie créée.

strcpy

strcpy copie src dans dst que l’on a déjà alloué préalablement.

char * strcpy(char * restrict dest, const char * restrict source);

Attention, la fonction strcpy est dangereuse. Si dst n’est pas alloué suffisamment, on écrit en dehors de la zone allouée et c’est le mal : .

Le mot clé restrict signifie que le pointeur est l’unique façon d’accéder aux données. C’est un contrat qu’on passe avec le compilateur qui fait des optimisations car il sait que c’est que les données derrière le pointeur ne pas modifié par quelqu’un d’autre. Dit autrement, on évite l’aliasing :

char* src = strup("mon buffer qui est aussi un peu la source");
char* dst = src + 5;
strcpy(dst, src); // on ne respecte pas le contrat car aliasing

strncpy

Pour ne pas dépasser la zone allouée, une première alternative, dangereuse aussi, est strncpy. Elle fait la même chose mais dans la limite de len caractères. Mais attention, si len est trop petit, strncpy n’écrit pas le caractère 0 à la fin des données tronquées.

size_t strncpy(char * restrict dst, const char restrict * src, size_t len);

Fonctions sécurisées de l’annexe K du C11

L’annexe K du C11 a ajouté des fonctions sécurisées comme :

errno_t strcpy_s( char * restrict destination, rsize_t destinationSize, const char * restrict source );

Si jamais destinationSize est trop petit pour accueillir la chaîne de caractères source alors la fonction renvoie un code d’erreur. Et voici la fonction qui tronque si jamais ça dépasse :

errno_t strncpy_s( char* restrict dest, rsize_t destsz,
                   const char* restrict src, rsize_t count );
size_t strlcpy(char * restrict dst, const char * restrict src, size_t dsze);

memcpy etc.

void * memcpy ( void * restrict destination, const void * restrict source, size_t nbOctetsACopier );

La même chose mais pour de la mémoire et pas de vérification de caractère nul ou autre. C’est fait pour copier n’importe quoi.

Concaténer deux chaînes

strcat de <string.h>

char * strcat(char* restrict dst, const char * restrict src);

Précondition :

  • il faut que dst soit suffisamment alloué pour contenir le contenu de la concaténation. Sinon .

Effet :

  • place src à la fin de dst
char[1000] s = "";
strcat(s, "Bienvenue ");
strcat(s, "à ");
strcat(s, "l'ENS de Lyon");

strcat renvoie :

  • dst ! C’est pour pouvoir faire des cascades d’appel

On peut donc chaîner les appels à strcat comme cela :

char[1000] s = "";
strcat(strcat(strcat(s, "Bienvenue "), "à "), "l'ENS de Lyon");

Malheureusement, la complexité de strcat est en O(|s1| + |s2|). cf https://www.joelonsoftware.com/2001/12/11/back-to-basics/ On souffre du problème de Shlemiel le peintre.

Exercice

Ecrire une version mystrcat qui fait la même chose que strcat mais renvoie un pointeur sur la fin de la chaîne.

char* mystrcat( char* dst, char* src )
{
    while (*dst) dst++;
    while (*dst++ = *src++);
    return --dst;
}

Variante à la Python

En utilisant memcpy, écrire une variante string_concat qui renvoie une nouvelle chaîne de caractères qui est la concaténation de s1 et s2.

/*
return a new string that is the concatenation of s1 and s2
(in Python, s1 + s2)
**/
char * string_concat(char* s1, char* s2) {
    int l1 = strlen(s1);
    int l2 = strlen(s2);

    char* result = malloc(l1 + l2 + 1);

    if(result == NULL)
        return NULL;

    memcpy(result, s1, l1);
    memcpy(result + l1, s2, l2+1);
    
    return result;
}

Variante avec réallocation

Ecrire une fonction qui prend une chaîne s1 et qui lui concatène s2. Si pas assez de mémoire, on réalloue s1. La fonction renvoie la nouvelle chaîne. Dans tous les cas, la chaîne s1 initiale est perdue.

char* strcatrealloc(char* s1, char* s2) {
    int l1 = strlen(s1);
    int l2 = strlen(s2);
    char* result = realloc(s1, l1 + l2 + 1 * sizeof(*result));
    if(result == NULL)
        return NULL;

    memcpy(result + l1, s2, l2 + 1);
    return result;
}

Tableaux de chaînes de caractères

- Dessiner un schéma mémoire de
#define NB_MAXCHAR 12

char planets[][NB_MAXCHAR] = {"Mercury", "Venus", "Earth",
                    "Mars", "Jupiter", "Saturn",
                    "Uranus", "Neptune", "Pluto"};
  • Même chose pour
char *planets[] = {"Mercury", "Venus", "Earth",
                    "Mars", "Jupiter", "Saturn",
                    "Uranus", "Neptune", "Pluto"};
  • Même chose pour
#define NB_PLANETS 9

char ** planets = malloc(NB_PLANETS*sizeof(*planets));
planets[0] = "Mercury";
planets[1] = "Venus";
planets[2] = "Earth";
planets[3] = "Mars";
planets[4] = "Jupiter";
planets[5] = "Saturn";
planets[6] = "Uranus";
planets[7] = "Neptune";
planets[8] = "Pluto";

Arguments en ligne de commande

En fait, un programme peut prendre des arguments dans la ligne de commande. Par exemple :

./programme -l arf.txt

Récupération des arguments du programme

On peut les récupérer dans la fonction main du programme qui a deux paramètres argc et argv. La fonction main est appelée avec comme argument argc qui vaut le nombre de paramètres + le programme lui-même. Puis argv est un tableau de chaîne de caractères, chaque chaîne est un argument.

int main(int argc, char *argv[]) {
    ...
}

Exemple 1

Par exemple si notre programme est programme et l’utilisateur lance la commande

./programme -l arf.txt

alors

  • argc vaut 3
  • argv[0] contient “programme”
  • argv[1] contient “-l”
  • argv[2] contient “arf.txt”
  • argv[3] est le pointeur NULL.

Exemple 2

Autre exemple, si l’utilisateur lance la commande

./programme -l "miaou dit le chat" 

alors

  • argc vaut 3
  • argv[0] contient “programme”
  • argv[1] contient “-l”
  • argv[2] contient “miaou dit le chat”
  • argv[3] est le pointeur NULL.

Exercices

Modifier les projets précédents pour qu’ils prennent en entrée les arguments en ligne de commande

Ecrire un programme qui prend en entrée un texte et qui renvoie sa version justifiée.