Menu

Pointeurs en C++ : opérateur d'adresse, déréférencement et nullptr

Les pointeurs en C++ expliqués depuis zéro : déclarer un pointeur, les opérateurs & (adresse de) et * (déréférencement), nullptr, les pointeurs vers des tableaux et les pièges des pointeurs pendants et non initialisés qui provoquent des plantages.

Cette page contient des éditeurs exécutables - modifiez, exécutez et voyez la sortie instantanément.

Une variable qui contient une adresse

Chaque variable réside quelque part en mémoire, à un emplacement numéroté appelé son adresse. La plupart du temps, peu vous importe où : vous utilisez simplement le nom de la variable. Un pointeur inverse cela : c'est une variable dont la valeur est une adresse. Au lieu de contenir 42, il contient « l'endroit où 42 est stocké ».

Cette indirection est ce qui rend les pointeurs puissants. Les fonctions peuvent modifier une variable de l'appelant via l'un d'eux, des structures de données comme les listes chaînées relient leurs nœuds avec eux, et (comme vous le verrez dans mémoire dynamique) c'est ainsi que l'on accède à la mémoire allouée à l'exécution.

Le & dans &score est l'opérateur adresse de : il produit l'emplacement de score. Le * dans *p est l'opérateur déréférencement : il suit l'adresse jusqu'à la valeur qui s'y trouve.

Les deux opérateurs : & et *

La chose la plus déroutante pour les débutants est que * signifie deux choses différentes selon l'endroit où il se trouve. Gardez ceci à l'esprit :

int* p;     // DÉCLARATION : « p est un pointeur vers int »
p = &x;     // & = adresse de : stocke l'adresse de x dans p
int y = *p; // * = déréférencement : lit la valeur pointée par p
*p = 99;    // déréférencement à gauche : écrit à travers le pointeur

Dans une déclaration, * fait partie du type. Dans une expression, * effectue un travail. Une fois un pointeur configuré, le déréférencer vous donne un accès complet en lecture/écriture à la variable d'origine :

Remarquez que vous n'avez plus jamais touché health par son nom après la ligne 1, et pourtant sa valeur a continué de changer. C'est tout l'intérêt : hp est un alias du même emplacement de stockage. L'espacement (int* p, int *p, int*p) est cosmétique et identique pour le compilateur ; ce guide utilise int* p.

nullptr : ne pointer sur rien

Un pointeur qui ne pointe nulle part doit être défini à nullptr (C++11). C'est une manière claire et sûre au niveau du type de dire « pas encore de cible », et cela vous donne quelque chose à tester avant de déréférencer.

Préférez nullptr à l'ancienne macro NULL ou à un 0 nu. Comme nullptr a un vrai type de pointeur, il n'est jamais interprété à tort comme l'entier 0 lors de la résolution de surcharge — un bug subtil que l'ancien style pouvait provoquer.

Piège : le déréférencement de null. Lire ou écrire à travers un pointeur null (ou non initialisé) est un comportement indéfini, généralement un plantage instantané :

int* p = nullptr;
cout << *p;   // PLANTAGE - déréférencer null est un comportement indéfini

Protégez-vous toujours avec if (p) (ou if (p != nullptr)) avant de déréférencer quoi que ce soit qui pourrait être null.

Pointeurs et tableaux

Le nom d'un tableau se dégrade (decay) en un pointeur vers son premier élément, si bien que pointeurs et tableaux sont profondément liés. Ajouter 1 à un pointeur n'ajoute pas un octet : cela avance d'un élément, ce qui fait fonctionner l'arithmétique des pointeurs :

p[i] et *(p + i) sont littéralement la même expression — cette équivalence explique pourquoi les tableaux sont indexés à partir de zéro. Le bug classique ici est d'aller au-delà de la fin : nums + 4 est un marqueur valide « un-au-delà-de-la-fin » à utiliser pour la comparaison, mais déréférencer *(nums + 4) lit hors limites. Les erreurs de décalage d'un (off-by-one) avec les pointeurs sont une cause majeure de plantages et de corruption silencieuse ; soyez donc rigoureux quant à votre condition d'arrêt.

const et les pointeurs

const peut s'appliquer à ce que le pointeur pointe, au pointeur lui-même, ou aux deux. Lisez la déclaration de droite à gauche pour la décoder :

const int* p;        // pointeur vers const int  - ne peut pas changer *p, peut repointer p
int* const p = &x;   // pointeur const vers int  - peut changer *p, ne peut pas repointer p
const int* const p = &x; // les deux verrouillés

Cela compte sans cesse dans le vrai code. Une fonction qui promet de ne pas modifier vos données prend un pointeur vers const :

Marquer le pointé const documente l'intention et permet au compilateur d'empêcher les écritures accidentelles — de la sûreté gratuite, sans coût à l'exécution.

Le grand piège : les pointeurs pendants

Un pointeur pendant pointe vers une mémoire qui ne contient plus la valeur attendue : la variable est sortie de sa portée, ou la mémoire a été libérée. Le déréférencer est un comportement indéfini, et le pire, c'est qu'il semble souvent fonctionner — jusqu'à ce que ce ne soit plus le cas.

int* makeBad() {
    int local = 5;
    return &local;   // BUG : local meurt au retour de la fonction
}                    // le pointeur renvoyé est maintenant pendant

L'adresse reste un nombre valide, mais elle pointe vers un emplacement de pile qui a été récupéré — le lire donne des données aberrantes ou plante. La même chose se produit si vous conservez un pointeur vers un objet du tas (heap) delete ou vers un élément d'un vector qui réalloue ensuite.

Trois règles vous gardent en sécurité :

  • Ne renvoyez jamais l'adresse d'une variable locale. Renvoyez par valeur, ou laissez l'appelant posséder le stockage.
  • Mettez un pointeur à nullptr une fois que ce qu'il pointait a disparu, et vérifiez avant de l'utiliser.
  • Pour la propriété et les durées de vie, tournez-vous vers les pointeurs intelligents plutôt que vers les new/delete bruts — ils libèrent la mémoire automatiquement et réduisent toute cette catégorie de bugs.

Suite : références vs pointeurs

Les pointeurs ne sont pas le seul moyen de référencer indirectement une autre variable. C++ propose aussi les références, qui semblent similaires mais ne peuvent pas être null, ne peuvent pas être réassignées et utilisent une syntaxe plus propre. Ensuite, nous les comparerons côte à côte dans références vs pointeurs pour que vous sachiez exactement quel outil choisir — et pourquoi la majeure partie du C++ moderne préfère les références quand il peut les utiliser.

Questions fréquentes

Qu'est-ce qu'un pointeur en C++ ?

Un pointeur est une variable qui stocke l'adresse mémoire d'une autre valeur plutôt que la valeur elle-même. On le déclare avec * (par exemple int* p), on récupère une adresse avec l'opérateur & (p = &x), et on lit ou écrit la valeur pointée en la déréférençant avec *p.

Quelle est la différence entre & et * dans les pointeurs C++ ?

Dans un contexte de pointeur, & est l'opérateur adresse de : &x donne l'adresse de x. * joue deux rôles : dans une déclaration (int* p), il marque la variable comme un pointeur, et dans une expression (*p), il déréférence le pointeur pour atteindre la valeur stockée à cette adresse.

Qu'est-ce que nullptr en C++ et pourquoi l'utiliser à la place de NULL ?

nullptr est un littéral de pointeur null sûr au niveau du type, ajouté en C++11. Il signifie « ne pointe sur rien ». Préférez-le à l'ancien NULL ou à un 0 nu, car nullptr a un vrai type de pointeur : il n'est donc jamais confondu avec un entier lors de la résolution de surcharge. Vérifiez toujours if (p) avant de déréférencer : déréférencer un pointeur null est un comportement indéfini.

Coddy programming languages illustration

Apprendre à coder avec Coddy

COMMENCER