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.
-
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. ↩