Les chaînes de caractères
Ce que ce chapitre apporte5 points
- Déclarer une chaîne, l'afficher, et la parcourir caractère par caractère.
- Expliquer le rôle de l'octet nul final, et compter la place qu'il occupe.
- Calculer la longueur d'une chaîne, et dire ce que ce calcul coûte.
- Copier et comparer deux chaînes sans fonction toute faite.
- Reconnaître le dépassement qui guette toute écriture dans un tableau de caractères.
Le C n'a pas de type pour le texte. Il a des tableaux de char, et une convention : le texte s'arrête au premier octet nul. Rien d'autre. Ni longueur enregistrée, ni capacité, ni vérification.
Cette convention a un nom, les chaînes terminées par zéro, et elle est à l'origine d'une part considérable des failles de sécurité de ces quarante dernières années. Ce chapitre montre comment elle fonctionne, ce qu'elle coûte à chaque opération, et pourquoi la place du zéro final se compte avant d'écrire une seule ligne.
Une chaîne est un tableau, plus un zéro
La dernière ligne montre l'essentiel : la case numéro 7, celle qui suit le r, contient zéro.
Ce zéro n'est pas le caractère '0', dont le code vaut 48 : c'est l'octet nul, et c'est lui qui dit
où le texte s'arrête.
char mot[] = "bonjour" réserve donc huit cases pour sept lettres. Oublier cette case est la
faute fondatrice de ce chapitre.
Ce que coûte une longueur
strlen ne consulte aucun compteur : elle parcourt les octets jusqu'à trouver le zéro. Elle se
réécrit en quatre lignes.
La conséquence est celle d'un coût caché : demander la longueur d'une chaîne parcourt la chaîne
entière. Écrire for (int i = 0; i < strlen(t); i++) parcourt donc la chaîne à chaque tour, ce
qui transforme une boucle en travail proportionnel au carré de la longueur.
for (int i = 0; i < strlen(t); i++) est correct et lent. Sur une chaîne de mille caractères, il fait un million de lectures au lieu de mille.Deux écritures l'évitent : ranger la longueur dans une variable avant la boucle, ou tester directement
t[i] != 0, qui s'arrête au bon endroit sans jamais compter.
Parcourir et transformer
Le passage en majuscules est une soustraction : dans la table des codes, les majuscules précèdent les minuscules de 32 rangs exactement. Ce genre de calcul n'est possible que parce qu'un caractère est un nombre, et il ne vaut que pour les lettres non accentuées.
c >= 'a' && c <= 'z' et relancer : les espaces se transforment eux aussi, leur code 32 devenant zéro, et l'affichage s'arrête au premier mot. La chaîne n'a pas été tronquée, elle a reçu une fin prématurée.C'est la meilleure illustration de ce qu'est l'octet nul : non pas la fin du tableau, mais une marque posée dans les données, que n'importe quelle écriture maladroite peut déplacer.
Copier, et la place qu'il faut
Copier une chaîne, c'est copier ses caractères et son zéro.
La boucle s'arrête sur le zéro sans le copier, puisque sa condition est fausse à ce moment. La ligne qui suit le pose donc à la main, et l'oublier laisse une chaîne sans fin, dont la lecture continue dans ce qui suit en mémoire.
Cette fonction a un défaut qu'aucune relecture ne corrige : elle ne sait pas combien de place la
destination offre. C'est exactement le défaut de strcpy, la fonction standard qui fait la même
chose, et c'est pour cela que les projets sérieux l'interdisent au profit de variantes qui reçoivent
la taille.
Hors de ces pages, ce programme écrit quatre octets au-delà du tableau, sur la pile, là où se trouvent d'autres variables locales et l'adresse de retour de la fonction. Écraser cette adresse de retour permet, dans certaines conditions, de détourner l'exécution du programme : c'est le mécanisme historique du débordement de tampon, et il tient entièrement dans une chaîne trop longue pour la place prévue.
Comparer
Deux chaînes ne se comparent pas avec ==, qui comparerait leurs adresses.
La dernière ligne rend zéro alors que les deux chaînes portent le même texte : ce sont deux tableaux
distincts, à deux adresses distinctes. Comparer deux chaînes avec == est une faute silencieuse, qui
donne parfois le bon résultat pour de mauvaises raisons, et c'est ce qui la rend difficile à trouver.
Le dernier return mérite un regard : arrivé là, l'une au moins des deux chaînes est finie. Les deux
sont identiques si et seulement si l'autre l'est aussi, donc si les deux caractères courants sont
égaux, tous deux nuls.
À calculer soi-même
La place d'une chaîne se compte avant de l'écrire.
Compter les octets d'un texte
- 1.
Combien d'octets occupe la chaîne "bonjour" en mémoire ?
- 2.
Combien de cases faut-il déclarer pour accueillir un texte de 30 caractères ?
- 3.
Un tableau char t[10] peut accueillir un texte de combien de caractères, au plus ?
- 4.
Que vaut strlen("") pour une chaîne vide ?
- 5.
Combien d'octets occupe la chaîne vide en mémoire ?
- 6.
Une boucle qui appelle strlen à chaque tour sur une chaîne de 1000 caractères : combien de lectures d'octets au total, en ordre de grandeur ?
- 7.
Le code de 'a' vaut 97 et celui de 'A' vaut 65. Que faut-il retrancher pour passer des minuscules aux majuscules ?
Vérification
1.Qu'est-ce qui marque la fin d'une chaîne en C ?
2.Combien d'octets faut-il réserver pour le texte "reseau" ?
3.Que fait strlen sur une chaîne de 1000 caractères ?
4.Comment comparer le contenu de deux chaînes ?
5.Qu'écrase un texte trop long copié dans un tableau de caractères ?
Exercices type
Pourquoi le C n'a-t-il pas enregistré la longueur des chaînes, comme d'autres langages ?
Parce qu'une longueur enregistrée coûte de la place à chaque chaîne, y compris aux plus courtes, et que le langage a été conçu pour des machines dont la mémoire se comptait en kilooctets. La convention du zéro final ne coûte qu'un octet, quelle que soit la longueur.
Le calcul a changé de sens depuis : le coût d'un strlen répété, et le coût des failles produites par
l'absence de bornes, dépassent de très loin l'économie de quelques octets. Les langages venus ensuite
ont tous choisi l'autre solution, et le C garde la sienne par compatibilité.
Comment écrire une copie qui refuse de dépasser la place disponible ?
En lui passant la capacité de la destination, et en s'arrêtant une case avant :
void copie_sure(char *destination, int capacite, char *source) {
int i = 0;
while (source[i] != 0 && i < capacite - 1) {
destination[i] = source[i];
i = i + 1;
}
destination[i] = 0;
}
Deux détails comptent. La condition i < capacite - 1 réserve la case du zéro final. Et le zéro est
posé dans tous les cas, y compris quand la copie a été tronquée, sans quoi une troncature produirait
une chaîne sans fin.
Une copie tronquée reste une perte de données, mais elle ne détruit rien d'autre.
Un programme lit un nom au clavier dans un tableau de 32 caractères, et l'utilisateur en saisit 40. Que se passe-t-il ?
Les huit caractères en trop sont écrits après le tableau, dans ce qui suit sur la pile. Selon ce qui s'y trouve, le programme continue en donnant un résultat faux, s'arrête brutalement, ou fait quelque chose d'entièrement différent si l'adresse de retour a été atteinte.
C'est le scénario du débordement de tampon, et il n'a rien d'exceptionnel : il suffit qu'une lecture ne soit pas bornée. La parade consiste à lire au plus 31 caractères, en indiquant cette limite à la fonction de lecture, et à traiter le reste comme une entrée rejetée.
La méthode
- Compter n plus 1 dès qu'une chaîne est déclarée, sans exception.
- Poser le zéro final à la main après toute écriture caractère par caractère.
- Calculer la longueur une seule fois, avant la boucle, ou tester
t[i] != 0. - Comparer avec une fonction, jamais avec
==, qui compare des adresses. - Passer la capacité de la destination à toute fonction qui écrit dedans.
Synthèse
- Une chaîne est un tableau de
charterminé par un octet nul. Le langage n'a pas d'autre type pour le texte. - Un texte de n caractères occupe n plus 1 octets, et cette case supplémentaire n'est pas facultative.
strlenparcourt la chaîne : l'appeler dans la condition d'une boucle multiplie le travail par la longueur.- Copier une chaîne copie aussi son zéro, et l'oublier laisse une chaîne dont la lecture ne s'arrête plus.
==compare des adresses : deux tableaux différents portant le même texte ne sont pas égaux.
Et ensuite
Les tableaux rencontrés jusqu'ici ont tous une taille fixée à l'écriture du programme. La mémoire dynamique montre comment demander de la place pendant l'exécution, et surtout comment la rendre.
Mettre en pratique
L'octet nul et la place qu'il prend, le coût d'un strlen, et le débordement d'un tampon trop court.
Tous les exercices sur les chaînes de caractères