Site de Jean-Michel RICHER

Maître de Conférences en Informatique à l'Université d'Angers

1. Algorithmique 4 - Pointeurs

1.1. Qu'est-ce qu'un pointeur ?

Définition :

Un pointeur est une variable qui contient l'adresse d'une autre variable en mémoire.

Même si les langages de programmation permettent de manipuler les variables grâce à leur nom, ces mêmes variables sont représentées et accédées par le microprocesseur au travers de leur adresse.

Par exemple, lorsque l'on écrit en langage C :

Afficher le code    ens/l1/algo4/algo4_ptr_prog1.cpp
  1. // déclaration d'une variable entière nommée x
  2. int x;
  3.  
  4. // affectation de la valeur 1 à la variable x
  5. x = 1;
  6.  

le codage en assembleur du morceau de code précédent est :

Afficher le code    ens/l1/algo4/algo4_ptr_prog2.asm
  1. ; déclaration des données
  2. section .data
  3.  
  4.     x:  dd 0
  5.    
  6. ; code 
  7. section .text
  8.  
  9.     mov     dword [x], 1
  10.  
  11.  

On déclare une variable $x$ de type dd (double word), c'est à dire une variable 32 bits entière qui est en fait accessible par son adresse en mémoire appelée x.

L'instruction mov dword [x], 1 signifie mettre la valeur 1 codée sur 32 bits (dword) à l'adresse de la variable $x$. Ce qui, par exemple, donne une fois désassemblé :


11a0:	c7 05 08 40 00 00 01 00 00 00	mov   DWORD PTR ds:0x4008,0x1
        \___/ \_________/ \_________/
         mov    adresse     valeur
                32 bits     32 bits

C'est à dire que l'instruction à l'adresse hexadécimale 0x11a0 indique qu'il faut mettre la valeur 1 qui sera codée sur 32 bits à l'adresse hexadécimale 0x00004008. La variable $x$ est donc stockée en mémoire sur 4 octets à partir de l'adresse 0x00004008 du programme.

L'adresse que contient un pointeur peut être :

  • l'adresse d'une variable existante
  • ou l'adresse d'une variable allouée dynamiquement

1.2. Qu'est ce que l'allocation dynamique

L' allocation dynamique est la possibilité de réserver plus de mémoire en piochant dans la mémoire restante de l'ordinateur.

Une partie de la mémoire est occupée par les programmes qui ont été chargés par le système d'exploitation puis par l'utilisateur et l'autre partie reste non utilisée, on l'appelle le tas (heap en anglais).

On pourra alors charger de nouveaux programmes dans le tas ou créer de nouvelles variables (notamment des tableaux ou des structures de données de type maillon d'une liste - cf listes).

L'allocation dynamique en C++ est réalisée à l'aide des opérateurs :

  • new pour l'allocation
  • et delete pour la libération de la mémoire allouée

Se référer à la section 1.3.5 pour voir comment utiliser new et delete.

1.3. Déclaration et manipulation des pointeurs

1.3.1. Déclaration d'un pointeur en C/C++

On utilise le symbole * qui est le symbole de la multiplication mais qui, utilisé lors de la déclaration de variables, signifie pointeur :

Afficher le code    ens/l1/algo4/algo4_ptr_prog3.cpp
  1. // p est un pointeur sur un entier signé de 32 bits
  2. int *p;
  3.  
  4. // q est un pointeur sur un nombre à virgule flottante
  5. // en simple précision
  6. float *q;
  7.  

On doit lire la déclaration dans l'ordre inverse de sa définition :

déclaration d'un pointeur en C

1.3.2. Faire référence à une variable existante

Un pointeur peut référencer une variable existante, c'est à dire pointer sur cette variable, et pourra alors être utilisé (cf. ci-après) pour manipuler le contenu de la variable.

Afficher le code    ens/l1/algo4/algo4_ptr_prog4.cpp
  1. int i;
  2. int *p;
  3.  
  4. // p pointe sur i ou fait référence à i
  5. p = &i;
  6.  

L'opérateur & signifie ici l'adresse de i.

Sur le schéma suivant on a représenté une variable entière i et le pointeur p qui pointera sur i. On notera sur les schéma ci-après :

  • une variable par [nom-variable] : [type-variable]
  • un pointeur par [nom-pointeur] > [type-pointé]

représentation d'un pointeur en C

1.3.3. Lire la valeur référencée par un pointeur :

Afficher le code    ens/l1/algo4/algo4_ptr_prog5.cpp
  1. int i = 3;
  2. int *p = &i;
  3.  
  4. cout << "p = " << p << endl;
  5. cout << "contenu de p = " << *p << endl;
  6.  

Le programme précédent affichera par exemple :

p = 0x7ffeb8aceb8c # adresse de i contenue dans p
contenu de p = 3 # le contenu de p est égal au contenu de i

L'opérateur * lorsqu'il n'est pas utilisé lors de la déclaration, mais dans une expression, signifie contenu de.

  • en tant que r-value, c'est à dire, valeur située à droite d'une affectation, l'expression *p signifie lire le contenu de p ou encore la valeur pointée par
  • en tant que l-value, c'est à dire, valeur située à gauche d'une affectation, l'expression *p signifie écrire le contenu de p ou encore écrire à l'adresse de p avec la valeur située à droite du symbole =, il s'agit en fait de p donc d'une adresse

Ainsi, pour incrémenter le contenu d'un pointeur d'entier, on peut écrire :

Afficher le code    ens/l1/algo4/algo4_ptr_prog5_2.cpp
  1. *p = *p + 1;
  2.  
  3. // ou encore
  4.  
  5. ++*p;  // qui se lit : incrémenter le contenu de p
  6.  
  7.  
  8.  

Attention : ne pas confondre avec ++p qui modifie le pointeur et non son contenu.

1.3.4. Modifier la valeur référencée par un pointeur

On écrit que le contenu de p est égal à une nouvelle valeur (ligne 4) :

Afficher le code    ens/l1/algo4/algo4_ptr_prog6.cpp
  1. int i = 1;
  2. int *p = &i;
  3. // modifier le contenu de p donc de i
  4. *p = 2;
  5.  
  6. cout << "contenu de p = " << *p << endl;
  7. cout << "valeur  de i = " << i << endl;
  8.  

Le programme précédent affichera :

contenu de p = 2
valeur  de i = 2

1.3.5. Allouer dynamiquement une variable

Afficher le code    ens/l1/algo4/algo4_ptr_prog7.cpp
  1. // ------------------------------------------------------------------
  2. // allouer une seule variable entière
  3. int *p = new int;
  4. // lui affecter la valeur 128
  5. *p = 128;
  6. // supprimer la variable
  7. delete p;
  8.  
  9. // ------------------------------------------------------------------
  10. // allouer un tableau de 10 entiers
  11. int *tab = new int [ 10 ];
  12. // le remplir
  13. for (int i = 0; i < 10; ++i) tab[ i ] = i;
  14. // supprimer le tableau
  15. delete [] p;
  16.  
  17.  

Attention : pour libérer la mémoire allouée pour un tableau il ne faut pas oublier d'utiliser les crochets [] juste après l'opérateur delete.

1.3.6. Equivalence tableau / pointeur en C

En C (et donc également en C++) il existe ce que l'on appelle l'équivalence tableau / pointeur.

Un pointeur représente une adresse en mémoire et on peut lui ajouter un entier.

Afficher le code    ens/l1/algo4/algo4_ptr_prog8.cpp
  1. int tableau[10] = { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 };
  2.  
  3. int *p = &tableau[0];
  4.  
  5. cout << *p << endl;
  6.  
  7. // équivalence pointeur tableau pour accéder
  8. // au 4ème élément du tableau
  9. cout << *(p + 3) << endl;
  10. cout << p[3] << endl;
  11.  

Le résultat de l'affichage du programme est le suivant :

10
13
13

Les deux expressions *(p + 3) et p[3] sont équivalentes.

  • la première signifie qu'on accède au contenu du 3ième élément à partir de p, p étant un pointeur sur un entier, il s'agit donc du 3ième entier
  • la seconde signifie le 3ième élément du tableau p

Il s'agit ici d'arithmétique de pointeur. Quand on écrit p + 3, cette expression est interprétée par le compilateur C/C++ par : prendre l'adresse de p et lui ajouter 3 fois la taille de ce sur quoi il pointe. Ici, p étant un pointeur sur un int, on ajoutera :

  • en architecture 32 bits: 3 fois 4 octets
  • en architecture 64 bits: 3 fois 8 octets

1.3.7. L'opérateur de référence & en C++

En C++ l'opérateur & s'est vu conférer une nouveau rôle en tant qu'opérateur de référence. Il permet de manipuler les pointeurs mais tout en gardant une syntaxe plus simple qui n'est pas celle des pointeurs.

Attention à ne pas le confondre avec l'opérateur & qui donne l'adresse d'une variable lorsqu'il est placé devant celle-ci.

Justement, pour ne pas les confondre, on place l'opérateur de référence collé à son type alors que l'opérateur * qui permet de déclarer un pointeur est collé au nom de la variable ainsi que l'opérateur & qui donne l'adresse d'une variable.

Afficher le code    ens/l1/algo4/algo4_ptr_prog9.cpp
  1. int i = 1;
  2.  
  3. // déclaration d'une référence sur i (i.e. un pointeur sur i)
  4. int& p = i;
  5.    
  6. // modification du contenu de p donc de i  
  7. p = 2;
  8.  
  9. cout << "p=" << p << endl;
  10. cout << "i=" << i << endl;
  11.  

Le résultat de l'affichage du programme est le suivant :

p=2
i=2

1.3.8. Manipulation de structures avec pointeur ou référence

Pour manipuler les champs d'une structure de données (struct) on utilise l'opérateur -> avec les pointeurs.

Par contre avec une référence une utilise l'opérateur point . :

Afficher le code    ens/l1/algo4/algo4_ptr_prog10.cpp
  1. #include <string>
  2. #include <iostream>
  3. using namespace std;
  4.  
  5. // Definition d'une personne avec son prénom et son age
  6. typedef struct Personne {
  7.  
  8.     string prenom;
  9.     int age;
  10.    
  11. } Personne;
  12.  
  13. // Définition d'un opérateur pour afficher une personne
  14. ostream& operator<<( ostream& out, Personne& p ) {
  15.     out << p.prenom << ", " << p.age;
  16.     return out;
  17. }
  18.  
  19.  
  20. // Manipulation sans pointeur
  21. Personne p1;
  22. p1.prenom = "John";
  23. p1.age = 33;
  24.  
  25.  
  26. // avec pointeur
  27. Personne *p2 = new Personne;
  28. p2->prenom = "Paul";
  29. p2->age = 34;
  30.  
  31.  
  32. // avec référence
  33. Personne& p3 = p1;
  34. Personne& p4 = *p2;
  35.  
  36. p3.age = 55;
  37. p4.age = 66;
  38.  
  39. cout << "p3=" << p3 << endl;
  40. cout << "p3=" << p4 << endl;
  41.  
  42. // suppression de p2
  43. delete p2;
  44.  

On obtient à l'affichage :

p3=John, 55
p3=Paul, 66

1.3.9. Copie de chaîne de caractère en C

On rappelle qu'en C une chaîne de caractères est un tableau de char terminé par le caractère '\0'.

Pour recopier une chaîne dans une autre, on peut utiliser la fonction copy_str comme sur le programme suivant :

Afficher le code    ens/l1/algo4/algo4_ptr_prog11.cpp
  1. #include <iostream>
  2. using namespace std;
  3.  
  4. char str1[] = "abcd";
  5. char str2[10];
  6.  
  7. // Copie la chaine source (src) dans la chaine destination (dst)
  8. // Version succinte
  9. void copy_str( char *dst, char *src ) {
  10.     while ( *dst++ = *src++ ) ;
  11. }
  12.  
  13. int main() {
  14.  
  15.     copy_str( str2, str1 );
  16.    
  17.     cout << "str2=" << str2 << endl;
  18.    
  19.     return EXIT_SUCCESS;
  20. }
  21.  

La fonction copy_str est en fait une version succinte de la fonction suivante :

Afficher le code    ens/l1/algo4/algo4_ptr_prog12.cpp
  1. // Copie la chaine source (src) dans la chaine destination (dst)
  2. // Version plus compréhensible
  3. void copy_str( char *dst, char *src ) {
  4.     while ( ( *dst = *src ) != '\0') {
  5.         dst++;
  6.         src++;
  7.     }
  8. }
  9.  

1.3.10. Pointeur de pointeur

Lorsque l'on définit la fonction main d'un programme C, on déclare un char *argv[] qui est équivalent à char **argv d'après l'équivalence tableau / pointeur évoquée précédemment.

Le paramètre argv est donc un tableau ([]) de char *, soit un tableau de chaînes de caractères en langage C.

Lorsque l'on déclare une variable au format **, comme par exemple float **m, cela peut signifier :

  • soit que l'on déclare un pointeur sur un pointeur de float
  • soit que l'on déclare un tableau à deux dimensions de float que l'on va allouer dynamiquement

Voici les programmes qui correspondent à ces deux cas de figure :

Pointeur sur un pointeur

Afficher le code    ens/l1/algo4/algo4_ptr_prog13.cpp
  1. // ------------------------------------------------------------------
  2. // Cas d'un pointeur sur un pointeur de float
  3. // ------------------------------------------------------------------
  4. #include <iostream>
  5. using namespace std;
  6.  
  7. int main( int argc, char *argv[] ) {
  8.     // déclaration de deux pointeurs sur des floats
  9.     float *p, *q;
  10.    
  11.     p = new float;
  12.     *p = 1.3;
  13.     q = new float;
  14.     *q = 2.4;
  15.    
  16.     // pointeur sur pointeur de float
  17.     float **m;
  18.  
  19.     m = &p;
  20.     cout << "m pointe sur p" << endl;
  21.     cout << "--------------" << endl;
  22.     cout << "contenu de m = " << **m << endl;
  23.     // modification du contenu de m donc de p
  24.     **m *= 2;
  25.     cout << "nouveau contenu de m = " << **m << endl;
  26.     cout << "contenu de p = " << *p << endl;
  27.  
  28.     m = &q;
  29.     cout << "m pointe sur q" << endl;
  30.     cout << "--------------" << endl;
  31.     cout << "contenu de m = " << **m << endl;
  32.     // modification du contenu de m donc de q
  33.     **m *= 2;
  34.     cout << "nouveau contenu de m = " << **m << endl;
  35.     cout << "contenu de q = " << *q << endl;
  36.  
  37.     return EXIT_SUCCESS;
  38. }
  39.  
  40.  
m pointe sur p
--------------
contenu de m = 1.3
nouveau contenu de m = 2.6
contenu de p = 2.6
m pointe sur q
--------------
contenu de m = 2.4
nouveau contenu de m = 4.8
contenu de q = 4.8

Tableau à deux dimensions alloué dynamiquement

Afficher le code    ens/l1/algo4/algo4_ptr_prog14.cpp
  1. // ------------------------------------------------------------------
  2. // Cas d'une matrice de float allouée dynamiquement
  3. // ------------------------------------------------------------------
  4.  
  5. #include <iostream>
  6. #include <iomanip>
  7. using namespace std;
  8.  
  9. int main( int argc, char *argv[] ) {
  10.  
  11.     // création d'une matrice de 3 lignes par 7 colonnes de float
  12.     float **m;
  13.     m = new float * [ 3 ];
  14.     for (int i = 0; i < 3; ++i) {
  15.         m[ i ] = new float [ 7 ];
  16.         for (int j = 0; j < 7; ++j) {
  17.             m[ i ][ j ] = i*10 + j;
  18.         }
  19.     }
  20.    
  21.     // affichage de la matrice
  22.     for (int i = 0; i < 3; ++i) {
  23.         for (int j = 0; j < 7; ++j) {
  24.             cout << std::fixed << setw(5) << setprecision(2);
  25.             cout << m[ i ][ j ] << " ";
  26.         }
  27.         cout << endl;
  28.     }
  29.    
  30.     cout << **m << endl;
  31.     cout << **(m + 1) << endl;
  32.     cout << **(m + 2) << endl;
  33.     cout << *(*(m+2) + 3) << endl;
  34.  
  35.     return EXIT_SUCCESS;
  36. }
  37.  
  38.  
 0.00  1.00  2.00  3.00  4.00  5.00  6.00 
10.00 11.00 12.00 13.00 14.00 15.00 16.00 
20.00 21.00 22.00 23.00 24.00 25.00 26.00
0.00	**m
10.00	**(m+1)
20.00	**(m+2)
23.00	*(*(m+2) + 3)

Enfin, il faut faire attention à l'endroit où ** est utilisé, comme dans l'exemple suivant :

Afficher le code    ens/l1/algo4/algo4_ptr_prog15.cpp
  1. #include <iostream>
  2. using namspace std;
  3.  
  4. int main() {
  5.    
  6.     int *x = new int;
  7.     int *y = new int;
  8.     *x = 2;
  9.     *y = 3;
  10.     // Attention :
  11.     // cette expression est interprétée comme :
  12.     // *x = (*x) * (*y)
  13.     // ce qui donne *x = 6
  14.     *x = *x**y;
  15.    
  16.     cout << "*x=" << *x << endl;
  17.        
  18.     return 0;
  19. }
  20.  

Dans ce genre de cas il est préférable de réécrire l'expression comme indiqué, à savoir (*x) * (*y) afin que la relecture soit plus facile. On comprend alors que c'est le contenu de x qui est multiplié par le contenu de y.

1.4. Manipuler aisément les pointeurs

Voici un petit programme qui permet de mieux comprendre (enfin je l'espère), l'utilisation des pointeurs. On utilise ici des macro-instructions du langage C afin d'écrire comme en français :

  • dans un premier temps on déclare deux variables entières a et b
  • on modifie a en utilisant un pointeur p
  • on modifie b par l'intermédiaire d'un pointeur de pointeur q qui pointe sur p, lui-même pointant sur b
Afficher le code    ens/l1/algo4/pointeurs_facile.cpp
  1. #include <iostream>
  2. using namespace std;
  3.  
  4. // déclare une variable 'var' de type 'type'
  5. #define declare_var( var, type ) type var
  6.  
  7. // déclare un pointeur 'ptr' de type 'type'
  8. #define declare_ptr( ptr, type) type *ptr
  9.  
  10. // déclare un pointeur vers un pointeur
  11. #define declare_ptr_ptr( ptrptr, type ) type **ptrptr
  12.  
  13. // donne le contenu du pointeur 'ptr'
  14. #define contenu_ptr( ptr ) (*ptr)
  15.  
  16. // donne l'adresse de la variable 'var'
  17. #define adresse_var( var ) (&var)
  18.  
  19. // fait pointer le pointeur 'ptr' sur la variable 'var'
  20. #define pointe_sur( ptr, var)  ptr = &var
  21.  
  22. // donne le nom de la variable, son adresse et sa valeur
  23. // qu'il s'agisse d'une variable entière ou d'un pointeur
  24. #define examine(x) \
  25.     cout << #x << " (adresse=" << &x << ") = " << x << endl;
  26.  
  27. /**
  28.  * Programme principal
  29.  *
  30.  */
  31. int main( ) {
  32.  
  33.     // on définit deux entiers
  34.     declare_var( a, int );
  35.     declare_var( b, int );
  36.  
  37.     a = 1;
  38.     b = 5;
  39.  
  40.     cout << "=======================" << endl;
  41.     cout << "Initialement" << endl;
  42.     cout << "=======================" << endl;
  43.     examine( a );
  44.     examine( b );
  45.  
  46.     // on déclare un pointeur sur un entier
  47.     declare_ptr( p, int );
  48.  
  49.     // on aurait pu écrire également : p = adresse_var( a )
  50.     pointe_sur( p, a );
  51.  
  52.     contenu_ptr( p ) = contenu_ptr( p ) + 1;
  53.  
  54.     cout << endl;
  55.     cout << "=======================" << endl;
  56.     cout << "Utilisation de int *p" << endl;
  57.     cout << "=======================" << endl;
  58.     cout << "| int *p = &a;" << endl;
  59.     cout << "| *p = *p + 1;" << endl;
  60.     cout << "-----------------------" << endl;
  61.     examine( a );
  62.     examine( b );
  63.     examine( p );
  64.  
  65.     cout << endl;
  66.     cout << "=======================" << endl;
  67.     cout << "Utilisation de int **q" << endl;
  68.     cout << "=======================" << endl;
  69.     cout << "| int **q = &p;" << endl;
  70.     cout << "| p = &b;" << endl;
  71.     cout << "| **q = 7;" << endl;
  72.     cout << "-----------------------" << endl;
  73.  
  74.     declare_ptr_ptr( q, int );
  75.     pointe_sur( q, p );
  76.     pointe_sur( p, b );
  77.  
  78.     contenu_ptr( contenu_ptr( q ) ) = 7;
  79.    
  80.     examine( a );
  81.     examine( b );
  82.     examine( p );
  83.     examine( q );
  84.  
  85.     return 0;
  86. }
  87.  

Le résultat de l'exécution est alors :

=======================
Initialement
=======================
a (adresse=0x7ffe1f3712d0) = 1
b (adresse=0x7ffe1f3712d4) = 5

=======================
Utilisation de int *p
=======================
| int *p = &a;
| *p = *p + 1;
-----------------------
a (adresse=0x7ffe1f3712d0) = 2
b (adresse=0x7ffe1f3712d4) = 5
p (adresse=0x7ffe1f3712d8) = 0x7ffe1f3712d0

=======================
Utilisation de int **q
=======================
| int **q = &p;
| p = &b;
| **q = 7;
-----------------------
a (adresse=0x7ffe1f3712d0) = 2
b (adresse=0x7ffe1f3712d4) = 7
p (adresse=0x7ffe1f3712d8) = 0x7ffe1f3712d4
q (adresse=0x7ffe1f3712e0) = 0x7ffe1f3712d8