Le caractère nul terminal, les fonctions de string.h, les pièges de strcpy et le dépassement de tampon, la manipulation caractère par caractère.
Le ver Morris de 1988 — celui qui ouvrait le chapitre 1 de l'UE de cybersécurité — s'est
propagé, entre autres, par un débordement de tampon dans le démon fingerd. Le code fautif
appelait gets(), une fonction qui lit une ligne dans un tampon sans jamais savoir quelle
taille il fait. Le ver envoyait une ligne plus longue que le tampon, écrasait l'adresse de
retour, et prenait la main.
gets a été retirée de la norme C en 2011, après vingt-trois ans de bons et loyaux services aux
attaquants. Mais le problème qu'elle illustre n'a pas disparu : il est inhérent à la façon
dont le C représente les chaînes, et c'est le sujet de ce chapitre.
Une chaîne est un tableau, plus une convention
Le C n'a pas de type chaîne. Une chaîne est un tableau de char terminé par un caractère
nul, noté '\0', de code 0.
char mot[] = "chat"; ┌─────┬─────┬─────┬─────┬──────┐│ 'c' │ 'h' │ 'a' │ 't' │ '\0' │└─────┴─────┴─────┴─────┴──────┘ 0 1 2 3 4 ← taille du tableau : 5, pas 4Toute la suite découle de ce schéma.
La longueur n'est pas stockée : elle se calcule, en parcourant jusqu'au marqueur. C'est la différence de fond avec les chaînes de Python ou de Java, qui portent leur longueur.
Il faut toujours une case de plus. Un tableau destiné à contenir caractères doit être
déclaré de taille . L'oubli du +1 est la faute la plus fréquente du chapitre, et elle
produit un débordement d'exactement une case — le genre d'erreur qui passe les tests.
Un marqueur perdu est une chaîne infinie. Si le '\0' est écrasé, toutes les fonctions de
chaîne continuent de lire au-delà du tableau, jusqu'à tomber par hasard sur un octet nul — ou
sur une page interdite.
Deux déclarations qu'il ne faut pas confondre :
char a[] = "chat"; /* tableau de 5 char, modifiable, sur la pile */char *b = "chat"; /* pointeur vers une chaîne littérale, EN LECTURE SEULE */Modifier b[0] est un comportement indéfini : les littéraux sont généralement placés dans une
zone protégée en écriture, et le programme meurt. On écrit donc const char *b pour que le
compilateur le rappelle.
Les fonctions de string.h
| Fonction | Rôle | Coût |
|---|---|---|
strlen(s) | longueur, sans le '\0' | |
strcpy(dst, src) | copie, marqueur compris | |
strcmp(a, b) | 0 si égales, signe de la différence sinon | |
strcat(dst, src) | concatène à la fin de dst | |
strchr(s, c) | première occurrence de c | |
strncpy, strncat | versions bornées par une taille |
La colonne des coûts mérite plus d'attention qu'on ne lui en accorde. strlen est linéaire,
puisqu'il faut parcourir jusqu'au marqueur. Écrire
for (int i = 0; i < strlen(s); i++) /* QUADRATIQUE */recalcule la longueur à chaque tour : la boucle devient . Sur une chaîne d'un million de caractères, c'est la différence entre instantané et plusieurs minutes. On calcule la longueur une fois, avant la boucle.
Deux rappels de syntaxe qui coûtent cher. strcmp rend 0 quand les chaînes sont égales,
donc if (strcmp(a, b)) teste la différence — le piège du chapitre 3. Et l'on ne compare
jamais deux chaînes avec ==, qui compare les adresses : a == b est vrai seulement si les
deux pointeurs désignent le même tableau.
Les pièges
Ils viennent tous du même endroit : aucune de ces fonctions ne connaît la taille de la destination.
strcpy(dst, src) copie jusqu'au marqueur de src, sans se demander si dst peut l'accueillir.
char petit[8];strcpy(petit, "une chaîne beaucoup trop longue"); /* débordement */Le débordement du chapitre 5, avec ses trois issues — dont l'écrasement de l'adresse de retour, qui est exactement le mécanisme du ver Morris.
Les versions bornées, strncpy et strncat, prennent une taille maximale. Elles sont plus
sûres, mais elles ont leurs propres pièges : strncpy ne pose pas le marqueur si la source
remplit exactement la destination, ce qui produit une chaîne non terminée. Il faut donc écrire
dst[n-1] = '\0' après coup. Et strncat prend en argument la place restante, pas la
taille du tampon — une source d'erreur classique.
D'où les recommandations d'usage : gets est interdite, on emploie fgets qui prend une
taille ; sprintf est à remplacer par snprintf pour la même raison ; et sur les systèmes qui
les offrent, strlcpy et strlcat font ce que strncpy aurait dû faire.
Pourquoi for (int i = 0; i < strlen(s); i++) est-il quadratique, et comment le corriger ?
Manipuler caractère par caractère
Un char est un entier — le chapitre 2 d'architecture l'avait posé — et l'arithmétique
directe sur les codes ASCII est parfaitement légitime en C.
char c = '7';int chiffre = c - '0'; /* 55 − 48 = 7 */char maj = c - 'a' + 'A'; /* bascule minuscule → majuscule */Le - '0' est l'idiome de conversion caractère vers chiffre, et il fonctionne parce que les
chiffres sont consécutifs dans la table. L'écart entre minuscule et majuscule vaut 32, soit
un seul bit — le chapitre 2 d'architecture l'avait noté.
Ces manipulations sont si fréquentes que ctype.h les encapsule : isdigit, isalpha,
isspace, toupper, tolower. Il vaut mieux les employer, pour une raison qui n'est pas
cosmétique : elles tiennent compte des paramètres régionaux, alors que les comparaisons
directes supposent l'ASCII et se cassent sur les caractères accentués — dont le chapitre 2
d'architecture rappelait qu'ils occupent plusieurs octets en UTF-8.
C'est d'ailleurs la limite à connaître de tout ce chapitre : strlen compte des octets, pas
des caractères. Sur « été », elle rend 5. Manipuler du texte réellement international demande
une bibliothèque dédiée, et le C seul n'y suffit pas.
char nom[5] ; strcpy(nom, « Alice ») — combien d'octets sont copiés, et pourquoi est-ce un débordement ?
À vous
L'exercice réimplémente strlen, strcpy et strcmp sur un tableau de caractères simulé, avec
un tampon de taille fixe et une variable voisine — le cadre du chapitre 5.
Trois choses à faire tomber. Le débordement du +1 oublié, en voyant l'octet nul écraser le
voisin. Le marqueur perdu, en écrasant le '\0' et en constatant que strlen part au-delà du
tampon. Et la boucle quadratique, en comptant les caractères examinés avec strlen dans la
condition puis en dehors — le rapport se voit dès une chaîne de cent caractères.
Réimplémentez strcpy, strlen et strcmp, puis faites tomber le « + 1 », le marqueur perdu et la boucle quadratique.
// La mémoire : un tampon de 8 octets, puis une variable voisine. function creerMemoire() { const m = new Array(16).fill(0); const TAMPON = 0, TAILLE = 8, VOISIN = 8; m[VOISIN] = 1234; return { tampon: TAMPON, taille: TAILLE, ecrire(i, code) { m[i] = code; }, lire(i) { return m[i]; }, voisin: () => m[VOISIN], vue() { return m.slice(0, 12).map((c, i) => { const s = c === 0 ? "\\0" : (i >= VOISIN ? String(c) : String.fromCharCode(c)); return (i === VOISIN ? "| " : "") + s; }).join(" "); }, }; } // Pose une chaîne littérale dans le tampon, SANS aucun contrôle — comme strcpy. function monStrcpy(m, depart, texte) { let i = 0; for (; i < texte.length; i++) m.ecrire(depart + i, texte.charCodeAt(i)); // ← à écrire : et le marqueur de fin ? combien d'octets au total ? return i; } // Longueur : on compte jusqu'au marqueur. AUCUNE borne. function monStrlen(m, depart, compteur = { n: 0 }) { let i = 0; while (m.lire(depart + i) !== 0) { compteur.n++; i++; if (i > 40) return -1; } return i; } function monStrcmp(m, a, b) { return 0; // ← à écrire : 0 si égales, sinon le signe de la différence } // ── À VOUS ──────────────────────────────────────────────────────────────── // 1. Faites copier le marqueur par monStrcpy, puis copiez « Alice » dans le // tampon de 8 : combien d'octets ? Et « personnage » ? // 2. Écrasez le marqueur et relancez monStrlen : que lit-elle ? // 3. Comptez les caractères examinés avec strlen DANS la condition d'une // boucle, puis calculée une seule fois avant. const m = creerMemoire(); monStrcpy(m, 0, "chat"); console.log("mémoire :", m.vue()); console.log("longueur :", monStrlen(m, 0), "| voisin :", m.voisin());
En travaux pratiques
Le caractère nul, et tout ce qui en dépend
Manipuler des chaînes C en comprenant que leur longueur n'est écrite nulle part, et réaliser le découpage de lignes dont le fil rouge a besoin.
- Le TP 5 : tableaux et débordements
- Le TP 2 d'Architecture, sur UTF-8
- 1. Compter les octets
Déclarez une chaîne littérale et affichez sa longueur par strlen et sa taille par sizeof. Expliquez la différence d'exactement un.
- 2. Perdre le zéro
Remplissez un tableau de dix caractères avec dix lettres, sans terminateur, puis affichez-le avec %s. Recommencez plusieurs fois et notez la variabilité.
- 3. Le débordement classique
Copiez une chaîne de vingt caractères dans un tampon de dix avec strcpy. Exécutez, puis recompilez avec le détecteur d'adresses.
- 4. La fausse correction
Remplacez par strncpy avec la taille du tampon. Affichez le résultat et cherchez le nouveau problème. Écrivez ensuite la version réellement correcte.
- 5. Comparer
Comparez deux chaînes de contenu identique avec l'opérateur d'égalité, puis avec strcmp. Expliquez pourquoi la première forme est parfois vraie.
- 6. Découper une ligne
Écrivez une fonction qui découpe une ligne en champs sur un séparateur, sans utiliser strtok. Testez avec des champs vides et des séparateurs consécutifs.
- 7. Au fil rouge
Branchez ce découpage dans journal : extraire l'adresse, la date, le code de réponse de chaque ligne. Testez sur une ligne tronquée et sur une ligne vide.
- 8. Les accents
Comptez les caractères d'une chaîne accentuée avec strlen, puis à la main. Écrivez une fonction qui compte les vrais caractères UTF-8.
- Vous expliquez pourquoi sizeof vaut strlen plus un sur un littéral
- Votre version de la copie est sûre ET termine toujours la chaîne
- Votre découpage gère un champ vide entre deux séparateurs
- Votre compteur UTF-8 donne 8 pour « éléphant »
Ce que la suite en fait
Le bloc IV explique enfin ce que ce chapitre a manipulé sans le nommer. char *b = "chat" est
un pointeur, strcpy(dst, src) reçoit deux adresses, et la conversion tableau-pointeur
du chapitre 5 est la raison pour laquelle une fonction de chaîne peut modifier la chaîne de son
appelant.
Le chapitre 8 lèvera aussi la contrainte de taille fixe : une chaîne dont la longueur n'est
connue qu'à l'exécution s'alloue dynamiquement — et il faudra alors ne pas oublier le +1 au
moment du malloc, où l'erreur est exactement la même.
À retenir
Vous avez parcouru les 8 sections.
Marquez-la terminée pour faire avancer votre parcours, ou revenez sur un point avant de passer à la suite.