Au cours de ce chapitre, nous traiterons de ce qui est appelé, en C, « Chaînes de caractères ».
Nous commencerons par étudier les caractères, qui sont l'élément de base des chaînes de caractères, puis nous verrons comment déclarer une chaîne de caractères. Ensuite, nous apprendrons à les manipuler grâce aux fonctions que fournit TIGCC, et, enfin, nous verrons comment nous pourrions les manipuler par pointeurs.
Tout d'abord, présentons, en quelques mots, ce qu'est une chaîne de caractères, pour ceux qui n'auraient pas l'habitude de ce terme technique.
Le terme de "chaîne de caractères" correspond à un type de données composé d'une suite ordonnée de caractères, et utilisé pour représenter du texte, un caractère étant une lettre, un chiffre, ou un symbole, visible ou non, tel, par exemple, les parenthèses, la ponctuation, ou l'espace.
Nous en avons déjà utilisé sans vraiment savoir qu'il s'agissait de ce dont nous allons parler au cours de ce chapitre.
Par exemple, "ceci est une chaîne de caractères".
Ce que l'on appelle "caractère" est le constituant de base de la chaîne de caractères ; il s'agit du terme utilisé en informatique pour désigner n'importe quel symbole pouvant apparaître dans du texte, que ce soit des lettres, des chiffres, ou n'importe quel autre symbole tel la ponctuation par exemple.
Nos TI codent les caractères sur un octet, en utilisant un codage proche de l'ASCII étendu.
Cela signifie que les 8 bits de chaque octet sont utilisés, ce qui permet 256 codes de caractères différents.
Vous trouverez, page 18 de l'annexe B de votre manuel, disponible en PDF ici si vous ne l'avez pas sous la main, la correspondance entre chaque caractère son code.
En C, pour représenter le code d'un caractère (on dit généralement code ASCII, même si ce n'est pas forcément de l'ASCII "pur"), on écrit le symbole correspondant à celui-ci en le plaçant entre guillemets simples.
Par exemple, le caractère 'a' a pour code, d'après la table de caractères dont nous avons parlé plus haut, 97 en décimal, soit 0x61 en hexadecimal.
Les caractères sont mémorisés, en C, sous forme de variables de type char.
Par exemple, pour déclarer la variable c, de type char, et contenant le caractère 'Y', nous utiliserons cette syntaxe :
En regardant la table de caractères, nous pouvons observer que les lettres de l'alphabet en majuscules sont toutes à la suite les unes des autres, et qu'il en va de même pour les lettres en minuscules.
Ceci va nous faciliter la vie : pour savoir si un caractère est, par exemple, une lettre majuscule, il nous suffira de tester si son code ASCII est compris entre celui de 'A' et celui de 'Z'.
A titre de curiosité, nous pouvons noter que les lettres minuscules ont des codes ASCII supérieurs à ceux de leurs équivalentes minuscules ; d'ailleurs, l'écart entre le code ASCII d'une lettre minuscule et celui de la lettre majuscule correspondante est, si l'on observe la table, toujours égal à 'a'-'A'.
A partir de là, il nous est facile d'écrire une fonction qui convertisse une lettre majuscule en la lettre minuscule correspondante : tout d'abord, nous testons si le caractère passé en paramètre est bien une lettre majuscule, et, si c'est le cas, nous renvoyons le code de la lettre minuscule correspondante. Si le caractère passé en paramètre n'était pas une lettre majuscule, nous renvoyons ce caractère.
Voici le code source correspondant à un programme implémentant et utilisant une telle fonction :
La fonction MAJ_to_min aurait aussi pu être écrite de manière plus concise, comme ceci :
Notons qu'il existe déjà une fonction dans les librairies de TIGCC qui serve à convertir des caractères de majuscule à minuscule ; il s'agit de la fonction tolower. Sa réciproque est la fonction toupper.
Les Caractères allant de 0x00 à 0x1F sont généralement appelés caractères non imprimables. Historiquement, ils correspondent à des codes de contrôle utilisés pour, par exemple, les machines à écrire ou les imprimantes en mode texte.
Parmi ceux-ci, on peut par exemple citer le caractère 0x0D, nommé "Carriage Return" (Retour chariot), qui correspond, sur certains systèmes, à un retour à la ligne.
La plupart de ces caractères, sur des machines tels nos ordinateurs, ne sont normalement pas visibles à l'écran, ce qui explique l'appellation de "caractères non imprimables".
Cela dit, la plupart d'entre-eux sont, sur TI, du fait qu'il n'y a pas de périphérique en mode texte à contrôler, représentés par un pictogramme visible à l'écran.
Les caractères de contrôle les plus fréquent peuvent être représentés de manière simple en C : il leur est fait correspondre, au niveau du compilateur, une lettre, précédée d'un antislash pour signifier qu'elle ne doit pas être interprétée telle qu'elle.
En particulier, nous pouvons citer ces quelques caractères :
Caractère | Signification |
---|---|
'\t' | Tabulation |
'\n' | Retour à la ligne |
'\\' | Antislash |
'\"' | Guillemets doubles |
'\'' | Guillemet simple |
Nous pouvons remarquer plus particulièrement les trois derniers caractères de notre tableau : ils doivent être précédés d'un antislash, afin que leur signification première soit annulée. Par exemple, le caractère '\'', sans l'antislash, correspondrait à la fin du caractère ; on aurait, autrement dit, ceci ''', soit deux guillemets n'encadrant aucun caractère, et un guillemet tout seul ne servant à rien... Cela entraînerait une erreur de compilation.
En somme, l'antislash permet de donner une signification différente au caractère qui le suit, ou, comme nous le verrons un peu plus bas, aux caractères qui le suivent. On dit généralement qu'il agit comme caractère d'échappement.
Le fait "d'échapper" un caractère revient donc à le faire précéder d'un antislash.
Il est des caractères qu'il est particulièrement difficile de saisir au clavier. Par exemple, si vous voulez utiliser le caractère '©', vous pouvez avoir quelques difficultés à le trouver.
C'est pour cela que le C permet de désigner des caractères directement par leur code, en octal en préfixant le code par un antislash, ou en hexadécimal en précédant le code par un antislash et un x.
Par exemple, le caractère '©' correspond, d'après la table des codes ASCII trouvée dans le manuel de votre calculatrice, à la valeur 169. 169 en décimal correspond à A9 en hexadécimal, et à 251 en octal.
Au lieu d'écrire '©' dans votre programme, il vous est possible d'utiliser '\251', ou '\xA9', selon la base numérique que vous préférez.
Notez que cela est indispensable pour les caractères non imprimables qui n'ont pas de code textuel. Par exemple, pour afficher un petit verrou, vous utiliserez ceci :
Maintenant que nous avons vu ce que sont les caractères, nous allons pouvoir commencer à étudier les chaînes de caractères, généralement nommées "string" en anglais.
La norme C défini une chaîne de caractères comme un tableau de chars, dont le dernier élément vaut 0. C'est ce marqueur de fin qui est caractéristique des chaînes de caractères, et toutes les fonctions permettant d'en manipuler supposent que ce 0 final est présent.
Autrement dit, lorsque vous avez une chaîne de caractères contenant 10 lettres, elle occupe en fait 11 emplacements mémoires ; pensez-y lorsque vous avez à déclarer de l'espace mémoire pour stocker une chaîne de caractères.
Une chaîne de caractères étant un liste de caractères, elle peut se déclarer comme telle. Par exemple :
Cela dit, il est extrêmement fastidieux d'utiliser ce type de syntaxe. C'est pourquoi le C fourni les guillemets doubles, qui indiquent au compilateur que ce qu'ils entourent est une chaîne de caractères.
On pourrait ré-écrire notre exemple précédent de cette manière :
Il est aussi possible de déclarer une chaîne de caractères en utilisant la syntaxe par pointeurs, comme ceci :
En effet, dans le premier cas (déclaration par tableau), str est un tableau de taille juste suffisamment grande pour pouvoir contenir la chaîne de caractères et son 0 de fin de chaîne. Quelque soit les manipulations effectuées sur la chaîne, str désignera toujours la même zone mémoire. De plus, à chaque fois que vous entrerez dans le bloc au sein de laquelle str est déclarée, la valeur de celle-ci sera réinitialisée.
En revanche, dans le cas de la déclaration par pointeur, le compilateur va créer une zone dans laquelle le texte sera stocké, et initialisera un pointeur pointant sur cette zone. A chaque fois qu'on rentrera dans le bloc contenant cette déclaration, ce sera le pointeur qui sera réinitialisé, et non le texte !
Pour illustrer mes propos, observons deux exemples, dans lesquels nous déclarons, au sein d'une boucle se répétant deux fois, une chaîne de caractères, l'affichons, et modifions son premier caractère.
Dans le premier cas, nous déclarons notre chaîne de caractères par tableau :
Nous obtenons, à l'exécution, les affichages suivants :
Dans le premier cas :
Cela dit, veuillez noter qu'il n'est généralement pas une bonne idée que de modifier une chaîne de caractères crée en la plaçant entre guillemets doubles.
En effet, il arrive que le compilateur choisisse, si deux chaînes sont identiques, de les fusionner en une seule. Dans ce cas vous modifiez l'une, cela modifiera aussi l'autre, puisqu'elles ne font finalement qu'une !
Par exemple, regardez cet exemple :
Pour en finir avec la déclaration de chaînes de caractères, j'en arrive à la forme qu'on utilise le plus fréquemment lorsque l'on souhaite en manipuler.
Il s'agit d'une déclaration de tableau, dont on précise la taille, mais sans l'initialiser. Encore une fois, il faut penser au 0 de fin de chaîne.
Par exemple, si on veut déclarer une chaîne de caractères qui puisse contenir 10 lettres, on déclarera un tableau de 11 chars, de la manière suivante :
Maintenant que nous savons comment déclarer des chaînes de caractères, nous allons apprendre, grâce à la suite de ce chapitre, à les manipuler.
Les chaînes de caractères étant très souvent employées, et leur format étant strictement défini par la norme C, de nombreuses fonctions permettant de les manipuler sont généralement inclues à aux bibliothèques des compilateurs.
Ainsi, TIGCC et AMS nous fournissent de nombreuses fonctions, dont un bon nombre sont des ROM_CALLs, dédiées au travail avec des chaînes de caractères. Notez que la plupart d'entre elles sont des fonctions ANSI, ce qui garantit que vous les retrouverez avec quasiment tous les compilateurs C existants.
Sous TIGCC, les fonctions de manipulation de chaînes de caractères sont regroupées dans le fichier d'en-tête <string.h>.
Au cours de cette partie, nous allons voir comment copier des chaînes de caractères vers d'autres chaînes de caractères, comment en concaténer, comment en formater, ou encore comment en obtenir la longueur...
Tout d'abord, la fonction strlen permet de déterminer la longueur, en nombre de caractères, de la chaîne de caractères qu'on lui passe en argument. Voici son prototype :
Pour copier une chaîne de caractères vers une autre, il convient d'utiliser la fonction strcpy, qui prend en paramètre un pointeur sur la zone mémoire de destination, et la chaîne d'origine :
A présent, passons à la concaténation de chaînes de caractères.
En informatique, on utilise le terme "concaténer", pour signifier que l'on veut mettre bout à bout deux chaînes de caractères. Par exemple, la concaténation de "Hello " et de "World !" donnera "Hello World !".
C'est la fonction strcat qui remplit ce rôle. Elle accole à la chaîne de caractères passée en second paramètre à la fin de la première.
Notez que les deux paramètres doivent être des chaînes de caractères valides ! En particulier, le code suivant, où la chaîne de caractères de destination n'a pas été initialisée, ne fonctionnera pas, et risque de causer un plantage :
Assez souvent, lorsque l'on travaille avec des chaînes de caractères, il nous arrive d'avoir à les comparer, pour savoir laquelle est la plus "grande", la plus "petite", ou si elles sont "égales".
Dans ce but, le C fournit la fonction strcmp, qui prend en paramètres deux chaînes de caractères, et retourne :
Pour la forme, voici un petit exemple ou nous testons les 3 cas : égalité, et plus "petit" ou plus "grand" :
Si vous êtes curieux, sachez que, en cas de différence entre les deux chaînes, la valeur retournée est égale à la différence entre les codes ASCII du premier caractère différent de chaque chaîne.
Par exemple, si on appelle strcmp sur les chaînes "aaax" et "aazb", la valeur retournée sera 'a'-'z', c'est-à-dire -25 : la chaîne "aaax" est plus petite que la chaîne "aazb".
Si vous avez besoin d'effectuer une comparaison qui ne soit pas sensible à la casse (c'est-à-dire, qui considère que les majuscules et minuscules sont égales), vous pouvez utiliser la fonction suivante :
La librairie C fourni aussi des fonctions permettant de copier, concaténer, ou comparer des chaînes, mais en se limitant à un certain nombre de caractères, que l'utilisateur peut définir, en comptant à partir du premier de la chaîne.
Ces fonctions sont nommées comme leurs équivalents travaillant sur des chaînes entières, mais en intercalant un 'n' entre "str" et ce que fait la fonction. Il faut aussi leur passer un troisième paramètre, qui correspond au nombre de caractères que la fonction devra traiter.
Voici leurs prototypes :
Pour la copie des n premiers caractères d'une chaîne vers un bloc mémoire :
Tout comme celles que nous avons vu précédemment, ces fonctions supposent que les chaînes qu'elles doivent lire sont valides (terminées par un 0), et que les zones mémoires où elles doivent écrire sont suffisamment grandes pour contenir ce que l'on essaye d'y placer.
De plus, comme on peut s'y attendre, si le nombre de caractères que vous spécifiez est plus grand que la taille effective de la chaîne, ces fonctions ignoreront le nombre de vous avez spécifié, et s'arrêteront à la fin de la chaîne.
Notez, et c'est important, que, pour strncpy, si le nombre de caractères dans la chaîne d'origine est supérieur ou égal à n, la chaîne de destination ne sera pas terminée par un '\0' ! Dans ce cas, pour que la chaîne de destination soit considèrée comme valide, ce qui est indispensable si vous voulez appeler une autre fonction dessus, il faudra que vous rajoutiez le '\0' de fin de chaîne vous-même !
Parfois, on dispose d'une chaîne de caractères, et on sait qu'elle contient une écriture de type numérique. On peut alors être amené à vouloir obtenir ce nombre sous forme d'une variable sur laquelle on puisse faire des calculs, telle, par exemple, un entier ou un flottant.
Il existe donc des fonctions qui permettent de convertir des chaînes de caractères en un type numérique. Les voici :
Pour convertir une chaîne de caractères en entier short :
Et, pour convertir une chaîne de caractères en flottant, on utilisera cette fonction :
Au cours de ce tutorial, nous avons souvent utilisé la fonction printf, qui permet d'afficher à l'écran du texte, en le formatant : insertion du contenu de variables au cours du texte, nombre de caractères fixes, ...
Le C fourni la fonction sprintf, qui fonctionne exactement de la même façon que printf, à la différence prêt que la chaîne de caractères résultante, au lieu d'être affichée à l'écran, est mémorisée dans une variable.
Voici le prototype de cette fonction :
Pour terminer ce chapitre, nous allons profiter du fait que certaines fonctions de manipulation de chaînes de caractères soient très simples pour faire un peu d'exercice concernant les tableaux et les pointeurs : nous allons voir comment il est possible de réécrire certaines des fonctions que nous avons ici appris à utiliser.
Pour certaines des fonctions que je choisirai de vous proposer de ré-implémenter, je donnerai une ou plusieurs solutions possibles. En particulier, du moins au début, je proposerai une solution qui pourrait correspondre à du code écrit par un débutant non habitué aux manipulations de pointeurs, et je fournirai une autre solution, correspondant plus à du code de programmeur C ayant de l'expérience dans ce langage. Naturellement, j'expliquerai les différences entre les différentes écritures, et pourquoi on peut passer de l'une à l'autre.
Je ne pense pas que cette partie soit indispensable à pour ce qui est de l'utilisation de base des chaînes de caractères, mais si, un jour, vous avez besoin d'écrire des fonctions travaillant sur du texte, ce que nous allons pourra probablement vous aider. De plus, travailler un peu avec les pointeurs ne peut que vous aider à mieux comprendre leurs principes, si ce n'est pas encore fait. Je vous conseille donc de ne pas négliger les quelques fonctions que nous allons à présenter développer.
J'insiste sur le fait que je ne réimplémente ici ces fonctions qu'à titre d'exercice, et que c'est chose qu'il est absolument ridicule de faire dans un programme utile ! Si des fonctions sont fournies dans les bibliothèques, ce n'est pas pour que vous les redéveloppiez dans vos programmes !
La première fonction que j'ai choisi de réimplémenter est la plus simple de celles que nous avons étudié au cours de ce chapitre ; il s'agit de strlen.
Pour rappel, strlen prend en paramètre une chaîne de caractères, et retourne le nombre de caractères qu'elle contient, sans compter le 0 de fin de chaîne.
Voici la première implémentation que je vous propose de découvrir :
C'est l'algorithme que nous continuerons d'utiliser pour strlen. Nous ne changerons que la façon d'écrire, afin d'arriver à du code légèrement plus concis.
Première petite modification : le caractère '\0' valant, comme le nom "0 de fin de chaîne" l'indique, zéro, et le 0 étant considéré comme faux dans les tests, on peut ré-écrire la condition de la boucle pour supprimer le test inutile :
A présent, dans la condition de la boucle, plutôt que d'accéder au caractère courant de la chaîne en indiçant un tableau, accédons-y par pointeur : à chaque cycle, on incrémente le pointeur correspondant à la chaîne, de sorte à ce qu'il pointe successivement sur le premier, puis le second, puis le troisième, et ainsi de suite.
Et on accède au caractère courant en déréférençant ce pointeur.
Il reste une idée que l'on pourrait mettre en application. En effet, en regardant le code de la fonction que nous venons d'écrire, nous réalisons encore deux incrémentations par boucle : celle du pointeur, et celle de la variable destinée à contenir la longueur de la portion de chaîne déjà parcourue. Pourquoi n'essayerions-nous pas de supprimer une de ces deux incrémentations ?
Après tout, cela est possible : supprimons l'incrémentation de la variable len... et la variable aussi, par la même occasion (si on ne l'incrémente plus, elle ne sert à rien).
Maintenant, il nous faut un moyen de déterminer de combien de cases le pointeur a été avancé... Une solution pour cela est de mémoriser, au début de la fonction, sur quelle case il pointe. Et, à la fin de la fonction, nous calculons la différence entre le pointeur str, qui correspond à l'adresse de fin de chaîne, et le pointeur qui a mémorisé l'adresse de début de celle-ci.
Et cette différence, d'après les règles d'arithmétique de pointeurs, correspond au nombre de cases qui séparent les deux pointeurs, c'est-à-dire, au nombre de caractères que la chaîne comporte.
Etant donné que nous avons choisi d'écrire l'instruction d'incrémentation du pointeur directement dans la condition de la boucle, le corps de celle-ci est désormais vide. Nous le remplaçons donc par un point-virgule, qui signifie "instruction vide", plus significatif qu'une paire d'accolades ne contenant rien.
Pour les autres fonctions que nous allons ré-implémenter, nous utiliserons à nouveau ce que nous avons employé ici. Nous ne ré-expliquerons pas tout en détail, puisque nous venons déjà de le faire.
A présent, nous allons nous pencher sur la fonction de copie de chaînes, strcpy.
Comme nous l'avons vu plus haut, cette fonction prend en paramètres un pointeur sur un bloc mémoire et une chaîne de caractères, et copie la chaîne de caractères vers le bloc mémoire, qui est supposé suffisamment grand pour pouvoir contenir la copie de la chaîne de caractères. Cette fonction retourne un pointeur sur la chaîne de destination.
Globalement, l'algorithme utilisé est simple : on parcourt la chaîne de caractères d'origine caractère par caractère, et, au fur et à mesure, on copie ces caractères vers le bloc mémoire de destination, sans oublier, bien entendu, le 0 de fin de chaîne.
Notre première implémentation est la suivante :
Pour notre seconde implémentation de strcpy, nous allons profiter d'une propriété du C, qui dit qu'une affectation a pour valeur celle qu'elle permet de stocker dans son opérande gauche.
Autrement dit,
Nous allons déplacer l'affectation depuis le corps de la boucle vers la condition. De la sorte, l'affectation sera fera maintenant avant de tester si on est en fin de chaîne, le test lui-même se faisant en tirant profit de la propriété que nous venons d'énoncer.
A présent, passons à l'utilisation de pointeurs, plutôt que de tableaux et d'indexes.
Les modifications sont proches de celles que nous avons effectué précédemment pour strlen ; je ne les détaillerai donc pas. L'algorithme reste exactement le même que pour notre implémentation précédente.
Enfin, exactement de la même manière que pour strlen, le test d'égalité entre le caractère courant (résultat de l'affectation) et '0\' est redondant, puisque '\0' vaut 0, et que 0 est équivalent à faux dans un contexte de condition.
Encore une fois, utiliser des pointeurs nous a permis d'utiliser de réduire le nombre de lignes de notre fonction, la rendant plus concise.
Cet exemple nous a aussi permis de réaliser l'importance que de petites "astuces" peut prendre. J'ajouterai que ce sont elles qui permettent souvent de faire la différence entre un programmeur C, et un programmeur C expérimenté : ce n'est généralement que lorsque l'on connaît bien le langage C que l'on est à même de se souvenir de certaines de ses particularités de ce genre, qui, même si elles sont loin d'être indispensables à l'écriture de programmes évolués, permettent parfois de se simplifier quelque peu la vie.
(Il aurait probablement été possible de se passer de ce genre d'astuce en utilisant une boucle do...while au lieu d'une boucle while... Mais je souhaitais attirer votre attention sur le fait que le C est un langage plein de subtilités, créées pour permettre des écritures concises, même si leur maîtrise n'est pas nécessairement indispensable.
A présent, je vais vous proposer une implémentation de strcat, qui permet de concaténer deux chaînes de caractères.
Etant donné que le fonctionnement de cette fonction est proche de celles que nous venons de voir, je ne donnerai qu'une seule écriture, et peu d'explications.
En effet, voici ce que strcat fait :
Voici l'implémentation de que je vous propose :
La dernière fonction que j'ai choisi de vous proposer pour cette partie est la fonction strcmp, qui permet, comme nous l'avons vu précédemment, de comparer deux chaînes de caractères.
Comme nous l'avons dit, si les deux chaînes sont égales, strcmp renvoie 0. Sinon, elle renvoie la différence entre les deux premiers caractères différents d'une chaîne à l'autre.
Pour les quatre implémentations que je vous propose, nous retournons toujours la même chose, que ce soit en écriture avec des tableaux indicés, ou des pointeurs ; je ne reviendrai donc pas dessus : il s'agit simplement de la soustraction évoquée plus haut.
Pour notre première implémentation, nous allons utiliser, pour parcourir nos deux chaînes, des tableaux indicés. Le parcours de chaînes en lui-même n'est pas une nouveauté, puisque nous en avons déjà effectué pour les fonctions précédentes ; je ne reviendrai donc pas dessus en détails.
Voici le code correspondant à notre première version de strcat:
Pour notre seconde implémentation, nous allons réfléchir un peu plus au sujet de la condition de notre boucle, en vue de l'optimiser. Tout d'abord, nous supprimons les tests avec '\0', qui sont redondants, puisque '\0' est faux.
Mais ensuite, demandons-nous pourquoi faire ces trois tests ? Est-ce qu'on ne pourrait pas n'en faire que deux ?
Certes, il faut vérifier qu'on n'est à la fin ni de la première chaîne, ni de la seconde... Mais pourquoi tester si str1[position] et str2[position] sont non-nuls, alors que nous testons aussi si str1[position] est égal à str2[position] ?
Nous avons plusieurs cas devant nous, en réfléchissant :
A présent, passons à un accès aux chaînes par pointeurs plutôt que par tableaux. La seule chose qui change est au niveau des écritures, qui nous permettent de nous débarrasser de la variable de position :
Et enfin, uniquement à titre de curiosité, voici comment, en utilisant une boucle for plutôt qu'une boucle while, nous pourrions écrire cette fonction en deux lignes :
Nous voici arrivé à la fin de ce chapitre du tutorial. Le suivant va nous permettre d'étudier comme regrouper plusieurs données au sein d'une même variable, en utilisant ce que le langage C appelle "structure".