Comme beaucoup d'autres langages, le C++ dispose de pointeurs. Tu connais déjà les références et tu sais que les pointeurs leur ressemblent, mais considère-les comme un niveau plus proche du fonctionnement interne de ton ordinateur. Les pointeurs sont des variables qui contiennent des adresses d'objets. On les utilise pour interagir directement avec les objets, ce qui permet l'allocation dynamique de mémoire et une manipulation efficace des données en C++.
Si tu découvres les pointeurs, ils peuvent te sembler un peu mystérieux, mais une fois que tu t'y es habitué, ils sont tout à fait simples.
Ils sont un élément crucial du C++, alors prends le temps de bien les comprendre. La version minimaliste présentée dans ce concept est aussi appelée pointeur brut ou pointeur non intelligent. Avec le C++ moderne, il existe aussi des pointeurs intelligents ; le type de base n'a lui rien d'intelligent et tu dois gérer tout le travail manuellement.
Avant d'entrer dans les détails, il vaut la peine de comprendre l'utilité des pointeurs. Les pointeurs sont un moyen de partager l'adresse d'un objet avec d'autres parties de notre programme, ce qui est utile pour deux raisons majeures :
Une déclaration de pointeur en C++ implique de préciser le type de données vers lequel le pointeur pointe, suivi d'un astérisque (*) et du nom du pointeur.
Lorsqu'on déclare des pointeurs, ils ne sont pas initialisés automatiquement.
Sans affectation explicite, un pointeur contient généralement une valeur indéterminée, souvent appelée «\u00A0adresse parasite\u00A0».
Certains compilateurs initialisent peut-être les pointeurs à nullptr, mais ce comportement n'est garanti sur aucun compilateur, il est donc essentiel de ne pas s'y fier.
La bonne pratique consiste à initialiser explicitement les pointeurs bruts et à vérifier qu'ils ne sont pas nuls avant de les utiliser, afin d'éviter des problèmes potentiels.
int* ptr{nullptr}; // Declares a pointer and makes sure it is not invalid
Pour affecter l'adresse d'une variable à un pointeur, on utilise l'opérateur d'adressage (&).
Déréférencer un pointeur se fait à l'aide de l'opérateur d'indirection (*).
std::string opponent{"Solomon Lane"};
// 'ethan' points to the address of the string opponent
std::string* ethan{&opponent};
// Instead of ethan's, the opponent's name address is given to the passPort
std::string passportName{*ethan};
Attention\u00A0: le déréférencement doit être fait explicitement, alors que les références fonctionnaient simplement comme un alias.
L'arithmétique des pointeurs permet d'effectuer des opérations arithmétiques sur des pointeurs, ce qui est particulièrement utile lorsqu'on travaille avec des tableaux. Ajouter un entier à un pointeur fait qu'il pointe vers un autre élément.
// Stargate Coordinate Code
int gateCode[] = {462, 753, 218, 611, 977};
// 'ptr' points to the first element of 'gateCode'
int* ptr{&gateCode[0]};
// Accesses the third Stargate address through pointer arithmetic
int dialedAddress{*(ptr + 2)};
// Chevron encoded! Dialing Stargate address:
openStarGate(dialedAddress);
L'arithmétique des pointeurs en C++ peut facilement mener à un comportement indéfini si l'on n'y prend pas garde. Le comportement indéfini peut se manifester par des résultats inattendus, des plantages, voire des failles de sécurité. Un exemple tristement célèbre des conséquences d'un comportement indéfini est l'explosion de la fusée Ariane 5 en 1996, où une exception logicielle provoquée par la conversion d'un nombre à virgule flottante de 64 bits en un entier signé de 16 bits a entraîné une défaillance catastrophique.
En C++, l'opérateur -> sert à accéder aux membres d'un objet à travers un pointeur vers cet objet.
C'est une notation abrégée qui simplifie l'accès aux membres des objets pointés par des pointeurs.
Par exemple, si ptr est un pointeur vers un objet qui possède une variable membre x, au lieu d'écrire (*ptr).x, tu peux écrire directement ptr->x.
Cet opérateur améliore la lisibilité du code et réduit la verbosité lorsqu'on travaille avec des pointeurs vers des objets.
Voici un exemple rapide, avec une structure Superhero qui possède une variable membre superpower.
La fonction main crée un pointeur dianaPrince vers un objet Superhero (qui représente Wonder Woman).
L'opérateur -> sert à accéder à la variable membre superpower, ce qui met en valeur le «\u00A0Lasso of Truth\u00A0» emblématique de Wonder Woman.
struct Superhero {
std::string superpower;
};
Superhero wonder_woman{};
Superhero* dianaPrince = &wonder_woman;
dianaPrince->superpower = "Lasso of Truth";
// Using the -> operator to access member variable superpower:
std::cout << "Wonder Woman, possesses the mighty " << dianaPrince->superpower;
// Memory cleanup:
delete dianaPrince;
Les pointeurs et les références permettent tous deux d'accéder indirectement à des objets, mais ils diffèrent par leurs capacités et par les garanties de sécurité qu'ils offrent. Les pointeurs offrent la souplesse de changer d'objet cible et peuvent recevoir la valeur nulle. Cette souplesse introduit toutefois des risques, comme le déréférencement d'un pointeur nul ou la création de pointeurs suspendus. Les références, quant à elles, ne peuvent pas être nulles et sont liées à des objets valides dès leur création, ce qui évite ces risques. Comme elles sont plus sûres, il faut préférer les références aux pointeurs, sauf lorsque les fonctionnalités supplémentaires offertes par les pointeurs sont nécessaires.
En C++, on utilise new pour allouer dynamiquement de la mémoire pour des objets sur le tas au moment de l'exécution.
Il renvoie un pointeur vers l'adresse mémoire allouée, ce qui permet à des objets d'avoir une durée de vie indépendante d'une portée donnée.
Il est toutefois important de toujours associer new à delete pour libérer correctement la mémoire.
L'allocation dynamique de mémoire doit être utilisée avec prudence pour éviter des problèmes courants comme les fuites de mémoire et l'accès à de la mémoire libérée.
Les pointeurs peuvent aussi recevoir une valeur spéciale appelée pointeur nul, qui indique qu'ils ne pointent vers aucune adresse valide. Les pointeurs nuls servent couramment à initialiser des pointeurs avant de leur affecter des valeurs valides.
// There is a dark mark bearer, but who is it?
std::string* darkMarkBearer{nullptr};
// It is Bellatrix!
darkMarkBearer = new std::string{"Bellatrix Lestrange"};
// She has been defeated
delete darkMarkBearer;
// Reset pointer to null, symbolizing there is no more darkMarkBearer at the moment
darkMarkBearer = nullptr;
Un pointeur doit soit contenir l'adresse d'un objet valide, soit valoir nullptr.
Ainsi, il te suffit de tester si un pointeur est nul, et on peut supposer que tout pointeur non nul est valide.
Lorsqu'un objet est détruit, les pointeurs qui pointaient vers cet objet deviennent suspendus (ils ne sont pas automatiquement mis à nullptr).
C'est à toi de détecter ces cas et de veiller à ce que ces pointeurs soient ensuite mis à nullptr.
Dans du code plus ancien, tu peux rencontrer deux alternatives à nullptr.
D'abord, le littéral 0 est spécifiquement interprété comme une valeur nulle pour les pointeurs, bien que ce soit le seul cas où un littéral entier peut être affecté à un pointeur.
Ensuite, la preprocessor macro NULL, héritée du C et définie dans l'en-tête <cstddef>, est une autre représentation du pointeur nul, même si son usage est moins courant dans le code C++ moderne.