Utilisez des pointeurs
Les adresses sont des nombres. Vous connaissez plusieurs types permettant de stocker des nombres : int
, unsigned int
, double
.
C'est possible de stocker une adresse dans une variable, mais pas avec les types que vous connaissez. Il nous faut utiliser un type un peu particulier : le pointeur.
Déclarez un pointeur
Pour déclarer un pointeur il faut, comme pour les variables, deux choses :
Un type.
Un nom.
Pour le nom, il n'y a rien de particulier à signaler. Les mêmes règles que pour les variables s'appliquent. Ouf !
Le type d'un pointeur a une petite subtilité. Il faut indiquer quel est le type de variable dont on veut stocker l'adresse, et ajouter une étoile *
.
Je crois qu'un exemple sera plus simple :
int *pointeur;
Ce code déclare un pointeur qui peut contenir l'adresse d'une variable de type int
.
On peut également écrire int* pointeur
(avec l'étoile collée au mot int
).
Mais cette notation a un léger inconvénient car elle ne permet pas de déclarer plusieurs pointeurs sur la même ligne, comme ceci : int* pointeur1, pointeur2, pointeur3;
.
Si l'on procède ainsi, seul pointeur1
sera un pointeur, les deux autres variables seront des entiers tout à fait standard.
On peut bien sûr faire cela pour n'importe quel type :
double *pointeurA;
//Un pointeur qui peut contenir l'adresse d'un nombre à virgule
unsigned int *pointeurB;
//Un pointeur qui peut contenir l'adresse d'un nombre entier positif
string *pointeurC;
//Un pointeur qui peut contenir l'adresse d'une chaîne de caractères
vector<int> *pointeurD;
//Un pointeur qui peut contenir l'adresse d'un tableau dynamique de nombres entiers
int const *pointeurE;
//Un pointeur qui peut contenir l'adresse d'un nombre entier constant
int *pointeur(0);
double *pointeurA(0);
unsigned int *pointeurB(0);
string *pointeurC(0);
vector<int> *pointeurD(0);
int const *pointeurE(0);
Vous l'avez peut-être remarqué sur mon schéma un peu plus tôt, la première case de la mémoire avait l'adresse 1. En effet, l'adresse 0 n'existe pas.
Lorsque vous créez un pointeur contenant l'adresse 0, cela signifie qu'il ne contient l'adresse d'aucune case.
Stockez une adresse
Maintenant qu'on a la variable, il n'y a plus qu'à y mettre une valeur. Vous savez déjà comment obtenir l'adresse d'une variable (rappelez-vous du &
). Allons-y :
int main()
{
int ageUtilisateur(16);
//Une variable de type int
int *ptr(0);
//Un pointeur pouvant contenir l'adresse d'un nombre entier
ptr = &ageUtilisateur;
//On met l'adresse de 'ageUtilisateur' dans le pointeur 'ptr'
return 0;
}
Voyons comment tout cela se déroule dans la mémoire grâce à un schéma :
On retrouve quelques éléments familiers : la mémoire avec sa grille de cases, et la variable ageUtilisateur
dans la case n°53768. La nouveauté est bien sûr le pointeur. Dans la case mémoire n°14566, il y a une variable nommée ptr
qui a pour valeur l'adresse 53768, c'est-à-dire l'adresse de la variable ageUtilisateur
.
Voilà, vous savez tout ou presque. Cela peut sembler absurde pour le moment :
Pourquoi stocker l'adresse d'une variable dans une autre case ?
… Mais faites-moi confiance : les choses vont progressivement s'éclaircir.
Affichez l'adresse
Comme pour toutes les variables, on peut afficher le contenu d'un pointeur :
#include <iostream>
using namespace std;
int main()
{
int ageUtilisateur(16);
int *ptr(0);
ptr = &ageUtilisateur;
cout << "L'adresse de 'ageUtilisateur' est : " << &ageUtilisateur << endl;
cout << "La valeur de pointeur est : " << ptr << endl;
return 0;
}
Cela donne :
L'adresse de 'ageUtilisateur' est : 0x2ff18 La valeur de pointeur est : 0x2ff18
La valeur du pointeur est bien l'adresse de la variable pointée : on a réussi à stocker une adresse !
Accédez à la valeur pointée
Vous vous souvenez du rôle des pointeurs ? Ils permettent d'accéder à une variable sans passer par son nom. Il faut utiliser l'étoile *
sur le pointeur pour afficher la valeur de la variable pointée :
int main()
{
int ageUtilisateur(16);
int *ptr(0);
ptr= &ageUtilisateur;
cout << "La valeur est : " << *ptr << endl;
return 0;
}
En faisant cout << *ptr
, le programme effectue les étapes suivantes :
Il va dans la case mémoire nommée
ptr
.Lit la valeur enregistrée.
"Suit la flèche" pour aller à l'adresse pointée.
Lit la valeur stockée dans la case.
Affiche cette valeur : ici, ce sera
16
.
Voici donc un deuxième moyen d'accéder à la valeur de ageUtilisateur
.
Mais à quoi ça sert ?
Je suis sûr que vous vous êtes retenu de poser la question avant. C'est vrai que cela a l'air assez inutile. Eh bien, je ne peux pas vous répondre rapidement pour le moment. Il va falloir lire la fin de ce chapitre pour tout savoir.
Souvenez-vous de la notation
Je suis d'accord avec vous, la notation est compliquée. L'étoile *
a deux significations différentes, et on utilise l'esperluette &
, alors qu'elle sert déjà pour les références… Ce n'est pas ma faute, mais il va falloir faire avec. Essayons donc de récapituler le tout.
Je vous invite à tester tout cela chez vous pour vérifier que vous avez bien compris comment afficher une adresse, comment utiliser un pointeur, etc.
Voici une petite vidéo pour résumer tous les points que nous avons vus :
"C'est en forgeant qu'on devient forgeron" dit le dicton.
Et bien, "c'est en programmant avec des pointeurs que l'on devient programmeur". 🤣
Il faut impérativement s'entraîner pour bien comprendre. Les meilleurs sont tous passés par là, et je peux vous assurer qu'ils ont aussi souffert en découvrant les pointeurs. Si vous ressentez une petite douleur dans la tête, faites une pause puis relisez ce que vous venez de lire, encore et encore. Aidez-vous en particulier des schémas !
Faites de l'allocation dynamique
Vous vouliez savoir à quoi servent les pointeurs ? Vous êtes sûr ? Bon, alors je vais vous montrer une première utilisation.
Tout ceci se faisait automatiquement, nous allons maintenant apprendre à le faire manuellement et pour cela… vous vous doutez sûrement que nous allons utiliser les pointeurs.
Allouez un espace mémoire
new
demande une case à l'ordinateur, et renvoie un pointeur pointant vers cette case :
int *pointeur(0);
pointeur = new int;
La deuxième ligne demande une case mémoire pouvant stocker un entier, et l'adresse de cette case est stockée dans le pointeur.
Le mieux est encore de faire appel à un petit schéma :
Ce schéma est très similaire au précédent. Il y a deux cases mémoire utilisées :
La case 14563 qui contient une variable de type
int
non initialisée.La case 53771 qui contient un pointeur pointant sur la variable.
Rien de neuf. Mais le point important, c'est que la variable dans la case 14563 n'a pas d'étiquette. Le seul moyen d'y accéder est donc de passer par le pointeur.
int *pointeur(0);
pointeur = new int;
*pointeur = 2; //On accède à la case mémoire pour en modifier la valeur
La case sans étiquette est maintenant remplie :
À part son accès un peu spécial (via *pointeur
), nous avons donc une variable en tout point semblable à une autre. Il faut maintenant rendre la mémoire que l'ordinateur nous a prêtée.
Libérez la mémoire
int *pointeur(0);
pointeur = new int;
delete pointeur; //On libère la case mémoire
La case est alors rendue à l'ordinateur qui pourra l'employer à autre chose. Le pointeur, lui, existe toujours et il pointe toujours sur la case, mais vous n'avez plus le droit de l'utiliser :
int *pointeur(0);
pointeur = new int;
delete pointeur; //On libère la case mémoire
pointeur = 0; //On indique que le pointeur ne pointe plus vers rien
Il y a plusieurs points à ne pas oublier lorsqu’on effectue une allocation dynamique ; c’est pour cette raison que je vous propose un screencast pour récapituler ce que l'on vient de voir :
Terminons cette section avec un exemple complet : un programme qui demande son âge à l'utilisateur, et qui l'affiche à l'aide un pointeur.
#include <iostream>
using namespace std;
int main()
{
int* pointeur(0);
pointeur = new int;
cout << "Quel est votre age ? ";
cin >> *pointeur;
//On écrit dans la case mémoire pointée par le pointeur 'pointeur'
cout << "Vous avez " << *pointeur << " ans." << endl;
//On utilise à nouveau *pointeur
delete pointeur; //Ne pas oublier de libérer la mémoire
pointeur = 0; //Et de faire pointer le pointeur vers rien
return 0;
}
Ce programme est plus compliqué que sa version sans allocation dynamique, c'est vrai ! Mais on a le contrôle complet sur l'allocation et la libération de la mémoire.
Utilisez des pointeurs dans les cas suivants
Je vous avais promis des explications sur quand utiliser des pointeurs. Les voici !
Il y a en réalité trois cas d'application :
Gérer soi-même le moment de la création et de la destruction des cases mémoire.
Partager une variable dans plusieurs morceaux du code.
Sélectionner une valeur parmi plusieurs options.
Si vous n'êtes dans aucun de ces trois cas, c'est très certainement que vous n'avez pas besoin des pointeurs.
Cas d'usage n° 1 : gérer la création et la destruction des cases mémoire
Vous connaissez déjà le premier de ces trois cas. Concentrons-nous sur les deux autres.
Cas d'usage n° 2 : partager une variable dans plusieurs morceaux de code
Programmer un jeu de stratégie du type Warcraft doit vous sembler très compliqué, mais vous pouvez quand même réfléchir à certains des mécanismes utilisés. Par exemple : chaque personnage a une cible précise. Comment feriez-vous pour indiquer, en C++, la cible d'un personnage ? Eh oui, un pointeur !
Chaque personnage possède un pointeur qui pointe vers sa cible. Il a ainsi un moyen de savoir qui viser et attaquer. On pourrait par exemple écrire quelque chose du type :
Personnage *cible; //Un pointeur qui pointe sur un autre personnage
Quand il n'y a pas de combat en cours, le pointeur pointe vers l'adresse 0, il n'a pas de cible.
Quand le combat est engagé, le pointeur pointe vers un ennemi.
Enfin, quand cet ennemi meurt, on déplace le pointeur vers une autre adresse, c'est-à-dire vers un autre personnage.
Le pointeur est donc réellement utilisé ici comme une flèche reliant un personnage à son ennemi.
Cas d'usage n° 3 : sélectionner une valeur parmi plusieurs options
Le troisième et dernier cas permet de faire évoluer un programme en fonction des choix de l'utilisateur.
Prenons le cas d'un QCM : nous allons demander à l'utilisateur de choisir parmi trois réponses possibles à une question. Une fois qu'il aura choisi, nous allons utiliser un pointeur pour indiquer quelle réponse a été sélectionnée.
#include <iostream>
#include <string>
using namespace std;
int main()
{
string reponseA, reponseB, reponseC;
reponseA = "La peur des jeux de loterie";
reponseB = "La peur du noir";
reponseC = "La peur des vendredis treize";
cout << "Qu'est-ce que la 'kenophobie' ? " << endl; //On pose la question
cout << "A) " << reponseA << endl; //Et on affiche les trois propositions
cout << "B) " << reponseB << endl;
cout << "C) " << reponseC << endl;
char reponse;
cout << "Votre reponse (A,B ou C) : ";
cin >> reponse; //On récupère la réponse de l'utilisateur
string *reponseUtilisateur(0); //Un pointeur qui pointera sur la réponse choisie
switch(reponse)
{
case 'A':
reponseUtilisateur = &reponseA; //On déplace le pointeur sur la réponse choisie
break;
case 'B':
reponseUtilisateur = &reponseB;
break;
case 'C':
reponseUtilisateur = &reponseC;
break;
}
//On peut alors utiliser le pointeur pour afficher la réponse choisie
cout << "Vous avez choisi la reponse : " << *reponseUtilisateur << endl;
return 0;
}
Une fois que le pointeur a été déplacé (dans le switch
), on peut l'utiliser comme moyen d'accès à la réponse de l'utilisateur. On a ainsi un moyen d'atteindre directement cette variable sans devoir refaire le test à chaque fois qu'on en a besoin. C'est une variable qui contient une valeur que l'on ne pouvait pas connaître avant (puisqu'elle dépend de ce que l'utilisateur a entré).
C'est certainement le cas d'utilisation le plus rare des trois, mais il arrive parfois qu'on soit dans cette situation. Il sera alors temps de vous rappeler les pointeurs !
En résumé
Chaque variable est stockée en mémoire à une adresse différente.
Il ne peut y avoir qu'une seule variable par adresse.
On peut récupérer l'adresse d'une variable avec le symbole
&
, comme ceci :&variable
.Un pointeur est une variable qui stocke l'adresse d'une autre variable.
Un pointeur se déclare comme ceci :
int *pointeur;
(dans le cas d'un pointeur vers une variable de typeint
).Par défaut, un pointeur affiche l'adresse qu'il contient. En revanche, si on écrit
*pointeur
, on obtient la valeur qui se trouve à l'adresse indiquée par le pointeur.On peut réserver manuellement une case en mémoire avec
new
. Dans ce cas, il faut libérer l'espace en mémoire dès qu'on n'en a plus besoin, avecdelete
.
Vous arrivez à la fin de ce cours, mais vous n'êtes pas au bout de votre apprentissage du C++… On se retrouve dans le dernier chapitre de ce cours pour voir comment aller plus loin !