Les pointeurs

Nous avons vu plus haut qu’en C++, les objets ne sont pas forcément des références. Si on déclare une variable Point p(12,42);, alors p est l’objet lui-même et non une référence. Nous avons aussi vu que nous pouvions passer un objet “par référence” à une procédure en ajoutant le suffixe & au type. Mais, en C++, nous pouvons aussi obtenir une référence vers un objet en utilisant & comme opérateur et non comme suffixe à un type. Considérons l’exemple suivant :

Point p(12,42);
Point* p2 = &p;

Nous voyons deux choses. Tout d’abord le type de p2 est Point* avec une * et on lit p2 est un pointeur vers un Point. Nous voyons aussi l’opérateur & qui signifie “l’adresse de”. La deuxième ligne se lit donc “p2 est un pointeur vers un Point et il pointe actuellement vers l’objet p”. On pourrait aussi dire que “la valeur de p2 est l’adresse de p”.

Comme en Java, on peut aussi assigner une valeur nulle à un pointeur :

Point* p2 = NULL;

Note

Dans le chapitre sur les fonctions, on a vu quelque chose qui ressemblait aux pointeurs avec les références. Nous avons vu ce concept dans le passage d’arguments, mais on peut aussi l’utiliser en dehors de ce contexte. On peut par exemple écrire :

Point p(12,42);
Point& p2 = p;

Dans ce cas p2 est une référence sur le point p. Contrairement aux pointeurs, les références sont des constantes et par conséquent ne peuvent pas être null.

Nous pouvons aussi créer un objet dynamiquement avec l’opérateur new qui appelle aussi le constructeur :

Point* p2 = new Point(12, 42);

Jusqu’ici ça ressemble assez à Java, mais C++ a une syntaxe différente pour accéder aux attributs d’un objet représenté par un pointeur. En effet, p2 dans les exemples ci-dessus est un pointeur et si on veut l’objet lui-même, il faut tout d’abord “déréférencer” le pointeur avec l’opérateur * :

Point* p2 = new Point(12, 42);
(*p2).move(1,2);

Cette deuxième ligne peut se lire : “suis le pointeur p2 pour obtenir un objet de type Point et déplace de point selon les arguments”. Notez que les parenthèses sont nécessaires, car l’opération . a une précédence plus élevée que *.

La combinaison d’opération “déréférence” + “accès à un attribut” est très fréquente eu C++ et le langage nous offre un raccourci. Nous pouvons écrire cette deuxième ligne ainsi :

p2->move(1,2);

Vous pouvez vous souvenir que l’opérateur . s’utilise pour les objets et “-> “s’utilise pour les pointeurs.

On peut aussi utiliser l’opérateur * pour copier un objet référencé :

Point p3 = *p2;

Ici p3 n’est plus une référence, mais un nouvel objet initialisé avec les membres de p2.

Ce qui est déroutant au début avec C++ c’est que les symboles & et * peuvent être utilisés comme opérateur ou comme suffixe à un type.

  • & comme opérateur précède une variable et retourne l’adresse de cette variable : &p
  • & comme suffixe à un type est utilisé dans les arguments des procédures et signifie que l’argument est passé par référence : (Point& p)
  • * comme opérateur précède une variable de type pointeur et retourne l’objet pointé par la variable : *p2
  • * comme suffixe à un type indique que le type est pointeur vers ce type: Point* p2

Sachez que C++ ne vérifie pas si un pointeur est une référence valide et l’utilisation de pointeurs non initialisés peut provoquer de graves erreurs difficiles à trouver.

Une autre différence de taille est que C++ n’a pas de “garbage collector” comme Java. Lorsque vous créez une référence avec new, il est de votre responsabilité de rendre la mémoire avec l’opérateur delete :

Point* p2 = new Point(12, 42);
...
delete p2;

Si vous oubliez de rendre la mémoire, personne ne le fera à votre place et votre programme souffrira d’un bug connu sous le nom de “memory leak”. Si le programme tourne suffisamment longtemps, il risque d’utiliser toute la mémoire disponible et provoquera l’arrêt brutal du programme.

En C++, les pointeurs ne sont pas réservés aux objets, et on peut très bien déclarer un pointeur vers un entier:

int i = 42;
int* j = &i;
*j = 43;

Dans l’exemple ci-dessus, j est un pointeur vers l’entier i et si on souhaite obtenir ou modifier sa valeur, vous devons le déréférencer avec l’opérateur *.

l’opérateur new peut aussi être utilisé en C++ pour créer des tableaux dynamiques. L’exemple suivant crée un tableau a de 10 entiers :

int* a = new int[10];

Le tableau est créé au “run time” et la variable a est donc un pointeur. Ca explique la notation du type avec l’astérisque (int*). Mais où est donc passé l’information que c’est un tableau ? Pourquoi n’avons-nous pas écrit int[]* a pour indiquer que a est un pointeur vers un tableau ? Comment savoir si a pointe vers un entier ou vers un tableau d’entiers ?

Pour C/C++, l’adresse vers un tableau est équivalente à l’adresse vers le premier élément du tableau. Quand nous voyons int* a nous savons que a pointe vers un entier, mais il peut aussi pointer sur le premier élément d’un tableau d’entiers.

Tout comme pour les objets, le programmeur C++ est responsable de rendre la mémoire allouée pour un tableau avec l’opérateur delete[].

delete[] a;

Nous retrouvons souvent ce concept avec les chaînes de caractères. En C/C++, une chaîne de caractères est un tableau de caractères et si une fonction attend une chaîne de caractère comme paramètre, elle le spécifie souvent comme un pointeur. Nous pouvons le voir par exemple dans la déclaration de la fonction printf :

int printf (const char* format, ...);

Le const indique que la méthode ne modifie pas la variable format et permet au compilateur de mieux optimiser le code.

Les opérateurs new et delete sont propre à C++. En C, nous pouvons aussi allouer et retourner de la mémoire avec les fonctions malloc/calloc et free de la bibliothèque stdlib.h.

En C/C++, en plus de pointer vers des variables, un pointeur peut aussi pointer vers une fonction. Les pointeurs de fonctions sont utilisés en C pour émuler les méthodes et pour permettre une pseudo-programmation orientée objet. On les utilise aussi un peu comme des interfaces fonctionnelles ou des lambdas1 de Java comme par exemple la fonction bsearch de la bibliothèque stdlib.h qui implémente une recherche binaire :

#include <stdlib.h>
void *bsearch(const void* key, const void* base,
    size_t nmemb, size_t size,
    int (*compar)(const void*, const void*));

Dans cette déclaration, nous voyons des pointeurs vers void qui sont des pointeurs “universels” et correspondent aux Object de Java et nous voyons aussi l’argument compar qui est un pointeur de fonction. Pour être compatible avec l’argument, le pointeur doit pointer vers une méthode qui prend deux arguments de type (const void*) et retourne un int.

À l’intérieur des méthodes, tout comme en Java, le mot clé this est une référence vers l’objet pour lequel la méthode a été appelée. En C++, this est un pointeur, et pour accéder aux membres, on doit tout d’abord le déréférencer ou utiliser la notation -> (par exemple this->Print()).

Selon une étude de Microsoft2, 70% des bugs de sécurité sont liés à la gestion de la mémoire. Ce n’est pas facile de bien gérer la mémoire et l’absence de garbage collector dans C++ ne simplifie pas la tâche. Afin d’adresser cette problématique, la version C++11 propose le concept de Smart Pointer. Ces Smart Pointers sont des sortes de wrappers qui libèrent la mémoire allouée pour un pointeur de manière sûre. Ces Smart Pointers sont disponibles en important la bibliothèque <memory>.

On distingue 3 types de Smart Pointer :

  • Les unique_ptr
  • Les shared_ptr
  • Les weak_ptr

Un unique_ptr, comme son nom l’indique, est unique et ne peut pas être partagé. On ne peut pas le copier vers un autre unique_ptr et on ne peut pas le passer “par valeur” à une fonction. En d’autres termes, il ne peut y avoir qu’un seul propriétaire de ce pointeur.

void UseSmartPointer()
{
    unique_ptr<Point> p(new Point(12, 42));
    // ...
} // p is automatically deleted here.

Dès que le propriétaire disparaît, son destructeur libère la mémoire. On ne risque donc plus d’oublier le delete.

Si on souhaite avoir plusieurs propriétaires pour un pointeur, il faut utiliser des shared_ptr avec éventuellement des weak_ptr. Ces concepts sortent du cadre de cours d’introduction et le lecteur intéressé trouvera beaucoup d’information en ligne ou dans la littérature sur le sujet.


  1. Le C++ connaît aussi le concept de lambda comme proposé par Java, mais nous ne l’étudierons pas dans le cadre de ce cours. 

  2. https://youtu.be/PjbGojjnBZQ?t=835