• 8 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

Ce cours existe en livre papier.

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 17/10/2022

Déclarez les pointeurs

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 :

  1. Un type.

  2. 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 :

La mémoire après la déclaration d'une variable et d'un pointeur pointant sur cette variable
La mémoire après la déclaration d'une variable et d'un pointeur pointant sur cette variable

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 :

  1. Il va dans la case mémoire nommée ptr  .

  2. Lit la valeur enregistrée.

  3. "Suit la flèche" pour aller à l'adresse pointée.

  4. Lit la valeur stockée dans la case.

  5. 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 :

La mémoire après l'allocation dynamique d'un entier
La mémoire après l'allocation dynamique d'un entier

Ce schéma est très similaire au précédent. Il y a deux cases mémoire utilisées :

  1. La case 14563 qui contient une variable de type int non initialisée.

  2. 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 :

La mémoire après avoir alloué une variable et changé la valeur de cette variable
La mémoire après avoir alloué une variable et changé la valeur de cette variable

À 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 :

Un pointeur pointant sur une case vide après un appel à delete
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 :

  1. Gérer soi-même le moment de la création et de la destruction des cases mémoire.

  2. Partager une variable dans plusieurs morceaux du code.

  3. 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 type int  ).

  • 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, avec delete  .

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 !

Exemple de certificat de réussite
Exemple de certificat de réussite