C++ — CHAÎNES DE CARACTÈRES, std::string ET std::string_view

Salut ce forum apprends les bases du c++

Moderator: Rick

Post Reply
Hydraxx
Site Admin
Posts: 114
Joined: Mon Jan 12, 2026 4:04 pm
Location: France
Contact:

C++ — CHAÎNES DE CARACTÈRES, std::string ET std::string_view

Post by Hydraxx »

C++ — CHAÎNES DE CARACTÈRES, std::string ET std::string_view

Objectif du cours
  • la représentation des données en mémoire ;
  • les pointeurs ;
  • les tableaux ;
  • les tailles et capacités de buffers ;
  • les allocations dynamiques ;
  • la durée de vie des objets ;
  • l'ownership ;
  • les copies et les réallocations ;
  • l'interopérabilité avec les API C, POSIX et Win32 ;
  • le parsing de données ;
  • les performances.
Dans ce cours, on va progressivement passer de la représentation la plus brute :

Code: Select all

const char*
à une représentation propriétaire moderne :

Code: Select all

std::string
puis à une vue non propriétaire extrêmement intéressante pour le développement système :

Code: Select all

std::string_view
1. COMMENT UN ORDINATEUR REPRÉSENTE UNE CHAÎNE ?

Un ordinateur ne possède pas un type matériel appelé "chaîne de caractères".

La mémoire contient essentiellement des octets.

Lorsque nous écrivons :

Code: Select all

"Hello"
le programme doit disposer quelque part d'une suite de valeurs représentant les caractères.

Dans le cas classique d'une chaîne C composée de caractères

Code: Select all

char
, on peut imaginer :

Code: Select all

Adresse      Valeur
0x1000       'H'
0x1001       'e'
0x1002       'l'
0x1003       'l'
0x1004       'o'
0x1005       '\0'
Chaque caractère occupe ici un élément du tableau.

Le dernier caractère,

Code: Select all

'\0'
, joue un rôle très particulier : il indique la fin de la chaîne.

Ainsi, la chaîne visible :

Code: Select all

Hello
occupe en réalité six éléments :

Code: Select all

'H' 'e' 'l' 'l' 'o' '\0'
Ce mécanisme vient du langage C et reste extrêmement important aujourd'hui.

2. LES CHAÎNES C ET LE TERMINATEUR NUL

Une chaîne C est généralement une séquence de caractères terminée par un caractère nul.

Exemple :

Code: Select all

char message[] = "Hello";
Le compilateur crée automatiquement :

Code: Select all

'H' 'e' 'l' 'l' 'o' '\0'
Le caractère nul possède la valeur numérique zéro.

Il peut être écrit :

Code: Select all

'\0'
Il ne faut pas le confondre avec le caractère :

Code: Select all

'0'
Le caractère

Code: Select all

'0'
correspond au chiffre zéro visible.

Le caractère

Code: Select all

'\0'
correspond à la valeur zéro et sert notamment de marqueur de fin.

C'est fondamental, car beaucoup de fonctions C ne reçoivent PAS explicitement la longueur de la chaîne.

Par exemple :

Code: Select all

strlen(message);
La fonction doit déterminer où se termine la chaîne.

Elle commence au premier caractère, puis avance jusqu'à rencontrer :

Code: Select all

'\0'
Conceptuellement :

Code: Select all

H -> e -> l -> l -> o -> \0
                         ^
                         arrêt
Sans terminateur nul valide, la fonction peut continuer à lire la mémoire au-delà du tableau.

3. sizeof() ET strlen() NE MESURENT PAS LA MÊME CHOSE

Considérons :

Code: Select all

char text[] = "Hello";
Le tableau contient six éléments.

Donc :

Code: Select all

sizeof(text)
donne normalement :

Code: Select all

6
Mais :

Code: Select all

strlen(text)
donne :

Code: Select all

5
Pourquoi ?

Parce que

Code: Select all

strlen()
mesure le nombre de caractères AVANT le terminateur nul.

Le

Code: Select all

'\0'
ne fait donc pas partie de la longueur logique du texte.

Cette distinction est importante :

Code: Select all

sizeof(text)   -> taille du tableau en mémoire
strlen(text)   -> longueur de la chaîne C
Mais attention : cela change lorsque l'on utilise un pointeur.

Exemple :

Code: Select all

const char* text = "Hello";
Ici :

Code: Select all

sizeof(text)
donne la taille du POINTEUR.

Sur une architecture 64 bits typique :

Code: Select all

sizeof(text) == 8
Cela ne signifie absolument pas que la chaîne mesure huit caractères.

Le pointeur contient simplement une adresse de 64 bits.

C'est une erreur classique en C/C++.

4. TABLEAU DE char CONTRE POINTEUR const char*

Ces deux déclarations ressemblent beaucoup :

Code: Select all

char a[] = "Hello";
const char* b = "Hello";
Mais elles ne décrivent pas exactement la même situation.

Avec :

Code: Select all

char a[] = "Hello";
le programme crée un tableau contenant une copie modifiable des caractères.

On peut faire :

Code: Select all

a[0] = 'Y';
Le tableau devient :

Code: Select all

"Yello"
Avec :

Code: Select all

const char* b = "Hello";
b contient l'adresse du premier caractère d'un littéral de chaîne.

La présence de

Code: Select all

const
signifie que le programme ne doit pas modifier le texte à travers ce pointeur.

C'est la bonne manière de manipuler un littéral via un pointeur.

5. UNE CHAÎNE C EST TRÈS PROCHE D'UN POINTEUR

Avec :

Code: Select all

const char* text = "Kernel";
on peut imaginer :

Code: Select all

text
  |
  v
+---+---+---+---+---+---+---+
| K | e | r | n | e | l |\0 |
+---+---+---+---+---+---+---+
L'expression :

Code: Select all

text[0]
est conceptuellement équivalente à :

Code: Select all

*(text + 0)
Et :

Code: Select all

text[2]
correspond à :

Code: Select all

*(text + 2)
On retrouve donc directement l'arithmétique des pointeurs.

Derrière le texte, on manipule donc réellement des adresses et des zones mémoire.

6. strlen() : CE QUI SE PASSE CONCEPTUELLEMENT

Une version simplifiée pourrait ressembler à :

Code: Select all

std::size_t my_strlen(const char* str)
{
    std::size_t length = 0;

    while (str[length] != '\0')
    {
        ++length;
    }

    return length;
}
7. LE DANGER D'UNE CHAÎNE NON TERMINÉE

Considérons :

Code: Select all

char buffer[5] = {
    'H',
    'e',
    'l',
    'l',
    'o'
};
Il n'y a aucune place pour :

Code: Select all

'\0'
Faire :

Code: Select all

strlen(buffer);
provoque un comportement indéfini.

Pourquoi ?

Parce que

Code: Select all

strlen()
continue à lire après le tableau.

Elle peut inspecter :

Code: Select all

buffer[5]
buffer[6]
buffer[7]
...
jusqu'à tomber par hasard sur un octet nul.

Le programme lit donc de la mémoire qui ne fait pas partie du tableau.

Dans le meilleur cas :
le résultat est faux.

Dans un cas plus grave :
le programme plante.

Dans un contexte de sécurité :
cela peut participer à une vulnérabilité.

8. CAPACITÉ DU BUFFER ET LONGUEUR DU TEXTE

Prenons :

Code: Select all

char buffer[64] = "abc";
Le tableau possède une capacité de :

Code: Select all

64 octets
Mais la chaîne possède une longueur logique de :

Code: Select all

3 caractères
Il faut donc distinguer :
  • la taille physique du stockage ;
  • la quantité de texte actuellement utilisée.
C'est une distinction omniprésente en programmation système.

On retrouve exactement la même idée dans :
  • les buffers réseau ;
  • les fichiers ;
  • les structures système ;
  • les vecteurs ;
  • les paquets ;
  • les buffers d'I/O.
Exemple :

Code: Select all

char buffer[4096];

ssize_t bytesRead =
    read(fd, buffer, sizeof(buffer));
Ici :

Code: Select all

sizeof(buffer)
est la capacité maximale du buffer.

Mais :

Code: Select all

bytesRead
indique combien d'octets valides ont réellement été reçus.

Ce sont deux informations différentes.

9. strcpy() ET LE RISQUE DE BUFFER OVERFLOW

Une fonction historique du C est :

Code: Select all

strcpy()
Exemple :

Code: Select all

char destination[32];

strcpy(destination, "Hello");
Cela fonctionne parce que le buffer est suffisamment grand.

Mais :

Code: Select all

char destination[8];

strcpy(
    destination,
    "Hello this string is much too long"
);
est extrêmement dangereux.

Code: Select all

strcpy()
ne connaît pas automatiquement la capacité du tableau destination.

Elle continue à copier les caractères.

Elle peut alors écrire au-delà des huit octets réservés.

Conceptuellement :

Code: Select all

destination

+---+---+---+---+---+---+---+---+
| H | e | l | l | o |   | t | h |
+---+---+---+---+---+---+---+---+
                                  |
                                  v
                     écriture hors du tableau
Une écriture hors limites peut :
  • écraser une autre variable ;
  • corrompre la pile ;
  • modifier des métadonnées mémoire ;
  • provoquer un crash ;
  • ouvrir une faille de sécurité.
Les buffer overflows historiques viennent très souvent de cette mauvaise gestion des tailles.

10. strcat() ET LA CONCATÉNATION C

Exemple :

Code: Select all

char result[32] = "Hello ";

strcat(result, "World");
La chaîne devient :

Code: Select all

"Hello World"
Mais le programmeur doit s'assurer lui-même que :

Code: Select all

longueur actuelle
+
longueur ajoutée
+
1 pour '\0'
ne dépasse pas la capacité du buffer.

11. strcmp() : COMPARER LE CONTENU ET NON LES ADRESSES

Erreur classique :

Code: Select all

const char* a = "hello";
const char* b = "hello";

if (a == b)
{
}
Cette expression compare les pointeurs.

Autrement dit, elle demande conceptuellement :

Code: Select all

adresse(a) == adresse(b) ?
Elle ne demande pas :

Code: Select all

contenu(a) == contenu(b) ?
Pour comparer les caractères d'une chaîne C, on utilise historiquement :

Code: Select all

strcmp(a, b)
Exemple :

Code: Select all

if (strcmp(a, b) == 0)
{
    // les textes sont identiques
}
Il faut donc distinguer l'adresse du contenu.

12. LES LITTÉRAUX DE CHAÎNES

Une expression comme :

Code: Select all

"Windows"
est un littéral de chaîne.

Le compilateur place les caractères dans une zone adaptée du programme.

On peut l'utiliser directement :

Code: Select all

printf("Hello");
ou obtenir un pointeur :

Code: Select all

const char* name = "Windows";
Il ne faut pas considérer cette zone comme un buffer modifiable.

Le code suivant est donc une mauvaise idée :

Code: Select all

char* text =
    const_cast<char*>("Hello");

text[0] = 'X';
Le comportement peut être indéfini.

13. CARACTÈRES D'ÉCHAPPEMENT

Certaines valeurs sont écrites avec une syntaxe spéciale :

Code: Select all

\n
\t
\r
\\
\"
\'
\0
Quelques significations :
Un chemin Windows classique peut donc être écrit :

Code: Select all

"C:\\Windows\\System32"
Chaque backslash visible nécessite ici un échappement.

14. RAW STRING LITERALS

C++ permet d'éviter certains échappements grâce aux raw strings.

Syntaxe :

Code: Select all

R"(texte)"
Exemple :

Code: Select all

const char* path =
    R"(C:\Windows\System32\drivers)";
C'est beaucoup plus lisible que :

Code: Select all

"C:\\Windows\\System32\\drivers"
Les raw strings sont particulièrement pratiques pour :
  • les chemins ;
  • les expressions régulières ;
  • le JSON ;
  • le XML ;
  • le SQL ;
  • les scripts ;
  • les textes multilignes.
Exemple multilignes :

Code: Select all

const char* config =
R"(
server=localhost
port=8080
debug=true
)";
Aucun

Code: Select all

\n
explicite n'est nécessaire pour chaque ligne.

15. DÉLIMITEURS PERSONNALISÉS DES RAW STRINGS

Le texte suivant :

Code: Select all

)"
ferme normalement une raw string.

Si le contenu lui-même doit contenir cette séquence, on peut utiliser un délimiteur personnel.

Exemple :

Code: Select all

const char* json =
R"JSON(
{
    "message": "hello",
    "path": "C:\Windows"
}
)JSON";
Le mot

Code: Select all

JSON
fait ici partie du délimiteur.

16. LES PRÉFIXES DE LITTÉRAUX ET LE DÉVELOPPEMENT WINDOWS

Tu peux rencontrer :

Code: Select all

"hello"
L"hello"
u8"hello"
u"hello"
U"hello"
Pour le développement Windows,

Code: Select all

L"..."
est particulièrement important.

Exemple :

Code: Select all

CreateFileW(
    L"C:\\Temp\\data.bin",
    GENERIC_READ,
    FILE_SHARE_READ,
    nullptr,
    OPEN_EXISTING,
    FILE_ATTRIBUTE_NORMAL,
    nullptr
);
La fonction

Code: Select all

CreateFileW
attend une chaîne wide.

Les types Win32 associés sont souvent :

Code: Select all

LPCWSTR
LPWSTR
WCHAR*
Il faut donc savoir reconnaître immédiatement ces représentations.

17. POURQUOI std::string EXISTE ?

Les chaînes C donnent énormément de contrôle.

Mais elles imposent aussi beaucoup de responsabilités.

Avec un buffer C, il faut souvent gérer :
  • la taille ;
  • la capacité ;
  • le terminateur nul ;
  • les allocations ;
  • les copies ;
  • les concaténations ;
  • les réallocations ;
  • la destruction.
C++ propose donc :

Code: Select all

std::string
qui encapsule une chaîne dynamique.

Exemple :

Code: Select all

#include <string>

std::string text{"Hello"};
L'objet connaît sa taille et gère son stockage.

18. std::string EST UN OBJET PROPRIÉTAIRE

Un

Code: Select all

std::string
possède logiquement les caractères qu'il représente.

Exemple :

Code: Select all

std::string text{"Kernel"};
On peut voir cela conceptuellement comme :

Code: Select all

text
+---------------------------+
| size     = 6              |
| capacity = ...            |
| stockage -> "Kernel"      |
+---------------------------+
Lorsque l'objet est détruit, sa mémoire est nettoyée automatiquement.

C'est le principe RAII.

19. CRÉATION ET AFFECTATION DE std::string

Exemples :

Code: Select all

std::string a;
std::string b{"Windows"};
std::string c{b};
Affectation :

Code: Select all

a = "Linux";
Puis :

Code: Select all

a = "Windows";
La bibliothèque gère automatiquement les changements nécessaires.

20. size() ET length()

Avec :

Code: Select all

std::string text{"Kernel"};
on peut utiliser :

Code: Select all

text.size()
ou :

Code: Select all

text.length()
Les deux représentent ici le nombre de caractères logiques.

Contrairement aux chaînes C,

Code: Select all

std::string
conserve cette information.

Il n'est pas nécessaire de rescanner toute la chaîne avec

Code: Select all

strlen()
.

21. size(), capacity() ET reserve()

Un

Code: Select all

std::string
peut posséder une capacité supérieure à sa taille actuelle.

Exemple conceptuel :

Code: Select all

size     = 10
capacity = 31
Cela signifie :
  • 10 caractères sont actuellement utilisés ;
  • le stockage peut éventuellement accueillir davantage de caractères avant une nouvelle allocation.
On peut réserver à l'avance :

Code: Select all

std::string output;

output.reserve(4096);
Attention :

Code: Select all

reserve(4096)
ne signifie PAS :

"la chaîne contient 4096 caractères".

Cela signifie plutôt :

"prépare suffisamment de capacité pour éviter certaines réallocations".

Sa taille logique peut toujours être zéro.

22. resize() N'EST PAS reserve()

Code: Select all

std::string text;

text.resize(100);
Ici, la taille logique devient 100.

La distinction est donc essentielle :

Code: Select all

reserve()
    -> modifie la capacité disponible

resize()
    -> modifie le nombre logique d'éléments
23. CONCATÉNATION AVEC std::string

Exemple :

Code: Select all

std::string first{"Hello"};
std::string second{" World"};

std::string result =
    first + second;
Résultat :

Code: Select all

"Hello World"
On peut aussi modifier directement :

Code: Select all

first += second;
Ou :

Code: Select all

first.append(second);
24. ACCÉDER AUX CARACTÈRES

Comme un tableau :

Code: Select all

std::string text{"Hello"};

char c = text[1];
c vaut :

Code: Select all

'e'
Il existe aussi :

Code: Select all

text.at(1)
La différence importante est que

Code: Select all

at()
effectue un contrôle de borne et peut lancer une exception si l'index est invalide.

L'opérateur :

Code: Select all

[]
ne fournit pas la même protection.

25. data() : ACCÈS AU BUFFER INTERNE

Une fonction essentielle pour ton profil est :

Code: Select all

text.data()
Elle retourne un pointeur vers le stockage contigu des caractères.

Exemple :

Code: Select all

std::string payload{"PING"};

send(
    socket,
    payload.data(),
    payload.size(),
    0
);
Même si

Code: Select all

std::string
est une abstraction plus haut niveau que

Code: Select all

char*
, elle te permet toujours d'obtenir directement :
  • l'adresse du buffer ;
  • la taille logique du buffer.
C'est précisément ce que demandent énormément d'API système.

26. c_str() : INTEROPÉRABILITÉ AVEC LES API C

Certaines fonctions demandent explicitement une chaîne C terminée par zéro.

Exemple :

Code: Select all

std::string filename{"data.bin"};

std::FILE* file =
    std::fopen(
        filename.c_str(),
        "rb"
    );

Code: Select all

c_str()
donne un pointeur adapté à ce type d'interface.

On retrouve ici la frontière entre deux mondes :

Code: Select all

C++ moderne
std::string
        |
        | c_str()
        v
API C
const char*
Savoir traverser proprement cette frontière est important en développement système.

27. STOCKAGE CONTIGU DE std::string

Les caractères d'un

Code: Select all

std::string
sont stockés de manière contiguë.

Conceptuellement :

Code: Select all

text.data()
    |
    v
+---+---+---+---+---+
| H | e | l | l | o |
+---+---+---+---+---+
Cela permet de traiter la chaîne comme un buffer lorsque l'API le permet.

Mais il faut toujours respecter :
  • la durée de vie ;
  • la taille ;
  • les règles de modification ;
  • les éventuelles invalidations.
28. INVALIDATION DES POINTEURS LORS D'UNE RÉALLOCATION

Considérons :

Code: Select all

std::string text{"Hello"};

const char* ptr =
    text.data();
Puis :

Code: Select all

text +=
    " une très longue quantité de texte";
La chaîne peut ne plus avoir assez de capacité.

Elle peut alors :
  • allouer un nouveau bloc ;
  • copier ou déplacer les caractères ;
  • libérer l'ancien bloc.
Le pointeur

Code: Select all

ptr
peut alors devenir invalide.

Conceptuellement :

Avant :

Code: Select all

ptr
 |
 v
[ ancien buffer ]
Après réallocation :

Code: Select all

text
 |
 v
[ nouveau buffer ]

ptr
 |
 v
[ ancien buffer libéré ]
Utiliser

Code: Select all

ptr
après cela peut provoquer un use-after-free ou un comportement indéfini.

29. SMALL STRING OPTIMIZATION

De nombreuses implémentations de

Code: Select all

std::string
utilisent une optimisation appelée :

Code: Select all

Small String Optimization
ou :

Code: Select all

SSO
L'idée est simple.

Une petite chaîne peut être stockée directement à l'intérieur de l'objet

Code: Select all

std::string
, sans allocation sur le tas.

Conceptuellement :

Code: Select all

std::string
+------------------------------+
| metadata | petits caractères |
+------------------------------+
Pour une chaîne plus grande, l'objet utilise généralement du stockage dynamique.

L'avantage est d'éviter une allocation coûteuse pour de petits textes très courants.

Attention :

la taille exacte à partir de laquelle l'implémentation alloue n'est pas garantie par le langage.

Il ne faut jamais écrire du code dépendant d'une valeur précise.

30. COMPARAISON AVEC std::string

Avec les chaînes C :

Code: Select all

a == b
compare généralement les pointeurs.

Avec

Code: Select all

std::string
:

Code: Select all

std::string a{"Linux"};
std::string b{"Linux"};

if (a == b)
{
}
compare bien les caractères.

On peut aussi utiliser :

Code: Select all

<
>
<=
>=
selon le type de comparaison souhaité.

31. find() : RECHERCHER UNE SOUS-CHAÎNE

Exemple :

Code: Select all

std::string text{
    "Windows Kernel Programming"
};

auto pos =
    text.find("Kernel");
Si le texte est trouvé,

Code: Select all

pos
contient sa position.

Sinon, la fonction retourne :

Code: Select all

std::string::npos
On écrit donc souvent :

Code: Select all

if (
    text.find("Kernel")
    !=
    std::string::npos
)
{
    // trouvé
}
32. std::string::npos

Code: Select all

npos
est une valeur spéciale indiquant qu'aucune position valide n'a été trouvée.

Il est préférable de l'utiliser explicitement plutôt que d'imaginer un index arbitraire comme

Code: Select all

-1
.

Exemple :

Code: Select all

auto pos =
    text.find("missing");

if (pos == std::string::npos)
{
    std::println("introuvable");
}
33. substr() : EXTRAIRE UNE COPIE

Exemple :

Code: Select all

std::string text{
    "Windows Kernel"
};

std::string part =
    text.substr(8, 6);
Résultat :

Code: Select all

"Kernel"
Point important :

Code: Select all

std::string::substr()
retourne un nouvel objet chaîne.

Cela signifie qu'une nouvelle représentation du texte est créée.

Il peut donc y avoir une copie et éventuellement une allocation.

Plus tard,

Code: Select all

std::string_view
permettra de représenter une sous-partie sans copier les caractères.

34. replace(), insert() ET erase()

Remplacement :

Code: Select all

std::string text{"Hello Linux"};

text.replace(
    6,
    5,
    "Windows"
);
Insertion :

Code: Select all

std::string text{"Helo"};

text.insert(
    3,
    "l"
);
Suppression :

Code: Select all

std::string text{
    "Hello World"
};

text.erase(
    5,
    6
);
Ces opérations sont pratiques, mais il ne faut pas oublier leur coût.

Insérer ou supprimer au milieu d'un tableau contigu peut nécessiter de déplacer des caractères.

35. starts_with(), ends_with() ET contains()

C++ moderne propose des opérations très lisibles.

Exemple :

Code: Select all

std::string filename{
    "driver.sys"
};

if (
    filename.ends_with(".sys")
)
{
    std::println(
        "driver potentiel"
    );
}
Préfixe :

Code: Select all

if (
    path.starts_with(
        "C:\\Windows\\"
    )
)
{
}
Recherche simple :

Code: Select all

if (
    text.contains("Kernel")
)
{
}
Cela permet d'exprimer directement l'intention du code.

36. clear() ET empty()

Vider une chaîne :

Code: Select all

text.clear();
Tester :

Code: Select all

if (text.empty())
{
}
C'est souvent plus lisible que :

Code: Select all

if (text.size() == 0)
{
}
Même si les deux expriment la même condition logique.

37. ITERER SUR UNE std::string

Une chaîne peut être parcourue comme un conteneur.

Exemple :

Code: Select all

std::string text{
    "ABC"
};

for (char c : text)
{
    std::println(
        "{}",
        c
    );
}
On obtient successivement :

Code: Select all

A
B
C
38. LES COÛTS CACHÉS RESTENT RÉELS

Par exemple :

Code: Select all

text += other;
peut entraîner :
  • une vérification de capacité ;
  • une allocation ;
  • une copie ou un déplacement ;
  • une libération de l'ancien stockage.
Un :

Code: Select all

substr()
peut créer une nouvelle chaîne.

Un :

Code: Select all

insert()
peut déplacer une grande partie du buffer.

Un développeur système doit donc savoir utiliser les abstractions tout en comprenant leur coût.

39. CONVERTIR UN NOMBRE EN CHAÎNE

Une valeur entière :

Code: Select all

4096
et le texte :

Code: Select all

"4096"
sont deux choses complètement différentes.

Le premier est une représentation numérique permettant directement des calculs.

Le second est une suite de caractères.

On peut convertir :

Code: Select all

int value{4096};

std::string text =
    std::to_string(value);
Résultat :

Code: Select all

"4096"
Cela suffit pour beaucoup d'applications courantes.

40. CONVERTIR UNE CHAÎNE EN NOMBRE

C++ fournit notamment :

Code: Select all

std::stoi()
std::stol()
std::stoll()
std::stoul()
std::stoull()
std::stof()
std::stod()
std::stold()
Exemple :

Code: Select all

std::string text{"4096"};

int value =
    std::stoi(text);
Maintenant :

Code: Select all

value == 4096
On peut réellement faire :

Code: Select all

value + 100
41. POURQUOI LE PARSING EST IMPORTANT EN SYSTÈME ?

Beaucoup d'informations arrivent sous forme textuelle :
  • fichiers de configuration ;
  • arguments de ligne de commande ;
  • /proc sous Linux ;
  • protocoles ;
  • logs ;
  • entrées utilisateur ;
  • formats textuels ;
  • outils de diagnostic.
Exemple :

Code: Select all

"PID=1337"
Pour utiliser réellement le PID, il faut transformer :

Code: Select all

"1337"
en entier.

42. std::to_chars : CONVERSION DIRECTEMENT DANS UN BUFFER

Pour un code plus bas niveau, C++ propose :

Code: Select all

std::to_chars
Exemple :

Code: Select all

#include <charconv>
#include <array>

std::array<char, 32> buffer{};

int value{12345};

auto result =
    std::to_chars(
        buffer.data(),
        buffer.data()
            + buffer.size(),
        value
    );
La fonction écrit directement dans la plage mémoire fournie.

Elle ne nécessite pas automatiquement la création d'un

Code: Select all

std::string
.

C'est particulièrement intéressant pour :
  • les buffers préalloués ;
  • le code performant ;
  • les protocoles ;
  • les systèmes de logs ;
  • les parsers.
43. std::from_chars : PARSER UNE PLAGE MÉMOIRE

L'opération inverse :

Code: Select all

std::string_view input{
    "4096"
};

int value{};

auto result =
    std::from_chars(
        input.data(),
        input.data()
            + input.size(),
        value
    );
La fonction reçoit :

Code: Select all

adresse début
adresse fin
destination
C'est très proche du modèle utilisé en bas niveau.

Aucun terminateur nul n'est nécessaire puisque la borne de fin est fournie explicitement.

44. POINTER + LENGTH : UN MODÈLE FONDAMENTAL

Beaucoup d'interfaces système manipulent les buffers sous cette forme :

Code: Select all

void process(
    const void* buffer,
    std::size_t length
);
Ou :

Code: Select all

send(
    socket,
    data,
    size,
    flags
);
Le modèle est toujours :

Code: Select all

adresse + longueur
C'est exactement ce qui rend

Code: Select all

std::string_view
particulièrement intéressant.

45. std::string_view : UNE VUE NON PROPRIÉTAIRE

Code: Select all

std::string_view
représente une vue sur des caractères existants.

Il ne possède pas leur mémoire.

Conceptuellement :

Code: Select all

struct StringViewConcept
{
    const char* data;
    std::size_t size;
};
La vraie classe standard offre évidemment davantage de fonctionnalités, mais ce modèle mental est excellent.

Une

Code: Select all

string_view
est donc essentiellement :

Code: Select all

pointeur + longueur
46. CRÉER UNE string_view

Depuis un littéral :

Code: Select all

std::string_view view{
    "Kernel"
};
Depuis une

Code: Select all

std::string
:

Code: Select all

std::string text{
    "Kernel"
};

std::string_view view{
    text
};
Depuis une zone mémoire :

Code: Select all

const char buffer[] = {
    'A',
    'B',
    'C',
    'D'
};

std::string_view view{
    buffer,
    4
};
Remarque importante :

le dernier buffer ne possède même pas nécessairement de

Code: Select all

'\0'
.

La longueur est explicitement connue.

47. std::string_view NE COPIE PAS LES CARACTÈRES

Considérons :

Code: Select all

std::string text{
    "Windows Kernel"
};

std::string_view view{
    text
};
On peut imaginer :

Code: Select all

text
  |
  v
+---+---+---+---+---+---+---+---+---+---+
| W | i | n | d | o | w | s |   | K |...|
+---+---+---+---+---+---+---+---+---+---+
  ^
  |
view.data()
La view ne possède pas une deuxième copie de la chaîne.

Elle pointe vers le stockage existant.

Cela évite :
  • une allocation ;
  • une copie ;
  • une destruction de chaîne temporaire.
48. substr() AVEC string_view

Exemple :

Code: Select all

std::string_view text{
    "Windows Kernel"
};

std::string_view part =
    text.substr(
        8,
        6
    );

Code: Select all

part
représente :

Code: Select all

"Kernel"
Mais les caractères n'ont pas été copiés.

La nouvelle vue référence seulement une sous-zone.

Conceptuellement :

Code: Select all

texte original
W i n d o w s   K e r n e l
                ^
                |
            part.data()

part.size() == 6
49. remove_prefix() ET remove_suffix()

Exemple :

Code: Select all

std::string_view command{
    "CMD:START"
};

command.remove_prefix(4);
La view représente maintenant :

Code: Select all

"START"
Aucun caractère n'a été déplacé.

La view a seulement modifié :
  • son pointeur de début ;
  • sa longueur.
Même principe :

Code: Select all

std::string_view file{
    "driver.sys"
};

file.remove_suffix(4);
La vue représente maintenant :

Code: Select all

"driver"
50. POURQUOI string_view EST IDÉAL POUR LES PARSERS

Imagine une commande :

Code: Select all

SET PRIORITY 12
On peut recevoir la commande dans :

Code: Select all

std::string input;
Créer une vue :

Code: Select all

std::string_view command{
    input
};
Tester le préfixe :

Code: Select all

constexpr
std::string_view prefix{
    "SET PRIORITY "
};

if (
    command.starts_with(prefix)
)
{
    command.remove_prefix(
        prefix.size()
    );
}
La view voit maintenant :

Code: Select all

"12"
On peut ensuite parser directement :

Code: Select all

int priority{};

auto result =
    std::from_chars(
        command.data(),
        command.data()
            + command.size(),
        priority
    );
Aucune sous-chaîne temporaire n'a été créée.

51. LE GRAND DANGER DE string_view : LA DURÉE DE VIE

Puisque la view ne possède pas la mémoire, elle dépend entièrement de la source.

Exemple incorrect :

Code: Select all

std::string_view getName()
{
    std::string name{
        "Windows"
    };

    return name;
}
À l'intérieur :

Code: Select all

name
 |
 v
[ caractères ]
 ^
 |
view
Puis la fonction se termine.

Code: Select all

name
est détruite.

Son stockage n'est plus valide.

Mais la view retournée contient toujours une adresse.

Elle devient :

Code: Select all

dangling
Autrement dit :
elle pointe vers une zone dont la durée de vie est terminée.

C'est le même problème qu'un pointeur ou une référence vers une variable locale détruite.

52. string_view ET OBJETS TEMPORAIRES

Autre erreur :

Code: Select all

std::string_view view =
    std::string{
        "Temporary"
    };
Le

Code: Select all

std::string
temporaire est détruit rapidement.

La view peut alors continuer à référencer l'ancien stockage.

Règle fondamentale :

Code: Select all

la source doit vivre
au moins aussi longtemps
que la string_view

53. string_view N'EST PAS FORCÉMENT TERMINÉE PAR ZÉRO

Considérons :

Code: Select all

std::string text{
    "ABCDEF"
};

std::string_view view{
    text.data() + 1,
    3
};
La view représente :

Code: Select all

"BCD"
Mais en mémoire :

Code: Select all

B C D E F \0
    ^
fin logique de view
Après

Code: Select all

D
, le caractère suivant est

Code: Select all

E
, pas forcément

Code: Select all

\0
.

Faire :

Code: Select all

printf(
    "%s",
    view.data()
);
peut donc afficher :

Code: Select all

BCDEF
au lieu de :

Code: Select all

BCD
car

Code: Select all

printf("%s")
ne connaît pas

Code: Select all

view.size()
.

Il continue jusqu'au terminateur nul.

C'est une distinction extrêmement importante.

54. API POINTEUR + TAILLE ET string_view

Si l'API accepte explicitement une taille, la situation est différente.

Exemple conceptuel :

Code: Select all

send(
    socket,
    view.data(),
    view.size(),
    0
);
L'API sait exactement combien d'octets utiliser.

Elle n'a pas besoin d'un

Code: Select all

\0
.


55. QUAND UTILISER std::string ?

Utilise une chaîne propriétaire lorsque l'objet doit posséder les caractères.

Exemple :

Code: Select all

class Process
{
private:
    std::string m_name;
};
Le nom doit rester valide aussi longtemps que l'objet

Code: Select all

Process
.

Il est logique que la classe en possède une copie.

56. QUAND UTILISER std::string_view ?

Pour une fonction qui veut seulement lire temporairement du texte :

Code: Select all

void parse(
    std::string_view input
);
C'est souvent très adapté.

La fonction n'a pas besoin :
  • d'allouer ;
  • de copier ;
  • de posséder les caractères.
Elle regarde simplement la zone pendant l'appel.

57. QUAND UTILISER const char* ?

Tu en as encore besoin notamment lorsque :
  • une API C le demande ;
  • une bibliothèque historique le demande ;
  • tu manipules directement un buffer C ;
  • tu travailles avec des interfaces ABI.
58. COMPARAISON DES TROIS MODÈLES

Code: Select all

const char*
Représente principalement une adresse.

La taille n'est pas intrinsèquement incluse.

Avec une chaîne C, la fin est souvent trouvée grâce à

Code: Select all

\0
.

Code: Select all

std::string
Possède et gère les caractères.

Connaît sa longueur.

Peut allouer et réallouer.

Code: Select all

std::string_view
Ne possède rien.

Conserve :
  • une vue vers les caractères ;
  • une longueur.
Très légère, mais dépendante de la durée de vie de sa source.

59. FORMATAGE MODERNE DES CHAÎNES

Le C utilise historiquement

Code: Select all

printf()
et le C++ a longtemps utilisé les flux

Code: Select all

std::cout
. Le C++ moderne propose un formatage plus lisible et typé avec

Code: Select all

std::format
,

Code: Select all

std::print
et

Code: Select all

std::println
.

Créer une chaîne formatée :

Code: Select all

std::string text =
    std::format(
        "pid={} size={}",
        pid,
        size
    );
Afficher directement :

Code: Select all

std::println(
    "PID={} SIZE={}",
    pid,
    size
);
Pour le développement système, les formats hexadécimal et binaire sont particulièrement utiles.

Hexadécimal :

Code: Select all

std::println(
    "value=0x{:X}",
    value
);
Quelques formes à connaître :
Pour afficher une valeur sur une largeur fixe avec des zéros en tête :

Code: Select all

std::println(
    "EAX=0x{:08X}",
    eax
);
Si

Code: Select all

eax == 0x2A
, on peut obtenir :

Code: Select all

EAX=0x0000002A
Pour une adresse 64 bits :

Code: Select all

std::uint64_t address{
    0x7FF612341000
};

std::println(
    "Address=0x{:016X}",
    address
);
Ce genre de sortie est très pratique dans un debugger, un outil de reverse engineering ou des logs système.

Le formatage permet aussi de contrôler l'alignement :

Code: Select all

{:<10}   // gauche
{:>10}   // droite
{:^10}   // centre
et d'utiliser un caractère de remplissage :

Code: Select all

std::println(
    "{:*^12}",
    "CPU"
);
Pour les nombres flottants :

Code: Select all

double value{
    3.14159265
};

std::println(
    "{:.2f}",
    value
);
affiche :

Code: Select all

3.14
On rencontre aussi

Code: Select all

{:e}
pour la notation scientifique et

Code: Select all

{:g}
pour une représentation générale.

Les versions modernes de la bibliothèque peuvent également formater certains conteneurs/ranges, ce qui est utile pour les tests et le debug.

Enfin, le système de formatage peut être étendu à tes propres types grâce aux formatters personnalisés. Un type représentant des registres peut, par exemple, être affiché automatiquement sous une forme du genre :

Code: Select all

RIP=0x00007FF612341000
RSP=0x0000008AF23FF000
RAX=0x000000000000002A
La syntaxe complète d'un formatter personnalisé est secondaire ici. Il faut surtout retenir que le mécanisme existe.

Attention : formatage et sérialisation ne sont pas la même chose.

Une sortie lisible destinée à un humain n'est pas automatiquement un format de fichier ou de protocole stable. Une sérialisation doit définir précisément l'encodage, les tailles, l'ordre des champs, les versions et la compatibilité.

72. STRING, STRING_VIEW ET OWNERSHIP

Le concept d'ownership est central en C++.

Demande-toi toujours :

Qui garantit que cette mémoire existe encore ?

Avec :

Code: Select all

std::string text;
l'objet

Code: Select all

text
possède son stockage.

Avec :

Code: Select all

std::string_view view;
la view ne possède pas les caractères.

Quelqu'un d'autre doit garantir leur durée de vie.

Avec :

Code: Select all

const char* ptr;
le pointeur ne donne pratiquement aucune information intrinsèque sur :
  • la taille ;
  • la propriété ;
  • la durée de vie.
C'est au contrat du programme de préciser ces règles.

73. ERREUR : RETOURNER UN POINTEUR VERS UNE CHAÎNE LOCALE

Exemple mauvais :

Code: Select all

const char* getName()
{
    std::string name{
        "Kernel"
    };

    return name.c_str();
}
Lorsque la fonction se termine :

Code: Select all

name
est détruite.

Le pointeur retourné référence alors un stockage invalide.

C'est un dangling pointer.

Même problème avec :

Code: Select all

std::string_view
si la source locale disparaît.

74. BONNE SOLUTION : RETOURNER std::string

Si la fonction doit produire un nouveau texte dont le caller devient propriétaire :

Code: Select all

std::string getName()
{
    return "Kernel";
}
Le C++ moderne optimise très bien ce type de retour grâce notamment aux mécanismes de move et d'élision de copie.

Il ne faut pas retourner un pointeur vers une variable locale pour "éviter une copie".

75. PASSER UNE CHAÎNE À UNE FONCTION

Plusieurs possibilités existent.

Copie :

Code: Select all

void f(
    std::string text
);
La fonction reçoit sa propre valeur.

Référence constante :

Code: Select all

void f(
    const std::string& text
);
Pas de copie de l'objet et la fonction travaille spécifiquement avec un

Code: Select all

std::string
.

Vue :

Code: Select all

void f(
    std::string_view text
);
Très flexible pour une fonction qui ne fait que lire temporairement du texte.

Le meilleur choix dépend du contrat.

76. std::string_view COMME PARAMÈTRE DE LECTURE

Exemple :

Code: Select all

bool isDriver(
    std::string_view filename
)
{
    return
        filename.ends_with(
            ".sys"
        );
}
On peut appeler :

Code: Select all

isDriver(
    "ntfs.sys"
);
Ou :

Code: Select all

std::string name{
    "disk.sys"
};

isDriver(name);
La fonction n'a pas besoin de gérer la propriété du texte.

Elle le consulte uniquement pendant son exécution.

77. NE PAS STOCKER UNE string_view SANS RÉFLÉCHIR

Exemple :

Code: Select all

class Process
{
private:
    std::string_view m_name;
};
Cela peut être parfaitement valide.

Mais seulement si l'objet référencé reste vivant assez longtemps.

Si le constructeur reçoit :

Code: Select all

Process{
    std::string{
        "notepad.exe"
    }
};
la chaîne temporaire peut disparaître immédiatement.

La classe conserve alors une view invalide.

Pour les données appartenant réellement à l'objet, un

Code: Select all

std::string
est généralement plus sûr.

78. INTEROPÉRABILITÉ WIN32

Le monde Windows utilise très souvent les chaînes wide.

Exemple :

Code: Select all

std::wstring path{
    L"C:\\Windows\\System32"
};
Puis :

Code: Select all

CreateFileW(
    path.c_str(),
    GENERIC_READ,
    FILE_SHARE_READ,
    nullptr,
    OPEN_EXISTING,
    FILE_ATTRIBUTE_NORMAL,
    nullptr
);
La logique reste très similaire à

Code: Select all

std::string
.

Il existe : Pour Win32 Unicode, tu rencontreras énormément :

Code: Select all

std::wstring
LPCWSTR
LPWSTR
WCHAR
79. INTEROPÉRABILITÉ POSIX

Sous POSIX, beaucoup d'interfaces utilisent directement :

Code: Select all

const char*
Exemple :

Code: Select all

std::string path{
    "/tmp/data.bin"
};

int fd =
    open(
        path.c_str(),
        O_RDONLY
    );
Tu peux donc écrire du C++ moderne tout en interagissant avec les API POSIX classiques.

Le fait de savoir passer de :

Code: Select all

std::string
à :

Code: Select all

const char*
est essentiel.

80. CHAÎNES ET SOCKETS

Les sockets illustrent parfaitement le modèle buffer + taille.

Exemple :

Code: Select all

std::string message{
    "HELLO"
};

send(
    sock,
    message.data(),
    message.size(),
    0
);
Ici, le réseau ne se préoccupe pas réellement de la notion de "chaîne".

Il envoie des octets.

L'API reçoit :
  • l'adresse des octets ;
  • le nombre d'octets.
81. UNE CHAÎNE N'EST PAS TOUJOURS UN BON BUFFER BINAIRE

Même si

Code: Select all

std::string
peut techniquement contenir des octets nuls, elle représente conceptuellement du texte.

Pour du binaire brut, on préfère souvent des structures comme :

Code: Select all

std::vector<std::byte>
ou :

Code: Select all

std::array<std::byte, N>
Exemple :

Code: Select all

std::vector<std::byte>
buffer(4096);
Puis :

Code: Select all

read(
    fd,
    buffer.data(),
    buffer.size()
);
Cela exprime mieux l'intention :
ce sont des octets, pas du texte.

82. '\0' AU MILIEU D'UN std::string

Un

Code: Select all

std::string
peut contenir un caractère nul au milieu.

Exemple :

Code: Select all

std::string data{
    "ABC\0DEF",
    7
};
Sa taille peut être :

Code: Select all

7
Mais une API C utilisant :

Code: Select all

strlen(
    data.c_str()
)
s'arrêtera au premier

Code: Select all

\0
.

Elle verra seulement :

Code: Select all

ABC
Cela montre encore une fois que :

Code: Select all

longueur std::string
et :

Code: Select all

longueur chaîne C
ne sont pas toujours équivalentes.

83. TEXTES, ENCODAGE ET NOMBRE DE CARACTÈRES

Il faut également faire attention à un point plus avancé.

Un

Code: Select all

char
correspond à une unité de stockage, pas nécessairement à un caractère humain complet.

Avec UTF-8, un caractère visible peut être représenté par plusieurs octets.

Donc :

Code: Select all

string.size()
mesure le nombre d'unités stockées, pas nécessairement le nombre de glyphes affichés à l'écran.

Pour du développement système, il est surtout important de ne pas confondre :
  • octets ;
  • unités d'encodage ;
  • caractères logiques ;
  • glyphes.
84. PERFORMANCE : ÉVITER LES COPIES INUTILES

Imaginons :

Code: Select all

void parse(
    std::string input
);
Chaque appel peut copier ou déplacer une chaîne selon le contexte.

Si la fonction ne fait que lire :

Code: Select all

void parse(
    std::string_view input
);
peut être plus approprié.

Autre exemple :

Code: Select all

std::string token =
    text.substr(
        begin,
        length
    );
crée une nouvelle chaîne.

Une vue :

Code: Select all

std::string_view token =
    view.substr(
        begin,
        length
    );
peut éviter la copie.

Cela compte dans les parsers de gros fichiers ou protocoles.

85. PERFORMANCE : reserve() LORS DE CONCATÉNATIONS

Imagine :

Code: Select all

std::string output;

for (...)
{
    output += piece;
}
Si la chaîne grandit constamment, plusieurs réallocations peuvent se produire.

Si une estimation est connue :

Code: Select all

std::string output;

output.reserve(
    estimatedSize
);
peut réduire ces allocations.

86. PERFORMANCE : NE PAS OPTIMISER À L'AVEUGLE

Le fait qu'une abstraction puisse allouer ne signifie pas qu'il faut revenir systématiquement à :

Code: Select all

malloc()
char*
memcpy()
Une optimisation doit répondre à un problème mesuré.

Le but est de comprendre leur coût et de choisir correctement.

87. EXEMPLE COMPLET : PARSER UNE COMMANDE SYSTÈME

Supposons que ton programme reçoive :

Code: Select all

READ 4096
On stocke le message :

Code: Select all

std::string input{
    "READ 4096"
};
On crée une vue :

Code: Select all

std::string_view view{
    input
};
On définit :

Code: Select all

constexpr
std::string_view prefix{
    "READ "
};
On teste :

Code: Select all

if (
    !view.starts_with(prefix)
)
{
    std::println(
        "Commande inconnue"
    );

    return;
}
On retire le préfixe :

Code: Select all

view.remove_prefix(
    prefix.size()
);
La vue contient maintenant :

Code: Select all

"4096"
On parse directement :

Code: Select all

std::size_t requestedSize{};

auto result =
    std::from_chars(
        view.data(),
        view.data()
            + view.size(),
        requestedSize
    );
Puis :

Code: Select all

if (
    result.ec
    !=
    std::errc{}
)
{
    std::println(
        "Taille invalide"
    );

    return;
}
Enfin :

Code: Select all

std::println(
    "Lecture demandée : {} octets",
    requestedSize
);
Ce petit exemple rassemble : Et surtout :
aucune sous-chaîne temporaire n'est nécessaire pour parser le nombre.

88. EXEMPLE COMPLET : AFFICHAGE D'INFORMATIONS PROCESSUS

Supposons :

Code: Select all

struct ProcessInfo
{
    unsigned long pid;
    std::uint64_t baseAddress;
    std::uint32_t flags;
};
Puis :

Code: Select all

ProcessInfo process{
    1337,
    0x7FF612341000,
    0x25
};
On peut afficher :

Code: Select all

std::println(
    "PID={} BASE=0x{:016X} FLAGS=0x{:08X}",
    process.pid,
    process.baseAddress,
    process.flags
);
Résultat possible :

Code: Select all

PID=1337 BASE=0x00007FF612341000 FLAGS=0x00000025
C'est exactement le genre de sortie utile pour un programme système.

89. EXEMPLE : std::string ET API C

Supposons une fonction externe :

Code: Select all

void legacy_log(
    const char* message
);
Ton programme moderne peut faire :

Code: Select all

std::string message{
    "Driver loaded"
};

legacy_log(
    message.c_str()
);
Le

Code: Select all

std::string
conserve la propriété du texte.

L'API C reçoit simplement une vue C temporaire vers le contenu.

Il faut évidemment que l'API ne conserve pas le pointeur après la destruction ou la modification de la chaîne, sauf contrat spécifique.

90. EXEMPLE : POURQUOI GARDER data() PEUT ÊTRE DANGEREUX

Code: Select all

std::string text{
    "ABC"
};

const char* ptr =
    text.data();

text +=
    "DEF";

std::println(
    "{}",
    ptr
);
Ce code n'est pas fiable.

Pourquoi ?

Parce que

Code: Select all

text += "DEF"
peut provoquer une réallocation.

Le pointeur mémorisé avant l'opération peut ne plus être valide.

Règle générale :

Après une opération pouvant réallouer, récupère à nouveau le pointeur si nécessaire.

91. EXEMPLE : string_view ET MODIFICATION DE LA SOURCE

Code: Select all

std::string text{
    "Hello"
};

std::string_view view{
    text
};

text +=
    " World";
Là encore, la modification de

Code: Select all

text
peut réallouer.

La

Code: Select all

view
peut donc devenir invalide.

Une string_view n'est pas magiquement reliée à l'objet source.

Elle conserve simplement une adresse et une longueur.

Si le stockage source bouge, la view ne se met pas automatiquement à jour.

92. LES CHAÎNES ET LE DÉVELOPPEMENT KERNEL

En kernel, les règles changent souvent.

La bibliothèque standard C++ peut être absente, limitée ou déconseillée selon l'environnement.

Sous Windows kernel, on rencontre notamment des structures spécifiques comme :

Code: Select all

UNICODE_STRING
Le modèle reste très intéressant :

Code: Select all

buffer
length
maximum length
On retrouve encore une fois la logique :

Code: Select all

adresse + taille
Donc comprendre les chaînes C, les buffers et

Code: Select all

string_view
aide réellement à comprendre les représentations système, même lorsque la STL n'est pas utilisée directement.

93. LES CHAÎNES ET LES API WINDOWS

Une structure conceptuelle Windows peut comporter :

Code: Select all

PWSTR Buffer;
USHORT Length;
USHORT MaximumLength;
Remarque la ressemblance conceptuelle avec :

Code: Select all

pointer
length
capacity
Cette manière de penser est utile.

Les abstractions C++ ne sont pas séparées du bas niveau.

Elles formalisent souvent les mêmes idées avec des règles plus sûres.

94. LE BON MODÈLE MENTAL POUR UN DÉVELOPPEUR SYSTÈME

Tu peux retenir :

Code: Select all

const char*
= une adresse vers des caractères.

Souvent associé à une chaîne C terminée par zéro, mais le pointeur lui-même ne stocke ni longueur ni ownership.

Code: Select all

std::string
= un propriétaire dynamique de caractères.

Il gère :
  • la taille ;
  • la capacité ;
  • le stockage ;
  • la destruction.

Code: Select all

std::string_view
= une fenêtre non propriétaire sur une séquence existante.

Elle représente conceptuellement :

Code: Select all

adresse + longueur
95. ERREURS CLASSIQUES À ÉVITER
  • Confondre

    Code: Select all

    sizeof(pointer)
    et longueur de chaîne.
  • Oublier le

    Code: Select all

    \0
    d'une chaîne C.
  • Utiliser

    Code: Select all

    strcpy()
    sans garantir la capacité.
  • Comparer des

    Code: Select all

    char*
    avec

    Code: Select all

    ==
    en pensant comparer le texte.
  • Conserver

    Code: Select all

    string.data()
    après une réallocation.
  • Retourner

    Code: Select all

    c_str()
    d'une chaîne locale.
  • Créer une

    Code: Select all

    string_view
    vers un temporaire.
  • Supposer qu'une

    Code: Select all

    string_view
    est terminée par zéro.
  • Utiliser

    Code: Select all

    printf("%s")
    avec une vue non terminée.
  • Créer des copies de

    Code: Select all

    std::string
    inutiles dans un parser.
96. CE QU'IL FAUT SAVOIR PAR CŒUR

Pour ton niveau professionnel futur, je te conseille de connaître sans hésitation :

Code: Select all

strlen()
strcmp()

std::string
std::string::size()
std::string::data()
std::string::c_str()
std::string::find()
std::string::substr()
std::string::reserve()
std::string::capacity()

std::string_view
std::string_view::data()
std::string_view::size()
std::string_view::substr()
std::string_view::remove_prefix()
std::string_view::remove_suffix()

std::from_chars()
std::to_chars()
Et surtout de comprendre profondément les règles mémoire derrière ces fonctions.

97. CE QUI PEUT ÊTRE CONNU SANS ÊTRE MÉMORISÉ IMMÉDIATEMENT

Tu n'as pas besoin de mémoriser chaque détail de :
  • tous les spécificateurs de format ;
  • tous les formats de ranges ;
  • les custom formatters complexes ;
  • chaque conversion numérique disponible.
L'important est de savoir qu'ils existent et de savoir consulter la documentation lorsque tu en as besoin.

98. MINI-VÉRIFICATIONS À SAVOIR FAIRE

Tu dois être capable d'expliquer immédiatement les cas suivants :

Code: Select all

char text[] = "ABC";

sizeof(text);   // 4
strlen(text);   // 3
car le tableau contient aussi .

Avec :

Code: Select all

const char* text = "ABC";

sizeof(text);

Code: Select all

sizeof
mesure le pointeur, pas la longueur de la chaîne.

Enfin, une

Code: Select all

std::string_view
retournée vers une

Code: Select all

std::string
locale devient dangling lorsque la chaîne locale est détruite. Ce raisonnement de durée de vie est plus important que de mémoriser une syntaxe.

103. EXERCICE PRATIQUE BAS NIVEAU

Écris un programme qui reçoit une chaîne :

Code: Select all

"WRITE 1024"
et qui :
  • stocke la chaîne dans un

    Code: Select all

    std::string
    ;
  • crée une

    Code: Select all

    std::string_view
    ;
  • vérifie le préfixe

    Code: Select all

    "WRITE "
    ;
  • retire le préfixe sans copie ;
  • convertit

    Code: Select all

    "1024"
    avec

    Code: Select all

    std::from_chars
    ;
  • affiche la valeur en décimal et en hexadécimal.
Exemple de sortie :

Code: Select all

SIZE=1024
HEX=0x400
Cet exercice combine les concepts les plus utiles du chapitre.

104. SYNTHÈSE GÉNÉRALE

Les chaînes sont un excellent exemple de la philosophie du C++ moderne.

Le langage ne t'oblige pas à abandonner le bas niveau.

Il te permet plutôt de choisir plusieurs niveaux d'abstraction.

Au niveau le plus brut :

Code: Select all

const char*
Tu manipules directement une adresse et tu dois comprendre le contrat du buffer.

Avec :

Code: Select all

std::string
tu délègues la propriété, les allocations et la destruction à un objet RAII.

Avec :

Code: Select all

std::string_view
tu obtiens une abstraction extrêmement légère représentant essentiellement :

Code: Select all

adresse + longueur
sans copie et sans ownership.

Un programme professionnel peut très bien faire :

Code: Select all

std::string command;
pour posséder l'entrée,

puis :

Code: Select all

std::string_view view{
    command
};
pour la parser sans copie,

puis :

Code: Select all

std::from_chars(
    view.data(),
    view.data() + view.size(),
    value
);
pour transformer directement une partie de la mémoire en nombre,

et finalement :

Code: Select all

std::println(
    "VALUE=0x{:08X}",
    value
);
pour produire un diagnostic lisible.

C'est exactement le type de combinaison qu'il faut rechercher :

abstractions modernes lorsque leur coût et leur contrat sont adaptés, accès mémoire direct lorsque c'est nécessaire.

À RETENIR ABSOLUMENT
  • Une chaîne C est généralement terminée par

    Code: Select all

    \0
    .
  • Code: Select all

    strlen()
    parcourt la chaîne jusqu'au terminateur.
  • Code: Select all

    sizeof(pointer)
    ne donne pas la longueur du texte.
  • Un buffer possède une capacité ; le texte possède une longueur.
  • Les fonctions C historiques peuvent provoquer des dépassements si les tailles sont mal gérées.
  • Code: Select all

    std::string
    possède et gère sa mémoire.
  • Code: Select all

    std::string::data()
    permet d'accéder au stockage contigu.
  • Code: Select all

    c_str()
    permet l'interopérabilité avec de nombreuses API C.
  • Une réallocation peut invalider les pointeurs vers le stockage interne.
  • Code: Select all

    std::string_view
    ne possède pas les données.
  • Une

    Code: Select all

    string_view
    représente essentiellement une adresse et une longueur.
  • Une view peut devenir dangling si sa source disparaît ou réalloue.
  • Une

    Code: Select all

    string_view
    n'est pas forcément terminée par zéro.
  • Code: Select all

    from_chars()
    et

    Code: Select all

    to_chars()
    sont particulièrement intéressants pour le parsing bas niveau.
  • Le formatage hexadécimal et binaire est très utile pour les outils système.
  • En développement bas niveau, l'objectif n'est pas d'éviter toutes les abstractions, mais de comprendre précisément ce qu'elles font en mémoire.

Return to “Bases du C++”

Who is online

Users browsing this forum: No registered users and 1 guest