Chaînes de caractères en C

Se termine par \0

"chouette"

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 :

QCM

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

  • Que pensez-vous de char[10] s = "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 ça 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 ça 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.

  • 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!"`. 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

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

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

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);

Cette fonction 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. Attention, si ce n'est pas alloué suffisamment : .

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

strncpy fait la même chose mais dans la limite de len caractères.

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

memcpy etc.

void * memcpy ( void * destination, const void * 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");

Variante efficace

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.

  • 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.

La signature de memcpy est

void * memcpy( void * restrict dst, const void * restrict src, size_t nbBytesToCopy );
/*
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

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

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.

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.

Aller plus loin

  • article philosophique sur les chaînes qui finissent par \0 : https://queue.acm.org/detail.cfm?id=2010365