Chaines de caractères et Co : j'ai besoin de pédagogie

Hello,

ayant appris les rudiments de programmation il y a longtemps avec le pascal, puis un peu de php, j'ai l'impression que j'ai pris des habitudes pas très compatibles avec le C++ notamment pour ce qui est de la manipulation des "textes" (terme général pour désigner les String, char[..] et autres *char[...] )

Bref, est-ce que qq'un aurait un tuto clair (en français de pref.) pour me permettre de déconstruire (je pense que c'est là le blocage dans ma petite tête) ce que je pense comprendre du bidule pour comprendre enfin comment on manipule des « textes » en C++ ?

Quand je lis la documentation, j'ai l'impression de voir une succession de mots mais sans comprendre l'idée derrière.

Merci, j'ai besoin de pédagogie...

Côté positif : je vois ce que vivent certains de mes élèves en cours :wink:

En gros une chaîne de caractère en C ou C++ est un tableau typé (char le plus souvent) terminé par un élément nul.

Tout le reste en découle que ce soit La classe String arduino ou string du C++

Bonjour @ProfesseurMephisto

Si on travaille avec peu de mémoire (avr…) il faut utiliser les c-string :

Sinon sur le web on trouve beaucoup d’explications sur la classe String qui est beaucoup plus simple à utiliser mais qui demande beaucoup plus de mémoire (ESP32…)

Bonne journée

Ok, ça je l'ai lu et relu. Mais ça imprime pas dans ma petite tête... il faut que j'arrive à me faire mes images mentales surtout quand ça commence à causer de pointeurs sur ces chaines.

Mais je vais m'accrocher, ça m'énerve trop de mal comprendre !

Merci je vais lire ça pour commencer...

Sinon pour les c-string, il suffit de taper dans le navigateur : "langage C les chaînes de caractères" et on tombe sur plein de liens :

https://www.locoduino.org/spip.php?article131

Une fois que vous aurez compris comment sont gérées ces chaînes de caractères en C (il vous faudra en passer par les tableaux de char et les pointeurs), vous verrez que la classe String est très simple à utiliser mais utilise plus de mémoire ...
Par contre si vous voulez comprendre comment ça fonctionne vraiment, faire l'impasse sur les c-string est une très mauvaise idée :wink:

Bonne journée.

PS : après ça c’est pour faire simple sinon vous avez le C++ moderne également mais je ne veux pas vous embrouiller.

EDIT 1 fois

1 Like

En gros voyez ça comme une série de cases mémoires consécutives contenant le code ASCII (pour faire simple) des caractères du texte suivies d’une case mémoire contenant un 0.
Les fonctions traditionnelles (calcul de la longueur, impression, concaténation, recherche,…) dépendent de ce zéro final (appelé caractère nul et noté '\0') pour savoir où terminer leur traitement

J’ai commis un petit tuto sur la mémoire - si ça peut aider ?

1 Like

Merci à tous, j'ai de la lecture pour revenir la prochaine fois avec des questions précises :wink:

Ca fait un moment que je n'ai pas fait de pascal, mais je ne vois pas une grande différence avec le pascal, qui possède aussi des adresses mémoires ?

@ProfesseurMephisto,
Malheureusement je ne suis pas pédagogue donc j'espère que vous allez me comprendre. J'ai pris un peu de temps pour vous faire appréhender les bases concernant les chaînes de caractère en C.

J'ai écris ce code afin d'essayer d'être le plus clair possible :

const char *texte = "bonjour";
const char txt[] = "salut";
const byte taille1 = strlen(texte) + 1; // +1 pour le zéro terminal
const byte taille2 = strlen(txt) + 1; // +1 pour le zéro terminal

void setup() {

  Serial.begin(115200);
  for (byte x = 0; x < taille1 - 1 ; x++) Serial.println(texte[x]);
  Serial.println(texte);
  for (byte x = 0; x < taille2 - 1 ; x++) Serial.println(txt[x]);
  Serial.println(txt);
}

void loop() {}

Si vous n'avait pas de carte de type avr :

En fait une chaîne de caractère (string en anglais) est une suite de caractères stockée dans un tableau de char et terminée par le caractère ‘\0’.

Le zéro nul marque la fin de la chaîne quelle que soit la taille du tableau. Toutes les fonctions de traitement de chaînes de caractères s’appuient sur ce zéro terminal pour trouver la fin de la chaîne.

Lorsque je déclare const char *texte = "bonjour"; texte est un pointeur sur une variable constante de type char. Le compilateur place la chaîne « bonjour » dans une zone mémoire contiguë de 8 octets, c’est à dire un octet pour chaque lettre (7) plus un pour le zéro terminal. « texte » c’est à dire le nom du pointeur contient l’adresse mémoire du premier caractère de la chaîne.

Et bien lorsque je déclare const char txt[] = "salut"; je fais exactement la même chose sauf qu’ici on va dire que txt[] est un tableau mais le compilateur place également la chaîne « salut » dans une zone mémoire contiguë de 6 octets c’est à dire un octet pour chaque lettre (5), plus un pour le zéro terminal.

Dans les deux cas le processus est le même.

Un tableau est en quelque sorte un mirage, il n’existe pas en tant que tel car il s’agit d’un pointeur masqué.

Donc je résume, le compilateur alloue une zone mémoire contiguë contenant les caractères et le zéro terminal, le nom du « tableau » n’est en fait qu’un pointeur qui pointe sur son premier élément (première lettre, premier octet). C'est une adresse mémoire tout simplement.

C’est la raison pour laquelle lorsque j’exécute les instructions Serial.println(texte); ou Serial.println(txt); c’est toute la chaîne que l’on retrouve dans le moniteur série car l’adresse de début de la chaîne (nom du pointeur ou du tableau) est connue ainsi que sa fin (zéro terminal).

PS : j'espère avoir été clair mais je n'en suis pas certain :cry:

Merci j'ai de la lecture pour ce soir...

Et merci pour la découverte de wokwi, j'aime bien ce genre de plateforme de test en ligne, c'est pratique

c'est un bon résumé - un peu simplifié

juste sur ce point:

C'est un gros raccourci. Il s'agit bien de deux types différents
Un pointeur ne connait pas la taille du contenu pointé
Un tableau connait la taille du contenu pointé

si vous faites cela

const char txt[] = "salut";
const byte taille2 = sizeof txt; 

taille2 contient bien le nombre d'octets alloués pour le tableau mais si vous faites

const char *texte = "bonjour";
const byte taille1 = sizeof texte;

vous avez dans taille1 la taille d'un pointeur sur la plateforme choisie (2 ou 4 octets)

c'est quand vous passez un tableau en paramètre à une fonction qui attend un pointeur que se produit le "decay" et que le type est changé en pointeur et donc au sein de la fonction on n'a plus accès à la taille du tableau.


Sinon, pour ceux qui veulent en savoir plus que de nécessaire (mais c'est important à comprendre)

Ce 'est plus tout à fait vrai depuis C++11 car on peut spécifier le type d'encoding que l'on souhaite avoir pour les caractères en rajoutant un préfixe à la chaîne (u8, u L, U)

void setup() {
  Serial.begin(115200);
  Serial.print("Nombre d'octets de base = "); Serial.println(sizeof "Hello");
  Serial.print("Nombre d'octets pour u8 = "); Serial.println(sizeof u8"Hello");
  Serial.print("Nombre d'octets pour u = "); Serial.println(sizeof u"Hello");
  Serial.print("Nombre d'octets pour L = "); Serial.println(sizeof L"Hello");
  Serial.print("Nombre d'octets pour U = "); Serial.println(sizeof U"Hello");
}

void loop() {}
  • sans préfixe, vous aurez 6 octets (les 5 lettres ASCII et le caractère nul sur un octet). le type est const char[6]

  • avec le préfixe u8 vous aurez aussi 6 octets. Avec u8, les lettres sont codée en UTF8. Comme ici c'est de l'ASCII on a un octet par caractère plus le nul donc ça fait 6. le type est const char[6] aussi mais changera en const char8_t[6]avec la version C++23

  • avec le préfixe u minuscule (sans le 8), vous aurez 12 octets. Avec ce u vous dites au compilateur que vous voulez forcer la représentation en UTF-16 (2 ou 4 octets par symbole) et dans ce cas le type du tableau est const char16_t[6] (ici 6 parce que on n'a que de l'ASCII dans le texte et ils tiennent tous sur 2 octets. l'UTF16 a certains caractères codés sur 4 octets)

  • avec le préfixe L vous aurez 12 octets. (les 5 lettres ASCII et le caractère nul). le stockage est de type wchar_t(wide char) qui est codé sur 2 octets, donc on a 12 octets en tout (et le 0 final est codé aussi sur 2 octets). Le type est const wchar_t[6]

  • avec le préfixe U majuscule, vous passez en représentation UTF-32, chaque symbole (on appelle cela des code points en fait) est codé dans un type char32_t donc sur 4 octet. Le type de notre chaîne est alors char32_t[6] ce qui fait 24 octets (le zero final est aussi sur 4 octets).

Notez que l'IDE a choisi l'utf8 pour l'encodage des chaînes par défaut, donc si vous mettez juste des trucs entre guillemets vous aurez en fait de l'UTF8, c'est pour cela qu'on peut mettre des caractères accentués, le symbole € et d'autres symboles dans une chaîne quand on programme.

Cependant certains caractères peuvent prendre jusqu'à 4 octets et les fonctions standard datant du C ne savent pas gérer cela... si vous demandez la longueur de la chaîne "1€" en faisant

Serial.println(strlen("1€"));

vous vous attendriez sans doute à avoir 2 puisqu'il y a deux caractères le '1' et le '€'... Et pourtant ça vous dira 4... parce que le symbole '€' est codé sur 3 octets en UTF8...

(en espérant ne pas avoir apporté de confusion :scream: :wink: )

Bonsoir @J-M-L,
Pas de confusion en ce qui me concerne. Merci d’avoir rectifié :

Merci également pour les précisions concernant les nouveautés depuis C++11. Je m’endormirais encore un peu moins bête ce soir :wink:
Je conseille également à @ProfesseurMephisto la lecture de votre tuto sur les pointeurs.
Bonne soirée

petit exercice pour les jours qui viennent:

puisque strlen() compte les octets et pas le "symboles", écrire une nouvelle fonction longueur() qui retournerait le bon nombre de "caractères" dans une c-string sur Arduino

  Serial.println(longueur("Coucou"));  // devra dire 6 comme on s'y attend
  Serial.println(longueur("1€"));      // devra dire 2 (et pas 4)
  Serial.println(longueur("àçèé§"));   // devra dire 5 et pas 10
  Serial.println(longueur("∆t←😇"));   // devra dire 4 et pas 11

Bonsoir @J-M-L,

Je viens de lire vôtre message.
A première vue, il faudrait traiter les caractères en fonction du nombre d'octets qu'ils représentent chacun en ce qui les concerne mais pour cela il faudrait connaître la taille de tous les caractères de plus d'un octet.
Mais c'est une première vue.
Cette demande concernant ce petit exercice (enfin petit pour vous) met bien en évidence toute la problématique de cette évolution par rapport au langage C :wink:

C'est relativement trivial, lire la description du codage UTF8 dans Wikipedia.

@ProfesseurMephisto ,

Après les chaînes de caractères du C, vous avez la classe String uniquement si votre carte dispose de suffisamment de mémoire :

Mais là c'est très simple à utiliser et je ne pense pas que vous ayez besoin de plus d'explications. Tout est dans le lien en ce qui concerne l'utilisation (et que l'utilisation) :wink:

Après on pourra peut-être aborder la manière dont le C++ moderne traite les chaines de caractère (quoique je me demande si c'est bien utile avec les microcontrôleurs).

Attention cette approche des chaînes de caractères est celle de quelqu'un qui est parti de zéro à partir de fin avril 2021, donc avec peu d’expérience et un âge certain. Elle reflète la chronologie d'apprentissage que j'ai suivi seul ou avec l'aide des membres du forum. Cette chronologie c'est moi qui l'ai choisie :

  • Langage C ;
  • Langage C++ ;
  • C++ moderne.

@J-M-L ou d'autres pourront nous confirmer si elle est convenable (si ils le souhaitent) :wink:

Voilà prenez donc ce que je vous explique avec précautions car je n'ai peut-être pas su percevoir toute la substance de ce que j'ai découvert.

PS: j'interviens ici sans prétention, je sais ce que je vaux (pas grand chose). Mon objectif c'est l'échange et apprendre toujours et encore.

Bonne soirée.

Merci @fdufnews,

J'irai voir ça demain matin.

Bonne soirée.

De memoire, au miillénaire dernier, les chaînes turbopascal étaient une "structure" formée de:
1 (short) entier non signé pour la longueur
et tous les caracteres (pas besoin de terminer par \0).
(ça limitait les chaînes de caractères à 256 caractères ASCII - 4 lignes environ, sans accents, sans possibilité d'écrire en -ordre alphabetique- arabe, cyrillique, hebreu).

Ce millénaire ci, pascal a toutes les variétés de chaines de caractères (compatibilité avec C++) Character and string types/fr - Free Pascal wiki -> c'est plus compliqué.

Oui, mais cela n'a pas trop d'influence sur la gestion mémoire entre C++ et Pascal.
Tu remarquera qu'une structure peut être assimilé à un objet sans fonction, donc on a bien un équivalent d'un String, avec une limitation sur la taille.
le caractère \0, n'est qu'un moyen de justement de ne pas avoir de limite de taille.
Je ne me rappels plus mais en Visual Basic, il n'y a pas le même genre de limite?

Enfin tout ça pour dire, que les pointeurs ou équivalents existe en pascal ?