Chapitre 24 - Pointeurs▲
Un pointeur est une variable qui contient l'adresse mémoire d'une autre variable stockée en mémoire. Soit P le pointeur et P^ la variable « pointée » par le pointeur. La déclaration d'une variable pointeur réserve 4 octets nécessaires au codage de l'adresse mémoire.
La déclaration d'un pointeur ne réserve aucune mémoire pour la variable pointée.
Cette mémoire, il faudra l'allouer dynamiquement (plus loin dans ce chapitre).
Jusqu'alors nous avons vu que la déclaration d'une variable provoque automatiquement la réservation d'un espace mémoire qui est fonction du type utilisé. Voir chapitre 4 (« Différents types de variables ») pour la taille en mémoire de chacun des types de variables utilisés ci-après.
Exemples :
Var
Somme : Integer
;
{ Réservation de 2 octets dans la mémoire }
Var
Moyenne : Real
;
{ Réservation de 6 octets dans la mémoire }
Var
Tableau : Array
[1
..100
] of
Integer
;
{ Réservation de 400 octets (100 * 4) dans la mémoire }
Var
Nom : String
[20
];
{ Réservation de 21 octets dans la mémoire }
Var
x, y, z : Integer
;
{ Réservation de 6 octets (3 * 2) dans la mémoire }
Var
Tab1, Yab2 : Array
[0
..10
,0
..10
] of
Integer
;
{ Réservation de 484 octets (2 * 11 * 11 * 2) dans la mémoire }
Type
Personne = Record
Nom, Prenom : String
[20
];
Age : Byte
;
Tel : Integer
;
end
;
Var
Client, Fournisseur : Personne;
{ Réservation de 90 octets (2 * (2 * 21 + 1 + 2)) dans la mémoire }
On comprend rapidement que s'il vous prenait l'envie de faire une matrice de 100 * 100 réels (100 * 100 * 6 = 60 ko) à passer en paramètre à une procédure, le compilateur vous renverrait une erreur du type : Structure too large, car il lui est impossible de réserver plus de 16 Ko en mémoire pour les variables des sous-programmes. Voir chapitre 23 (« Gestion de la mémoire par l'exécutable »).
D'où l'intérêt des pointeurs, car, quelle que soit la taille de la variable pointée, la place en mémoire du pointeur est toujours la même : 4 octets. Ces quatre octets correspondent à la taille mémoire nécessaire pour stocker l'adresse mémoire de la variable pointée.
Mais qu'est-ce qu'est-ce qu'une adresse mémoire ? C'est en fait un enregistrement comprenant deux nombres de type Word (2 fois 2 octets font bien 4), qui représentent respectivement l'adresse du segment de donnée utilisé et l'indice (le déplacement) du premier octet servant à coder la variable à l'intérieur de ce même segment (un segment étant un bloc de 65536 octets). Cette taille de segment implique qu'une variable ne peut pas dépasser la taille de 65536 octets, et que la taille de l'ensemble des variables globales ne peut pas dépasser 65536 octets ou encore que la taille de l'ensemble des variables d'un sous-programme ne peut dépasser cette même valeur limite.
La déclaration d'un pointeur permet donc de réserver une petite place de la mémoire qui pointe vers une autre qui peut être très volumineuse. L'intérêt des pointeurs est que la variable pointée ne se voit pas réserver de mémoire dans la zone mémoire des variables globales, mais plutôt dans le tas. Puisque la pile, normalement destinée aux variables des sous-programmes, est trop petite (16 Ko), on utilise donc le tas, dont la taille peut atteindre 640 Ko.
Déclaration▲
Avant d'utiliser une variable de type pointeur, il faut déclarer ce type en fonction du type de variable que l'on souhaite pointer.
Exemple :
Type
PEntier = ^Integer
;
Var
P : PEntier;
On déclare une variable P de type PEntier qui est en fait un pointeur pointant vers un Integer (à noter la présence indispensable de l'accent circonflexe !). Donc la variable P contient une adresse mémoire, celle d'une autre variable qui est elle, de type Integer. Ainsi, l'adresse mémoire contenue dans P est l'endroit où se trouve le premier octet de la variable de type Integer. Il est inutile de préciser l'adresse mémoire de fin de l'emplacement de la variable de type Integer,car une variable de type connu, quelle que soit sa valeur, occupe toujours le même espace. Le compilateur sachant à l'avance combien de place tient tel ou tel type de variable, il lui suffit de connaître grâce au pointeur l'adresse mémoire du premier octet occupé et de faire l'addition adresse mémoire contenue dans le pointeur + taille mémoire du type utilisé pour définir totalement l'emplacement mémoire de la variable pointée par le pointeur.
Accès à la variable pointée▲
Tout ça, c'est très bien, mais comment fait-on pour accéder au contenu de la variable pointée par le pointeur ? Il suffit d'utiliser l'identificateur du pointeur à la fin duquel on rajoute un accent circonflexe.
Exemple :
P^ := 128
;
Donc comprenons-nous bien, P est le pointeur contenant l'adresse mémoire d'une variable et P^ (avec l'accent circonflexe) contient la valeur de la variable pointée. On passe donc du pointeur à la variable pointée par l'ajout du symbole spécifique ^ à l'identificateur du pointeur.
Type
Tableau = Array
[1
..100
] Of
Real
;
PTableau = ^Tableau;
Var
P : PTableau;
Ici, on déclare un type Tableau qui est un tableau de 100 Real. On déclare aussi un type de pointeur PTableau pointant vers le type Tableau. C'est-à -dire que toute variable de type PTableau, contiendra l'adresse mémoire du premier octet d'une variable de type Tableau. Ce type Tableau occupe 100*6 = 600 octets en mémoire; le compilateur sait donc parfaitement comment écrire une variable de type Tableau en mémoire. Quant à la variable P de type PTableau, elle contient l'adresse mémoire du premier octet d'une variable de type Tableau. Pour accéder à la variable de type Tableau pointée par P, il suffira d'utiliser la syntaxe P^.
P étant le pointeur et P^ étant la variable pointée. P contenant donc une adresse mémoire et P^ contenant un tableau de 100 Real. Ainsi, P^[10] représente la valeur du dixième élément de P^ (c'est donc un nombre de type Real) tandis que P[10] est une opération invalide, qui déclenche une erreur du compilateur.
New et Dispose▲
La déclaration au début du programme des diverses variables et pointeurs a pour conséquence que les variables se voient allouer un bloc mémoire à la compilation. Et ce dernier reste réservé à la variable associée jusqu'à la fin du programme.
Avec l'utilisation des pointeurs, tout cela change puisque la mémoire est allouée dynamiquement. On a vu que seul le pointeur se voit allouer (réserver) de la mémoire (4 octets, c'est très peu) pour toute la durée de l'exécution du programme, mais pas la variable correspondante. Il est cependant nécessaire de réserver de la mémoire à la valeur pointée en cours de programme (et pas forcément pour toute la durée de celui-ci) en passant en paramètre un pointeur P qui contiendra l'adresse mémoire correspondant à la variable associée P^. Pour pouvoir utiliser la variable pointée par le pointeur, il est absolument indispensable de lui réserver dynamiquement de la mémoire comme suit :
New(P);
Et pour la supprimer, c'est-à -dire libérer la place en mémoire qui lui correspondait et perdre bien sûr son contenu :
Dispose(P);
Ainsi, lorsqu'on en a fini avec une variable volumineuse et qu'on doit purger la mémoire afin d'en utiliser d'autres tout autant volumineuses, on utilise Dispose. Si, après,au cours du programme, on veut réallouer de la mémoire à une variable pointée par un pointeur, c'est possible (autant de fois que l'on veut !).
Une variable allouée dans le tas par New contient n'importe quoi, jusqu'à ce que lui ait affecté une valeur !
Type
Tab2D = Array
[1
..10
,1
..10
] of
Integer
;
PMatrice = ^Tab2D;
Var
GogoGadgetAuTableau : PMatrice;
On a donc une variable GogoGadgetAuTableau (4 octets) qui pointe vers une autre variable (10 * 10 * 2 = 200 octets) de type Tab2D qui est un tableau de deux dimensions contenant 10 * 10 nombres entiers. Pour être précis, la variable GogoGadgetAuTableau est d'un type PMatrice pointant vers le type Tab2D. Donc la taille de GogoGadgetAuTableau sera de 4 octets, puisque contenant une adresse mémoire, et GogoGadgetAuTableau^ (avec un ^) sera la variable de type Tab2D contenant 100 nombres de type Integer.
On pourra affecter des valeurs à la variable comme suit :
GogoGadgetAuTableau^[i,j] := 3
;
Toutes les opérations possibles concernant les affectations de variables, ou leur utilisation dans des fonctions sont valables pour les variables pointées par des pointeurs.
Il est bien entendu impossible de travailler sur la variable pointée par le pointeur sans l'avoir auparavant allouée !
Program
Exemple29c;
Const
Max = 10
;
Type
Tab2D = Array
[1
..Max,1
..Max] of
Integer
;
PMatrice = ^Tab2D;
Var
GogoGadgetAuTableau : PMatrice;
i, j, x : Integer
;
BEGIN
New(GogoGadgetAuTableau);
for
i := 1
to
Max do
for
j := 1
to
Max do
GogoGadgetAuTableau^[i,j] := i + j;
x := GogoGadgetAuTableau^[Max,Max] * Sqr(Max);
WriteLn(Cos(GogoGadgetAuTableau^[Max,Max]));
Dispose(GogoGadgetAuTableau);
END
.
Ce court programme Exemple29c montre qu'on utilise une variable pointée par un pointeur comme n'importe quelle autre variable.
Program
Exemple29d;
Type
Point = Record
x, y : Integer
;
Couleur : Byte
;
end
;
PPoint = ^Point;
Var
Pixel1, Pixel2 : PPoint;
BEGIN
Randomize;
New(Pixel1);
New(Pixel2);
with
Pixel1^ do
begin
x := 50
;
y := 100
;
Couleur := Random(14
) + 1
;
end
;
Pixel2^ := Pixel1^;
Pixel2^.Couleur := 0
;
Dispose(Pixel1);
Dispose(Pixel2);
END
.
Dans ce programme Exemple29d, on déclare deux variables pointant chacune vers une variable de type Point, ce dernier étant un type structuré (enregistrement). La ligne d'instruction Pixel2^ := Pixel1^; signifie qu'on égalise champ à champ les variables Pixel1 et Pixel2.
Si les symboles ^ avaient été omis, cela n'aurait pas provoqué d'erreur, mais cela aurait eu une tout autre signification : Pixel2 := Pixel1; signifierait que le pointeur Pixel2 prend la valeur du pointeur Pixel1, c'est-à -dire que Pixel2 pointera vers la même adresse mémoire que Pixel1. Ainsi, les deux pointeurs pointent vers le même bloc mémoire et donc vers la même variable. Donc Pixel1^ et Pixel2^ deviennent alors une seule et même variable.
On ne peut égaliser deux pointeurs que s'ils ont le même type de base (comme pour les tableaux).
Et dans ce cas, les deux pointeurs pointent exactement vers la même variable.
Je rappelle qu'il est impossible de travailler sur la valeur pointée par le pointeur sans avoir utilisé auparavant la procédure New qui alloue l'adresse mémoire au pointeur. Si vous compilez votre programme sans avoir utilisé New, une erreur fatale vous rappellera à l'ordre !
Program
Exemple29e;
Const
Max = 10
;
Type
Personne = Record
Nom, Prenom : String
;
Matricule : Integer
;
end
;
Tableau = Array
[1
..Max] of
Personne;
PTableau = ^Tableau;
Var
Tab : PTableau;
i : Integer
;
BEGIN
New(Tab);
with
Tab^[1
] do
begin
Nom := 'Cyber'
;
Prenom := 'Zoïde'
;
Matricule := 1256
;
end
;
for
i := 1
to
Max do
WriteLn(Tab^[i].Nom);
{ Seul l'élément d'indice 1 dans le tableau est initialisé }
{ Les éléments 2 à 10 contiennent n'importe quoi (rappel) }
Dispose(Tab);
END
.
Il est possible de combiner les enregistrements, les tableaux et les pointeurs. Cela donne un vaste panel de combinaisons. Essayons-en quelques-unes :
Type
TabP = Array
[1
..100
] of
^Integer
;
Var
Tab : TabP;
{ Tableau de pointeurs pointant vers des entiers. }
{ Tab[i] est un pointeur et Tab[i]^ est un entier. }
Type
Tab = Array
[1
..100
] of
Integer
;
PTab = ^Tab;
Var
Tab : PTab;
{ Pointeur pointant vers un tableau d'entiers. }
{ Tab^[i] est un entier et Tab est un pointeur. }
GetMem et FreeMem▲
Il existe des procédures similaires au couple New et Dispose :
GetMem(Pointeur,Taille_Memoire);
Cette procédure réserve un nombre d'octets en mémoire égal à Taille_Memoire au pointeur Pointeur. Taille_Memoire est donc la taille de la variable pointée par le pointeur Pointeur.
FreeMem(Pointeur,Taille_Memoire);
Cette fonction supprime de la mémoire la variable pointée par le pointeur Pointeur, qui occupait Taille_Memoire octets.
Si vous utilisez New pour allouer une variable dynamique, il faudra utiliser Dispose et non pas FreeMem pour la désallouer.
De même, si vous utilisez GetMem pour l'allouer, il faudra utiliser FreeMem et non pas Dispose pour la désallouer.
Program
Exemple29f;
Const
Max = 10
;
Type
Personne = Record
Nom, Prenom : String
;
Matricule : Integer
;
end
;
Tableau = Array
[1
..Max] of
Personne;
PTableau = ^Tableau;
Var
Tab : PTableau;
i : Integer
;
BEGIN
GetMem(Tab,Max*SizeOf(Personne));
for
i := 1
to
Max do
ReadLn(Tab^[i].Nom);
FreeMem(Tab,Max*SizeOf(Personne));
END
.
Vous aurez remarqué que ce programme Exemple29f est exactement le même que Exemple29e, mis à part qu'il utilise le couple GetMem et FreeMem au lieu des traditionnels New et Dispose. C'est un peu moins pratique à utiliser puisqu'il faut savoir exactement quelle place en mémoire occupe la variable pointée par le pointeur spécifié. Mais ça peut être très pratique si Max = 90000 (très grand) et si vous décidez de faire entrer au clavier la borne supérieure du tableau.
Voir le programme suivant :
Program
Exemple29g;
Const
Max = 90000
;
Type
Personne = Record
Nom, Prenom : String
;
Matricule : Integer
;
end
;
Tableau = Array
[1
..Max] Of
Personne;
PTableau = ^Tableau;
Var
Tab : PTableau;
i : Integer
;
N : LongInt
;
BEGIN
Write
('Combien de personnes ? '
);
ReadLn(N);
GetMem(Tab,N * SizeOf(Personne));
for
i := 1
To
N do
ReadLn(Tab^[i].Nom);
FreeMem(Tab,N * SizeOf(Personne));
END
.
Nil▲
Nous l'avons vu en long et en large, un pointeur contient une adresse. Mais comment indiquer qu'il ne contient… aucune adresse ? Il existe une adresse qui ne pointe vers rien : Nil (on rencontre souvent NULL dans d'autres langages). Il peut en effet être utile de tester qu'un pointeur pointe vers quelque chose ou bien vers rien, par exemple lorsque l'on travaille avec des listes chainées. Pour schématiser la structure d'une liste chainée, il s'agit d'une chaine d'éléments contenant non seulement une valeur, mais aussi l'adresse de l'élément suivant.
Exemple :
Type
pElement = ^tElement;
tElement = Record
Valeur : Integer
; { Valeur de l'élément }
Suivant : pElement; { Pointe vers l'élément suivant dans la liste chainée }
end
;
Le dernier élément de la liste chainée doit pointer vers… rien. On lui affectera donc la valeur Nil :
Var
Element : pElement;
BEGIN
...
New(Element);
Element^.Valeur := ...;
Element^.Suivant := Nil
;
...
END
.
Pour tester qu'un pointeur pointe vers quelque chose ou rien, c'est tout simple :
if
p = Nil
then
{ Pointe vers rien }
else
...