Les pointeurs
===
###### tags: `algo` `bourg` `c++` `iut` `cm`
Le cours en deux vidéos:
{%youtube ZHouWKj07UQ %}
{%youtube tW-FggMtzrY %}
## Introduction
Pour l'instant, vous n'avez rencontré que le concept d'allocation *statique* de mémoire. En termes simples, vous allouez de la mémoire *statiquement* dès lors que vous déclarez une variable. L'espace mémoire pour votre variable est donné lors de la déclaration, et libéré à la fin de la zone de portée (le bloc) de la variable. Cela signifie que jusqu'à présent, la mémoire est gérée automatiquement par votre programme. Ceci rend actuellement impossible certaines opérations:
- On est obligé de se fixer des limites pour les tailles de tableaux, et on ne peut pas changer leur taille en cours de programme.
- Quand on a besoin d'un très grand nombre de variables, on aimerait libérer la mémoire à notre guise pour que la mémoire de notre machine ne soit pas saturée.
- On peut difficilement faire des structures de données évoluées comme les listes chaînées, les arbres ou les graphes.
Tous ces problèmes trouvent une solution dans l'*allocation dynamique*. Celle-ci consiste à demander au système de réserver/libérer de la place mémoire disponible pour le programme quand on veut. Les fonctions réalisant ces opérations s'appellent ``new`` et ``delete``. Si elles permettent d'allouer de la mémoire (et donc des variables) dynamiquement, leur type de retour est nouveau pour vous, puisqu'il s'agit d'une *adresse mémoire*. Autrement dit, toutes vos variables alloués dynamiquement seront accessibles à partir de leur adresse mémoire, et non pas à partir de leur nom (comme c'était le cas jusqu'à présent avec vos variables statiques). Bien entendu, ces adresses mémoires doivent elles-mêmes être stockées en mémoire dans des variables que l'on appelera **pointeurs**. Pour cette raison, la maitrise des pointeurs est la première chose à apprendre ; l'allocation dynamique viendra ensuite au semestre 2.
## Fondamentaux sur les pointeurs
Un pointeur est donc une variable destinée à accueillir l'adresse mémoire d'une autre variable.
:::info
C'est la première chose à retenir !! Un pointeur est un nouveau type de variable, qui correspond aux adresses mémoires.
:::
Pour déclarer un pointeur, on a besoin de connaitre le type de la variable dont on veut stocker l'adresse mémoire. Pour différencier une variable de type pointeur de tous les autres types de variables rencontrées jusqu'à maintenant, on va ajouter le symbole ``*`` avant le nom du pointeur. Par exemple, je veux déclarer un pointeur "sur" un entier (càd un pointeur censé accueillir l'adresse mémoire d'un entier). Alors j'écrirai
````C++
int* pt_entier;
````
Ici la variable ``pt_entier`` est de type ``int*``, autrement dit un pointeur sur un entier, et devra donc contenir des adresses mémoires de variables entières.
Si je veux déclarer un pointeur sur une structure ``Personne``, alors j'écrirai de la même façon ``Personne* pt_pers``.
Ensuite, si je veux pouvoir initialiser et modifier mes variables pointeurs, j'ai besoin de savoir comment accéder à l'adresse mémoire d'une variable existante. L'opérateur qui nous permet de connaitre l'adresse mémoire d'une variable est le ``&``. On l'utilise le faisant suivre de la variable dont on veut connaitre l'adresse. Par exemple, si ``a`` est une variable de type quelconque (entier, réel, structure...), alors ``&a`` correspond à son adresse mémoire.
:::danger
Attention à ne pas confondre le symbole ``&`` dans le cadre d'une entête de fonction, où il indique un passage de paramètre modifiable - autrement dit une référence - et ce même symbole utilisé devant une variable, où il correspond à son adresse mémoire.
:::
Maintenant qu'on sait accéder à l'adresse mémoire d'une variable, on peut initialiser nos pointeurs:
````C++
int a=12;
int* pt=&a; //affectation cohérente: pt est du type pointeur sur un entier, et &a est l'adresse mémoire d'un entier.
````
:::warning
Le type de pointeur est important. Par ex le code suivant:
````C++
float f=13.45;
int* pt=&f; //ne compile pas
````
ne compile pas car ``&f`` est l'adresse mémoire d'un float et pas d'un int. Autrement dit, ``&f`` est de type ``float*`` et pas ``int*``.
:::
Vous savez qu'une règle principale lorsqu'on déclare des variables, c'est de les initialiser au plus vite pour éviter les erreurs. Dans le cadre de variables numériques ou de chaines de caractères, c'est plutot simple (souvent vous mettez $0$, ou encore la chaine vide...). Mais comment initialiser un pointeur avec une valeur par défaut si on ne connait pas d'adresse mémoire d'autre variable ? Une valeur est là pour nous aider: le pointeur nul, noté ``NULL``. Il est valable quel que soit le type de pointeur. Cela permet de dire que votre pointeur ne contient l'adresse mémoire d'aucune variable pour le moment. Si vous ne l'initialisez pas à ``NULL``, alors le compilateur donnera à votre pointeur une adresse mémoire aléatoire (càd ne correspondant à aucune variable de votre programme), et les erreurs d'exécution risquent de survenir rapidement. Donc prenez l'habitude d'intialiser vos pointeurs à ``NULL`` après déclaration. Exemple: ``double* pt=NULL``.
L'intérêt d'un pointeur est de pouvoir accéder/modifier à une variable sans passer par son nom. Etant donné une pointeur ``pt`` qui contient l'adresse mémoire d'une variable, l'opérateur ``*`` suivi du pointeur correspond à la variable pointée par ``pt``. Exemple:
````C++=
int a=3.2;
int* pt=&a;
*pt=2.7; //équivalent à a=2.7
cout << a; //affiche 2.7
````
A la ligne 3, ``*pt`` correspond à la variable pointée par ``pt``, il s'agit de ``a`` (car ``pt`` contient l'adresse mémoire de ``a``). Donc la ligne 3 est équivalente à ``a=2.7``. On a donc modifié la variable ``a`` sans passer par son nom ! C'est là tout l'intérêt des pointeurs ; cela sera encore plus puissant dans le cadre de l'allocation dynamique, puisque les variables alloués dynamiquement n'auront pas de nom. Elles seront uniquement identifiées via leurs adresses mémoires stockées dans des pointeurs.
:::warning
Ne confondez pas ``*`` et ``&`` !! Le symbole ``*`` doit TOUJOURS être suivi d'une variable de type pointeur, alors que ``&`` suivi d'une variable correspond à l'adresse mémoire de cette variable.
:::
:::warning
Si ``pt`` contient le pointeur nul ou bien une adresse mémoire ne correspondant à aucune variable de votre programme, alors l'appel à ``*pt`` provoquera une erreur d'exécution.
:::
## Pointeurs et tableaux 1D
Les pointeurs ont d'autres intérêt que l'allocation dynamique. Le premier est la gestion des tableaux. Depuis le début du cours, on vous a légérèment menti, le type tableau n'existe pas vraiment.

En fait, un tableau est un pointeur !! D'ailleurs, suite à une déclaration du genre ``int tab[10]``, vous avez jusqu'à présent toujours travaillé avec les cases ``tab[i]``. Mais essayez maintenant d'afficher la valeur ``tab``. Vous verrez que le programme affiche une adresse mémoire ! Donc le nom du tableau ``tab`` est de type ``int*``. Plus précisément, ``tab`` est l'adresse mémoire de la première case du tableau, soit ``tab[0]``. Et comme les cases d'un tableau sont contigües dans la mémoire de l'ordinateur, ``tab+1`` correspond à l'adresse mémoire de l'entier ``tab[1]``, ``tab+2`` à l'adresse de ``tab[2]``... Autrement dit, on a les propriétés suivantes:
````C++
tab=&tab[0]
tab+i=&tab[i] pour tout indice i du tableau
*tab=tab[0]
*(tab+i)=tab[i]pour tout indice i du tableau
````
Si vous avez bien compris ce qui précède, les lignes suivantes doivent donc vous paraitre cohérentes:
````C++
int tab[]={1,5,7};
int* pt;
pt=tab; //autorisé car pt et tab sont de même type, à savoir int* !
pt[2]=-8; //autorisé, équivalent à tab[2]=-8
cout << tab[2]; // affiche -8
````
Dans le code qui précède, le pointeur ``pt`` est en quelque sorte un alias du tableau ``tab``. Ces deux variables pointent en effet vers le même emplacement mémoire (à savoir la case ``tab[0]``). On a donc le droit d'utiliser l'opérateur ``[]`` après ``pt``.
Maintenant que vous savez que le nom d'un tableau 1D est homogène à un pointeur sur la première case du tableau, vous ne serez pas surpris de voir que les entêtes de fonctions avec des tableaux passés en paramètres peuvent aussi s'écrire avec une notation pointeur. C'est d'ailleurs le choix le plus fréquent des programmeurs C++. Concrètement, cela signifie par exemple que les deux entêtes ci-dessous sont équivalentes:
````C++
int ma_fonction(int tableau[]);
int ma_fonction(int * tableau);
````
En d'autres termes, **passer un tableau en paramètre à une fonction, c'est équivalent à passer l'adresse de la première case de ce tableau**.
Un autre exemple pour les chaines de caractères: les entêtes "officielles" des fonctions bien connues de la librairie ``cstring`` sont
````C++
int strlen(char* ch);
int strcmp(char* ch1, char* ch2);
void strcpy(char* dest, char* source);
````
:::warning
Comme vous le remarquez, le notation pointeur pour un tableau passé en paramère ignore complètement la taille du tableau. C'est pour cette raison que bien souvent, on choisit de passer la taille également en paramètre. Notez qu'on ne le fera pas pour les chaines de caractères, grâce à la présence du marqueur de fin de chaine ``\0``. Voici un exemple pour une fonction de tri de tableau d'entiers:
````C++
void tri_tableau(int * tableau, int taille); //entete
int main(){
int tab[12];
...
tri_tableau(tab,12); //appel de fonction
...
````
:::
## Passage de paramètre par adresse
Jusqu'à présent, vous avez vu deux types de passage de paramètre: le passage par valeur (pour les paramètres non modifiables), et le passage par référence (pour les paramètres modifiés par la fonction). Or il se trouve qu'il existe un troisième type de passage de paramètre, le passage par adresse. Il est très utilisé en langage C car le passage par référence n'y existe pas. Dans votre cas, il est important de le connaitre et de savoir le manipuler car nous allons utiliser en TP une librairie graphique codée en langage C, et qui utilise donc ce type de passage de paramètre. C'est en quelque sorte une alternative au passage par référence, pour traiter des paramètres modifiables.
En quoi cela consiste-t-il ? Dans les deux types de passage vus jusqu'à présent, on passait directement les variables en paramètres:
- Dans le cas du passage par valeur, la variable passée en paramètre est copiée. C'est pour cette raison que ce type de passage ne permet pas de modifier les variables à l'intérieur de la fonction.
- Dans le passage par référence, on passe un alias de la variable en paramètre. C'est pour cette raison que ce type de passage permet à la fonction de modifier la variable en question.
Dans le cas du passage par adresse, on va passer l'adresse mémoire de la variable en paramètre. Comme les adresses mémoires sont stockées dans des variables de type pointeur, cela signifie que nos entêtes de fonctions vont contenir des paramètres pointeurs vers les variables que l'on va passer. Prenons par exemple une fonction qui échange deux variables entières. Avec un passage par référence, on a le code suivant qui vous est familier:
````C++
void echange(int& a, int& b){
int tmp;
tmp=a;
a=b;
b=tmp;
}
int main(){
int i=3,j=7;
echange(i,j); //appel à la fonction echange
...
}
````
Si l'on choisit de coder cette fonction via un passage par adresse, alors la syntaxe devient la suivante:
````C++=
void echange(int* a, int* b){//on met des paramètres pointeurs pour les
//variables modifiées par la fonction
int tmp;
tmp=*a;
*a=*b;
*b=tmp;
}
int main(){
int i=3,j=7;
echange(&i,&j); //appel à la fonction echange avec les adresses
//des variables à modifier
...
}
````
Quant vous regardez le code ci-dessus, il est important que vous compreniez les différences avec le passage par référence ou par valeur:
- L'entête des fonctions utilise un pointeur pour chaque variable que la fonction doit modifier.
- Par conséquent, quand on appelle ce genre de fonction, pour chaque paramètre pointeur, on doit passer l'adresse mémoire de la variable correspondante. Dans le code qui précède, on veut échanger les variables ``i`` et ``j``, on passe donc leur adresse mémoire en paramètre au moment de l'appel (ligne 10). C'est cohérent, car ``&i`` et ``&j`` sont bien du type ``int*``.
- Dans le corps de la fonction, comme ``a`` et ``b`` correspondent aux adresses mémoires de ``i`` et ``j``, si l'on veut échanger ``i`` et ``j``, il faut utiliser l'opérateur ``*`` pour y accéder. En effet, pour rappel, comme ``a=&i`` et ``b=&j``, alors ``*a`` et ``*b`` correspondent respectivement à ``i`` et ``j`` (lignes 4 à 6).
:::warning
Notez bien que le passage par adresse est fréquemment utilisé en langage C (dans une moindre mesure en C++) pour passer des paramètres modifiables à une fonction, ou bien pour éviter des copies de paramètres lourds comme des structures.
:::
## Pointeurs et structures
Une nouvelle notation intervient pour les structures. Elle apparaît notamment dans les corps des fonctions, car le passage de structure par adresse est fréquent pour éviter des copies de variables. Imaginons la structure ``Personne`` et une fonction qui l'initialise:
````C++
struct Personne{
char nom[30];
int age;
}
void init(Personne* p, char n[],int a){
(*p).age=a;
strcpy((*p).nom,n);
}
````
Le code ci-dessus ne doit pas vous surprendre si vous avez bien compris le paragraphe précédent. Mais il est un peu lourd: en effet à chaque fois qu'on veut accéder aux champs d'une structure qui est elle-même désignée par un pointeur, on a tout d'abord besoin d'appliquer l'opérateur ``*`` pour récupérer la structure pointée, puis l'opérateur ``.`` pour accéder aux champs. Cette opération peut se condenser grâce à l'opérateur ``->``, qui correspond exactement à la succession de ``*`` puis ``.``. Concrètement, la fonction ``init`` ci-dessus s'écrira plus élégamment:
````C++
void init(Personne* p, char n[],int a){
p->age=a;
strcpy(p->nom,n);
}
````
:::danger
L'opérateur ``->`` doit toujours être précédé d'une variable de type pointeur sur structure. Bien souvent, avant de l'appeler, on demande aussi un test de non nullité du pointeur, afin d'éviter les erreurs à l'exécution.
:::