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 :
// déclaration d'une variable entière nommée x
int x;
// affectation de la valeur 1 à la variable x
x = 1;
le codage en assembleur du morceau de code précédent est :
; déclaration des données
section .data
x: dd 0
; code
section .text
mov dword [x], 1
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 :
// p est un pointeur sur un entier signé de 32 bits
int *p;
// q est un pointeur sur un nombre à virgule flottante
// en simple précision
float *q;
On doit lire la déclaration dans l'ordre inverse de sa définition :
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.
int i;
int *p;
// p pointe sur i ou fait référence à i
p = &i;
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é]
1.3.3. Lire la valeur référencée par un pointeur :
int i = 3;
int *p = &i;
cout << "p = " << p << endl;
cout << "contenu de p = " << *p << endl;
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 :
*p = *p + 1;
// ou encore
++*p; // qui se lit : incrémenter le contenu de p
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) :
int i = 1;
int *p = &i;
// modifier le contenu de p donc de i
*p = 2;
cout << "contenu de p = " << *p << endl;
cout << "valeur de i = " << i << endl;
Le programme précédent affichera :
contenu de p = 2
valeur de i = 2
1.3.5. Allouer dynamiquement une variable
// ------------------------------------------------------------------
// allouer une seule variable entière
int *p = new int;
// lui affecter la valeur 128
*p = 128;
// supprimer la variable
delete p;
// ------------------------------------------------------------------
// allouer un tableau de 10 entiers
int *tab = new int [ 10 ];
// le remplir
for (int i = 0; i < 10; ++i) tab[ i ] = i;
// supprimer le tableau
delete [] p;
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.
int tableau[10] = { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 };
int *p = &tableau[0];
cout << *p << endl;
// équivalence pointeur tableau pour accéder
// au 4ème élément du tableau
cout << *(p + 3) << endl;
cout << p[3] << endl;
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.
int i = 1;
// déclaration d'une référence sur i (i.e. un pointeur sur i)
int& p = i;
// modification du contenu de p donc de i
p = 2;
cout << "p=" << p << endl;
cout << "i=" << i << endl;
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 . :
#include <string>
#include <iostream>
using namespace std;
// Definition d'une personne avec son prénom et son age
typedef struct Personne {
string prenom;
int age;
} Personne;
// Définition d'un opérateur pour afficher une personne
ostream& operator<<( ostream& out, Personne& p ) {
out << p.prenom << ", " << p.age;
return out;
}
// Manipulation sans pointeur
Personne p1;
p1.prenom = "John";
p1.age = 33;
// avec pointeur
Personne *p2 = new Personne;
p2->prenom = "Paul";
p2->age = 34;
// avec référence
Personne& p3 = p1;
Personne& p4 = *p2;
p3.age = 55;
p4.age = 66;
cout << "p3=" << p3 << endl;
cout << "p3=" << p4 << endl;
// suppression de p2
delete p2;
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 :
#include <iostream>
using namespace std;
char str1[] = "abcd";
char str2[10];
// Copie la chaine source (src) dans la chaine destination (dst)
// Version succinte
void copy_str( char *dst, char *src ) {
while ( *dst++ = *src++ ) ;
}
int main() {
copy_str( str2, str1 );
cout << "str2=" << str2 << endl;
return EXIT_SUCCESS;
}
La fonction copy_str est en fait une version succinte de la fonction suivante :
// Copie la chaine source (src) dans la chaine destination (dst)
// Version plus compréhensible
void copy_str( char *dst, char *src ) {
while ( ( *dst = *src ) != '\0') {
dst++;
src++;
}
}
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
// ------------------------------------------------------------------
// Cas d'un pointeur sur un pointeur de float
// ------------------------------------------------------------------
#include <iostream>
using namespace std;
int main( int argc, char *argv[] ) {
// déclaration de deux pointeurs sur des floats
float *p, *q;
p = new float;
*p = 1.3;
q = new float;
*q = 2.4;
// pointeur sur pointeur de float
float **m;
m = &p;
cout << "m pointe sur p" << endl;
cout << "--------------" << endl;
cout << "contenu de m = " << **m << endl;
// modification du contenu de m donc de p
**m *= 2;
cout << "nouveau contenu de m = " << **m << endl;
cout << "contenu de p = " << *p << endl;
m = &q;
cout << "m pointe sur q" << endl;
cout << "--------------" << endl;
cout << "contenu de m = " << **m << endl;
// modification du contenu de m donc de q
**m *= 2;
cout << "nouveau contenu de m = " << **m << endl;
cout << "contenu de q = " << *q << endl;
return EXIT_SUCCESS;
}
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
// ------------------------------------------------------------------
// Cas d'une matrice de float allouée dynamiquement
// ------------------------------------------------------------------
#include <iostream>
#include <iomanip>
using namespace std;
int main( int argc, char *argv[] ) {
// création d'une matrice de 3 lignes par 7 colonnes de float
float **m;
m = new float * [ 3 ];
for (int i = 0; i < 3; ++i) {
m[ i ] = new float [ 7 ];
for (int j = 0; j < 7; ++j) {
m[ i ][ j ] = i*10 + j;
}
}
// affichage de la matrice
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 7; ++j) {
cout << std::fixed << setw(5) << setprecision(2);
cout << m[ i ][ j ] << " ";
}
cout << endl;
}
cout << **m << endl;
cout << **(m + 1) << endl;
cout << **(m + 2) << endl;
cout << *(*(m+2) + 3) << endl;
return EXIT_SUCCESS;
}
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 :
#include <iostream>
using namspace std;
int main() {
int *x = new int;
int *y = new int;
*x = 2;
*y = 3;
// Attention :
// cette expression est interprétée comme :
// *x = (*x) * (*y)
// ce qui donne *x = 6
*x = *x**y;
cout << "*x=" << *x << endl;
return 0;
}
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
#include <iostream>
using namespace std;
// déclare une variable 'var' de type 'type'
#define declare_var( var, type ) type var
// déclare un pointeur 'ptr' de type 'type'
#define declare_ptr( ptr, type) type *ptr
// déclare un pointeur vers un pointeur
#define declare_ptr_ptr( ptrptr, type ) type **ptrptr
// donne le contenu du pointeur 'ptr'
#define contenu_ptr( ptr ) (*ptr)
// donne l'adresse de la variable 'var'
#define adresse_var( var ) (&var)
// fait pointer le pointeur 'ptr' sur la variable 'var'
#define pointe_sur( ptr, var) ptr = &var
// donne le nom de la variable, son adresse et sa valeur
// qu'il s'agisse d'une variable entière ou d'un pointeur
#define examine(x) \
cout << #x << " (adresse=" << &x << ") = " << x << endl;
/**
* Programme principal
*
*/
int main( ) {
// on définit deux entiers
declare_var( a, int );
declare_var( b, int );
a = 1;
b = 5;
cout << "=======================" << endl;
cout << "Initialement" << endl;
cout << "=======================" << endl;
examine( a );
examine( b );
// on déclare un pointeur sur un entier
declare_ptr( p, int );
// on aurait pu écrire également : p = adresse_var( a )
pointe_sur( p, a );
contenu_ptr( p ) = contenu_ptr( p ) + 1;
cout << endl;
cout << "=======================" << endl;
cout << "Utilisation de int *p" << endl;
cout << "=======================" << endl;
cout << "| int *p = &a;" << endl;
cout << "| *p = *p + 1;" << endl;
cout << "-----------------------" << endl;
examine( a );
examine( b );
examine( p );
cout << endl;
cout << "=======================" << endl;
cout << "Utilisation de int **q" << endl;
cout << "=======================" << endl;
cout << "| int **q = &p;" << endl;
cout << "| p = &b;" << endl;
cout << "| **q = 7;" << endl;
cout << "-----------------------" << endl;
declare_ptr_ptr( q, int );
pointe_sur( q, p );
pointe_sur( p, b );
contenu_ptr( contenu_ptr( q ) ) = 7;
examine( a );
examine( b );
examine( p );
examine( q );
return 0;
}
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