• 50 heures
  • Difficile

Ce cours est visible gratuitement en ligne.

Ce cours existe en livre papier.

Vous pouvez obtenir un certificat de réussite à l'issue de ce cours.

Vous pouvez être accompagné et mentoré par un professeur particulier par visioconférence sur ce cours.

J'ai tout compris !

Mis à jour le 25/03/2019

Déclarez les pointeurs

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

:Nous voilà dans le dernier chapitre de présentation des bases du C++. Accrochez-vous car le niveau monte d'un cran !
Le sujet des pointeurs fait peur à beaucoup de monde et c'est certainement un des chapitres les plus complexes de ce cours. Une fois cet écueil passé, beaucoup de choses vous paraîtront plus simples et plus claires.

Les pointeurs sont utilisés dans tous les programmes C++, même si vous n'en avez pas eu conscience jusque là. Il y en a partout. Pour l'instant, ils étaient cachés et vous n'avez pas eu à en manipuler directement. Cela va changer avec ce chapitre. Nous allons apprendre à gérer très finement ce qui se passe dans la mémoire de l'ordinateur.

C'est un chapitre plutôt difficile, il est donc normal que vous ne compreniez pas tout du premier coup. N'ayez pas peur de le relire une seconde fois dans quelques jours pour vous assurer que vous avez bien tout assimilé !

Une question d'adresse

Est-ce que vous vous rappelez le chapitre sur la mémoire ? Oui, celui qui présentait la notion de variable. Je vous invite à le relire et surtout à vous remémorer les différents schémas.

Je vous avais dit que, lorsque l'on déclare une variable, l'ordinateur nous « prête » une place dans sa mémoire et y accroche une étiquette portant le nom de la variable.

int main()
{
    int ageUtilisateur(16);
    return 0;
}

On pouvait donc représenter la mémoire utilisée dans ce programme comme sur le schéma :

La mémoire lorsque l'on déclare une variable
La mémoire lorsque l'on déclare une variable

C'était simple et beau. Malheureusement, je vous ai un peu menti. Je vous ai simplifié les choses !
Vous commencez à le savoir, dans un ordinateur tout est bien ordonné et rangé de manière logique. Le système des étiquettes dont je vous ai parlé n'est donc pas tout à fait correct.

La mémoire d'un ordinateur est réellement constituée de « cases », là je ne vous ai pas menti. Il y en a même énormément, plusieurs milliards sur un ordinateur récent ! Il faut donc un système pour que l'ordinateur puisse retrouver les cases dont il a besoin. Chacune d'entre elles possède un numéro unique, son adresse :

Le vrai visage de la mémoire : beaucoup de cases, toutes numérotées
Le vrai visage de la mémoire : beaucoup de cases, toutes numérotées

Sur le schéma, on voit cette fois toutes les cases de la mémoire avec leurs adresses. Notre programme utilise une seule de ces cases, la 53768, pour y stocker sa variable.

Le point important ici est que chaque variable possède une seule adresse et que chaque adresse correspond à une seule variable.

L'adresse est donc un deuxième moyen d'accéder à une variable. On peut atteindre la case jaune du schéma par deux chemins différents :

  • On peut passer par son nom (l'étiquette) comme on sait déjà le faire…

  • Mais on peut aussi accéder à la variable grâce à son adresse (son numéro de case). On pourrait alors dire à l'ordinateur « Affiche moi le contenu de l'adresse 53768 » ou encore « Additionne les contenus des adresses 1267 et 91238 ».

Est-ce que cela vous tente d'essayer ? Vous vous demandez peut-être à quoi cela peut bien servir. Utiliser l'étiquette était un moyen simple et efficace, c'est vrai. Mais nous verrons plus loin que passer par les adresses est parfois nécessaire.

Commençons par voir comment connaître l'adresse d'une variable.

Affichez l'adresse

En C++, le symbole pour obtenir l'adresse d'une variable est l'esperluette (&). Si je veux afficher l'adresse de la variableageUtilisateur, je dois donc écrire&ageUtilisateur. Essayons.

#include <iostream>
using namespace std;

int main()
{
   int ageUtilisateur(16);
   cout << "L'adresse est : " << &ageUtilisateur << endl;
   //Affichage de l'adresse de la variable
   return 0;
}

Chez moi, j'obtiens le résultat suivant :

L'adresse est : 0x22ff1c

Même si elle contient des lettres, cette adresse est un nombre. Celui-ci est simplement écrit en hexadécimal (en base 16, si vous voulez tout savoir), une autre façon d'écrire les nombres. Les ordinateurs aiment bien travailler dans cette base. Pour information, en base 10 (notre écriture courante), cette adresse correspond à 2 293 532. Cependant, ce n'est pas une information très intéressante.

Ce qui est sûr, c'est qu'afficher une adresse est très rarement utile. Souvenez-vous simplement de la notation. L'esperluette veut dire « adresse de ». Donccout << &a;se traduit en français par « Affiche l'adresse de la variable a ».

Voyons maintenant ce que l'on peut faire avec ces adresses.

Les pointeurs

Les adresses sont des nombres. Vous connaissez plusieurs types permettant de stocker des nombres :int,unsigned int,double. Peut-on donc stocker une adresse dans une variable ?

La réponse est « oui ». C'est possible, mais pas avec les types que vous connaissez. Il nous faut utiliser un type un peu particulier : le pointeur.

Un pointeur est une variable qui contient l'adresse d'une autre variable.

Retenez bien cette phrase. Elle peut vous sauver la vie dans les moments les plus difficiles de ce chapitre.

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

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

Pour le moment, ces pointeurs ne contiennent aucune adresse connue. C'est une situation très dangereuse. Si vous essayez d'utiliser le pointeur, vous ne savez pas quelle case de la mémoire vous manipulez. Ce peut être n'importe quelle case, par exemple celle qui contient votre mot de passe Windows ou celle stockant l'heure actuelle. J'imagine que vous vous rendez compte des conséquences que peut avoir une mauvaise manipulation des pointeurs. Il ne faut donc jamais déclarer un pointeur sans lui donner d'adresse.

Par conséquent, pour être tranquille, il faut toujours déclarer un pointeur en lui donnant la valeur 0 :

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;
}

La ligneptr = &ageUtilisateur;est celle qui nous intéresse. Elle écrit l'adresse de la variableageUtilisateurdans le pointeurptr. On dit alors que le pointeurptrpointe surageUtilisateur.

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 variableageUtilisateurdans 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éeptrqui a pour valeur l'adresse 53768, c'est-à-dire l'adresse de la variableageUtilisateur.

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'éclairer pour vous.
Si vous avez compris le schéma précédent, alors vous pouvez vous attaquer aux programmes les plus complexes.

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 donc bien l'adresse de la variable pointée. On a bien 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. Voici comment faire : 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 faisantcout << *ptr, le programme effectue les étapes suivantes :

  1. Aller dans la case mémoire nomméeptr;

  2. Lire la valeur enregistrée ;

  3. « Suivre la flèche » pour aller à l'adresse pointée ;

  4. Lire la valeur stockée dans la case ;

  5. Afficher cette valeur : ici, ce sera bien sûr 16.

En utilisant l'étoile, on accède à la valeur de la variable pointée. C'est ce qui s'appelle déréférencer un pointeur.
Voici donc un deuxième moyen d'accéder à la valeur deageUtilisateur.

Mais à quoi cela sert-il ?

Je suis sûr que vous vous êtes retenus 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.

Récapitulatif 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.

Pour une variableint nombre:

  • nombrepermet d'accéder à la valeur de la variable ;

  • &nombrepermet d'accéder à l'adresse de la variable.

Sur un pointeurint *pointeur:

  • pointeurpermet d'accéder à la valeur du pointeur, c'est-à-dire à l'adresse de la variable pointée ;

  • *pointeurpermet d'accéder à la valeur de la variable pointée.

C'est ce qu'il faut retenir de cette section. 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.

« C'est en forgeant qu'on devient forgeron » dit le dicton, eh 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, prenez un cachet d'aspirine, faites une pause puis relisez ce que vous venez de lire, encore et encore. Aidez-vous en particulier des schémas !

L'allocation dynamique

Vous vouliez savoir à quoi servent les pointeurs ? Vous êtes sûrs ? Bon, alors je vous montrer une première utilisation.

La gestion automatique de la mémoire

Dans notre tout premier chapitre sur les variables, je vous avais expliqué que, lors de la déclaration d'une variable, le programme effectue deux étapes :

  1. Il demande à l'ordinateur de lui fournir une zone dans la mémoire. En termes techniques, on parle d'allocation de la mémoire.

  2. Il remplit cette case avec la valeur fournie. On parle alors d'initialisation de la variable.

Tout cela est entièrement automatique, le programme se débrouille tout seul. De même, lorsque l'on arrive à la fin d'une fonction, le programme rend la mémoire utilisée à l'ordinateur. C'est ce qu'on appelle la libération de la mémoire. C'est à nouveau automatique : nous n'avons jamais dû dire à l'ordinateur : « Tiens, reprends cette case mémoire, je n'en ai plus besoin ».

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

Pour demander manuellement une case dans la mémoire, il faut utiliser l'opérateurnew.
newdemande 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émoires utilisées :

  • la case 14563 qui contient une variable de typeintnon 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.

Une fois allouée manuellement, la variable s'utilise comme n'importe quelle autre. On doit juste se rappeler qu'il faut y accéder par le pointeur, en le déréférençant.

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 est donc dans l'état présenté :

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 nous faut maintenant rendre la mémoire que l'ordinateur nous a gentiment prêtée.

Libérez la mémoire

Une fois que l'on n'a plus besoin de la case mémoire, il faut la rendre à l'ordinateur. Cela se fait via l'opérateurdelete.

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 (figure suivante).

Un pointeur pointant sur une case vide après un appel à delete

L'image est très parlante. Si l'on suit la flèche, on arrive sur une case qui ne nous appartient pas. Il faut donc impérativement empêcher cela. Imaginez que cette case soit soudainement utilisée par un autre programme ! Vous risqueriez de modifier les variables de cet autre programme.
Après avoir fait appel àdelete, il est donc essentiel de supprimer cette « flèche » en mettant le pointeur à l'adresse 0. Ne pas le faire est une cause très courante de plantage des programmes.

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

Un exemple complet

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.

Dans la plupart des cas, ce n'est pas utile de le faire. Mais vous verrez plus tard que, pour faire des fenêtres, la bibliothèque Qt utilise beaucoupnewetdelete. On peut ainsi maîtriser précisément quand une fenêtre est ouverte et quand on la referme, par exemple.

Quand utiliser des pointeurs

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.

Vous connaissez déjà le premier de ces trois cas. Concentrons nous sur les deux autres.

Partagez une variable

Pour l'instant, je ne peux pas vous donner un code source complet pour ce cas d'utilisation. Ou alors, il ne sera pas intéressant du tout. Quand vous aurez quelques notions de programmation orientée objet, vous aurez de vrais exemples.

En attendant, je vous propose un exemple plus… visuel.

Vous avez déjà joué à un jeu de stratégie ? Prenons un exemple tiré d'un jeu de ce genre. Voici une image issue du fameux Warcraft III (figure suivante).

Le jeu Warcraft III
Le jeu Warcraft III

Programmer un tel jeu est bien sûr très compliqué mais on peut quand même réfléchir à certains des mécanismes utilisés. Sur l'image, on voit des humains (en rouge) attaquer des orcs (en bleu). Chaque personnage a une cible précise. Par exemple, le fusilier au milieu de l'écran semble tirer sur le gros personnage bleu qui tient une hache.

Nous verrons dans la suite de ce cours comment créer des objets, c'est-à-dire des variables plus évoluées (par exemple une variable de type « personnage », de type « orc » ou encore de type « bâtiment »). Bref, chaque élément du jeu pourra être modélisé en C++ par un objet.

Comment feriez-vous pour indiquer, en C++, la cible du personnage rouge ?
Bien sûr, vous ne savez pas encore comment faire en détail mais vous avez peut-être une petite idée. Rappelez-vous le titre de ce chapitre.
Oui oui, un pointeur est une bonne solution ! 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.

Nous verrons par la suite comment écrire ce type de code ; je crois même que créer un mini-RPG (un mini jeu de rôle, si vous préférez) sera le thème principal des chapitres de la partie II. Mais chut, c'est pour plus tard. ;)

Choisissez parmi plusieurs éléments

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 leswitch), 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 avecnew. Dans ce cas, il faut libérer l'espace en mémoire dès qu'on n'en a plus besoin, avecdelete.

  • Les pointeurs sont une notion complexe à saisir du premier coup. N'hésitez pas à relire ce chapitre plusieurs fois. Vous comprendrez mieux leur intérêt plus loin dans cet ouvrage.

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