Aller au contenu

Pointeurs : adresses, déréférencement et arithmétique

Comprenez les pointeurs en tant qu'adresses mémoire, apprenez les opérateurs d'adresse et de déréférencement, l'arithmétique des pointeurs dans les tableaux, la const-correction avec les pointeurs, et quand les pointeurs bruts restent appropriés en C++ moderne.

Qu'est-ce qu'un pointeur ?

Un pointeur est une variable qui stocke une adresse mémoire. Chaque objet en C++ réside à une certaine adresse en mémoire, et un pointeur vous permet de désigner cet objet indirectement. Les pointeurs sont fondamentaux en C++ — ils rendent possibles les structures de données dynamiques, le polymorphisme et le passage efficace de paramètres.

Un type pointeur se déclare avec * : int* p; déclare un pointeur vers un int. Le pointeur lui-même occupe de la mémoire (généralement 8 octets sur les systèmes 64 bits) et sa valeur est une adresse, pas les données vers lesquelles il pointe.

Adresse et déréférencement

Les deux opérateurs fondamentaux des pointeurs sont & (prise d'adresse) et * (déréférencement). L'opérateur & obtient l'adresse d'une variable. L'opérateur * suit l'adresse pour accéder à l'objet pointé.

address_deref.cpp
#include <iostream>

int main() {
    int value = 42;
    int* ptr = &value;  // ptr holds the address of value

    std::cout << "value:  " << value << '\n';        // 42
    std::cout << "&value: " << &value << '\n';       // e.g. 0x7ffd5a3c
    std::cout << "ptr:    " << ptr << '\n';           // same address
    std::cout << "*ptr:   " << *ptr << '\n';          // 42 (dereference)

    *ptr = 100;  // modify value through the pointer
    std::cout << "value after *ptr = 100: " << value << '\n'; // 100

    int* null_ptr = nullptr;  // points to nothing — safe sentinel
    // *null_ptr = 5;  // UNDEFINED BEHAVIOR — crash on most systems
}

Arithmétique des pointeurs et conversion tableau-pointeur

L'arithmétique des pointeurs n'est valide qu'à l'intérieur d'un tableau (ou une position après la fin). Le compilateur ajuste l'arithmétique en fonction de sizeof(T). Dans la plupart des contextes, le nom d'un tableau se dégrade (decay) en pointeur vers son premier élément — c'est pourquoi les tableaux de style C perdent l'information sur leur taille lorsqu'ils sont passés à des fonctions.

pointer_arithmetic.cpp
#include <iostream>
#include <cstddef>  // std::ptrdiff_t

int main() {
    int arr[5] = {10, 20, 30, 40, 50};
    int* p = arr;  // array decays to pointer to first element

    std::cout << *p << '\n';        // 10
    std::cout << *(p + 2) << '\n';  // 30  (moves 2*sizeof(int) bytes)
    std::cout << p[3] << '\n';      // 40  (p[i] is *(p + i))

    // Iterating via pointer
    for (int* it = arr; it != arr + 5; ++it) {
        std::cout << *it << ' ';
    }
    std::cout << '\n';  // 10 20 30 40 50

    // Pointer difference
    std::ptrdiff_t diff = (arr + 4) - arr;  // 4 (elements, not bytes)
    std::cout << "diff: " << diff << '\n';

    // WARNING: arithmetic outside the array is UNDEFINED BEHAVIOR
    // int* bad = arr + 10;  // UB — don't do this
}

Pointeurs const et pointeurs vers const

La position de const par rapport à * détermine ce qui est immuable :

- const int* p (ou int const* p) — pointeur vers const : vous ne pouvez pas modifier la valeur pointée via p, mais vous pouvez faire pointer p ailleurs.
- int* const p — pointeur const : le pointeur lui-même est immuable (impossible de le faire pointer ailleurs), mais vous pouvez modifier la valeur vers laquelle il pointe.
- const int* const p — le pointeur et la valeur pointée sont tous deux immuables.

Lisez les déclarations de droite à gauche : int* const p se lit « p est un pointeur const vers un int ».

Les pointeurs void (void*) peuvent contenir n'importe quelle adresse mais ne peuvent pas être déréférencés sans conversion. Ils sont principalement utilisés dans les API de style C et dans les implémentations d'allocateurs.

Bonne pratique

En C++ moderne, les pointeurs bruts doivent être utilisés exclusivement comme des pointeurs observateurs, non propriétaires. Ils signifient : « Je peux voir cet objet, mais je ne suis pas responsable de sa durée de vie. » Usages légitimes courants :

- Implémentation de structures de données (nœuds d'un arbre pointant vers leur parent)
- Paramètres de fonction qui observent sans posséder (même si les références sont souvent préférables)
- Interfaçage avec des API C
- Itérateurs dans des conteneurs personnalisés

Si un pointeur possède une ressource, enveloppez-le dans un std::unique_ptr ou un std::shared_ptr. Si vous vous surprenez à écrire delete, vous faites presque certainement fausse route en C++ moderne.

Piège

Les pointeurs pendants (dangling pointers) sont le bogue n° 1 lié aux pointeurs. Un pointeur devient pendant lorsque l'objet vers lequel il pointe est détruit :

- Retourner un pointeur vers une variable locale
- Supprimer un objet alors qu'un autre pointeur y fait encore référence
- Un pointeur vers un élément d'un std::vector qui est réalloué après un push_back

Les pointeurs non initialisés contiennent des adresses aléatoires. Initialisez-les toujours à nullptr si vous n'avez pas encore de cible. Le compilateur ne vous avertira PAS de toutes les utilisations de pointeurs non initialisés.

Pointeur ou référence : lequel utiliser ?

Les références et les pointeurs offrent tous deux une indirection, mais ils diffèrent sur des points importants. Les références ne peuvent pas être nulles, ne peuvent pas être réassociées à un autre objet et ne nécessitent pas de déréférencement explicite. Préférez les références pour les paramètres de fonction ; utilisez des pointeurs lorsque la nullabilité ou la réaffectation est nécessaire.

ptr_vs_ref.cpp
#include <iostream>

void increment_ref(int& val) { ++val; }         // cannot be null
void increment_ptr(int* val) { if (val) ++(*val); } // must null-check

int main() {
    int x = 10;
    increment_ref(x);        // clean call syntax
    increment_ptr(&x);       // caller must take address
    std::cout << x << '\n';  // 12

    int* p = &x;
    p = nullptr;  // OK — pointers can be re-seated and nulled

    int& r = x;
    // r = ???;    // references cannot be re-seated
    // int& bad;   // ERROR — references must be initialized
}
Points clés
  • & obtient une adresse, * la suit — ce sont des opérations inverses
  • L'arithmétique des pointeurs n'est valide qu'à l'intérieur des tableaux et s'ajuste selon sizeof(T)
  • Les noms de tableaux se dégradent en pointeurs et perdent l'information sur leur taille
  • Lisez la position de const de droite à gauche : const int* vs int* const
  • En C++ moderne, les pointeurs bruts ne doivent être que des observateurs non propriétaires
  • Préférez les références aux pointeurs, sauf si vous avez besoin de nullabilité ou de réaffectation

Quiz — Testez vos connaissances

(15 XP)

1. Que signifie `const int* p` ?

2. Que se passe-t-il lorsqu'un tableau est passé à une fonction qui attend un paramètre pointeur ?

3. Quand l'arithmétique des pointeurs est-elle valide en C++ ?