Les pointeurs ! Enfin ! S'il y a bien une particularité en C++ (comme en C) c'est les pointeurs ! C'est sûrement la plus grande peur, le plus grand danger pour les développeurs débutants (et expérimentés aussi). Mais pourquoi donc ? Je pense que cette angoisse vient du fait que les pointeurs sont associés aux termes "gestion de la mémoire", "allocation dynamique de mémoire", "adresse mémoire", "programmation de bas niveau", etc... Bref, de bien jolis termes scientifiques qui, j'en suis certain, vous parlent ;)
Un pointeur: C'est un type un peu "spécial" de variable. En effet, un pointeur contient l'adresse d'une autre variable. Et pis c'est tout ! En comprenant ça, vous avez déjà compris 95% de ce qu'il y a à comprendre sur les pointeurs. Donc n'oubliez pas que quand on parle de pointeurs, on parle d'adresse. Ok ?!
- Ok mais à quoi ça sert d'avoir une variable qui contient l'adresse d'une autre variable ?Déclaration d'un pointeur: Lorque vous souhaitez utiliser un pointeur, vous devez obligatoirement spécifier le type de pointeur que vous désirez utiliser. Est-ce un pointeur vers un entier ? un pointeur vers un caractère ? un pointeur vers une structure ? Il faut le préciser au moment de la déclaration de votre pointeur. C'est primordial. D'ailleurs, à chaque fois que vous parlez de pointeurs, n'oubliez jamais de dire vers quel type votre pointeur pointe!
int* p;
Ici, je déclare une variable, nommée p, de type "pointeur vers un entier". Vous voyez, j'ai bien dit "pointeur vers un entier" et non "pointeur". Ça peut vous paraître être un détail mais c'est très important. Si vous voulez un jour maîtriser ces satanés pointeurs, il faut savoir exactement à qui on a à faire. Et ça passe par les nommer correctement !
Donc, p est une variable qui va contenir l'adresse d'un entier. J'ai bien dit "qui va" car pour le moment elle ne contient rien. Vous allez me demander : "Mais comment qu'on fait pour affecter à p l'adresse d'un entier ?" Eh bien, il existe un opérateur en C++ qui permet d'obtenir l'adresse d'une variable. Cet opérateur c'est & (ET commercial). Donc, il nous faut d'abord une variable de type entier puis grâce à l'opérateur & on va lui donner l'adresse de cet entier.
int i; int* p; p = &i;
Dans l'exemple ci-dessus, j'ai d'abord déclaré une variable i de type entier. Puis une variable p de type pointeur vers un entier et enfin j'ai affecté à p l'adresse de la variable i : "p = &i;". A partir de cette ligne, p contient l'adresse de i. Si vous affichez la valeur de p, vous aurez quelque chose du genre 0x28ff0c (sous Code::Blocks) ou encore 0035FD74 (sous Visual). Cette série de caractères bizarres représente une adresse :) Dans notre cas, c'est l'adresse de la variable i.
- Bon, c'est bien gentil mais l'adresse d'une variable ça va pas me servir à grand chose!
- C'est sûr ! Mais maintenant on va voir qu'à partir de l'adresse d'une variable, on va pouvoir modifier cette variable.
- Allons-y alors !
int i; int* p; p = &i; *p = 15;
Ici, on a affecté à i la valeurs 15 en passant par le pointeur p. Comme p pointe vers i (instruction "p = &i";), l'expression "*p" permet d'atteindre i et ainsi de modifier sa valeur.
On a donc vu comment déclarer un pointeur et comment, à partir d'un pointeur, modifier la valeur d'une variable pointée. Si vous avez compris ce début de cours, vous êtes sur la bonne voie ! On se fait un p'tit exercice avant de continuer ?
Essayez de deviner le résultat de l'exécution du programme ci-dessous :
int main() { int i = 20; int* p; p = &i; *p = 10; cout << *p << endl; return 0; }
10 Process returned 0 (0x0) execution time : 0.009 s Press any key to continue.
Pas de commentaires, tout est dans le cours.
Alors, c'est dur ?! Bon, on complique un peu...
Essayez de deviner le résultat de l'exécution du programme ci-dessous :
int main() { int i = 20; int* p; p = &i; *p = 10; cout << p << endl; return 0; }
0x28ff0c Process returned 0 (0x0) execution time : 0.016 s Press any key to continue.
Tous ceux qui s'étaient dit "Mais y a un problème ! Je ne connais pas l'adresse de i !" ont compris ce début de cours.
Pour les autres, je ré-explique : la variable p est de type "pointeur vers un entier". On lui donne l'adresse de la variable i
avec l'instruction "p = &i". Donc, à partir de cette ligne, p contient l'adresse de i. L'instruction "cout << p << endl;" va afficher
le contenu de la variable p. Elle va donc afficher l'adresse de i.
Dernier exercice de chauffe :
Completez le programme ci-dessous pour qu'il affiche l'adresse de la variable p :
int main() {
int i = 20;
int* p;
p = &i;
*p = 10;
// Compléter le programme ici
return 0;
}
int main() { int i = 20; int* p; p = &i; *p = 10; cout << &p << endl; return 0; }
Même si p est un pointeur, il reste une variable comme les autres ! Dès l'instant où vous le déclarez, votre programme va lui trouver une place en mémoire. Il a donc une adresse lui aussi. Dans ce cours, nous avons vu comment obtenir l'adresse d'une variable. Il faut utiliser l'opérateur &. L'instruction "cout << &p << endl;" va donc afficher l'adresse de la variable p.
- Mais si on peut obtenir l'adresse de p, on pourrait alors mettre cette adresse dans une autre variable de type... euh... pointeur vers un pointeur ?int main() { int i = 20; int* p; p = &i; *p = 10; cout << &p << endl; int** pp; pp = &p; cout << pp << endl; return 0; }
La variable pp est déclarée en tant que "pointeur vers un pointeur vers un entier" (instruction "int** p;"). Cela signifie que pp contient l'adresse d'un pointeur vers un entier. Dans notre cas pp contient l'adresse du pointeur p (qui lui même contient l'adresse de i). Vous suivez ? Afficher pp ou &p revient donc à la même chose : afficher l'adresse de p. Essayez le programme ci-dessus !
Retour au cours !
- Vous connaissez sûrement les tableaux. Mais savez-vous que l'identificateur d'un tableau (son nom quoi) est un pointeur ?
- Euh non... mais tu n'as pas dit qu'il fallait toujours préciser un pointeur vers quoi ?
- En effet. Pour les tableaux, ça dépend de quel type sont les éléments du tableau en question. S'il s'agit d'un tableau d'entiers alors
l'identificateur de ce tableau est un pointeur vers un entier. S'il s'agit d'un tableau de réels alors l'identificateur du tableau est un
pointeur vers un réel. Facile non ?!
- Et c'est tout ?
- Non :( Il faut préciser que l'identificateur d'un tableau est un pointeur vers le premier élément du tableau.
- Wahouah ! Alors si je comprends bien, il contient l'adresse du premier élément du tableau. C'est bien ça ?
- Corrrrrrrrrect !
Un exemple pour résumer tout ça.
Soit le programme suivant :
int main() { int tab[4] = {15, 2, 43, 7}; cout << "tab = " << tab << endl << endl; for(int i=0; i<4; i++) { cout << "&tab[" << i << "] = " << &tab[i] << endl; } return 0; }
Le résultat de ce programme est :
tab = 0x28ff00 &tab[0] = 0x28ff00 &tab[1] = 0x28ff04 &tab[2] = 0x28ff08 &tab[3] = 0x28ff0c Process returned 0 (0x0) execution time : 0.017 s Press any key to continue.
Et voilà un schémas simplifié de la mémoire lors de l'exécution de ce programme afin que vous compreniez mieux ce qu'il se passe en mémoire :
Deux remarques :
Continuons avec nos tableaux et nos pointeurs.
Si l'identificateur d'un tableau peut être considéré comme un pointeur (un pointeur constant en réalité), nous pouvons écrire le programme suivant:
int main() { int tab[4] = {15, 2, 43, 7}; int* pi; pi = tab; cout << "*pi = " << *pi << endl << endl; for(int i=0; i<4; i++) { cout << "tab[" << i << "] = " << tab[i] << endl; } return 0; }
Le résultat de ce programme est :
*pi = 15 tab[0] = 15 tab[1] = 2 tab[2] = 43 tab[3] = 7 Process returned 0 (0x0) execution time : 0.008 s Press any key to continue.
Que remarquez-vous ? Les expressions "*pi" et "tab[0]" renvoient le même résultat. Pourquoi ? Parce que la variable pi est de type pointeur vers un entier (je vous rappelle qu'un pointeur contient l'adresse d'une variable). Cette variable est initialisée avec la valeur de tab (instruction "pi = tab"). Que vaut tab ? Eh bien comme on l'a dit plus haut, tab étant un tableau, tab contient l'adresse du premier élément du tableau. Donc, après cette affectation, pi contient aussi l'adresse du premier élément du tableau. Que signifie "*pi" ? Cela signifie "le contenu de la case pointée par pi". Et quelle est la case (mémoire) pointée par pi ? C'est le premier élément du tableau tab ! Donc "*pi" signifie le contenu du premier élément du tableau. C'est à dire tab[0]. Compris ?
Arithmétique des pointeurs.
Il est possible de faire des opérations de base sur les pointeurs. Vous pouvez en effet additionner ou soustraire deux pointeurs, incrémenter ou encore décrémenter un même pointeur. Voyons un exemple :
int main() { int tab[4] = {15, 2, 43, 7}; int* p1; int* p2; p1 = tab; // p1 prend l'adresse du premier élément de tab. C'est à dire &tab[0] cout << "*p1 = " << *p1 << endl; // Affiche le contenu de la case pointée par p1. C'est à dire tab[0] p1++; // Incrémentation de p1. Maintenant p1 contient l'adresse de l'entier qui suit tab[0]. C'est à dire &tab[1] cout << "*p1 = " << *p1 << endl; // Affiche le contenu de la case pointée par p1. C'est à dire tab[1] p1--; cout << "*p1 = " << *p1 << endl; p1 = p1 + 2; cout << "*p1 = " << *p1 << endl; p2 = p1 - 1; // p2 va contenir l'adresse de l'entier rangé juste avant l'entier pointé par p1. C'est à dire &tab[1] cout << "*p2 = " << *p2 << endl; return 0; }
Le résultat de ce programme est :
*p1 = 15 *p1 = 2 *p1 = 15 *p1 = 43 *p2 = 2 Process returned 0 (0x0) execution time : 0.029 s Press any key to continue.
return index;