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 :

  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 :

  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 :

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 :

  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.

  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 :

représentation d'un pointeur en C

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

  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.

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

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

  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

  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.

  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.

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 :

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.

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

  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 :

  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 :

  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 :

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

Pointeur sur un pointeur

  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

  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 :

  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 :

  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