Developpez.com - Pascal
X

Choisissez d'abord la catégorieensuite la rubrique :

 

CHAPITRE XXIV : Pointeurs

Par Hugo ETIEVANT

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 mais ne réserve aucune mémoire pour la variable pointée.

Jusqu'alors nous avions 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 4 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 12 octets (3*4) dans la mémoire.

Var tab1,tab2 : Array[0..10,0..10] Of Integer ;
Réservation de 968 octets (2*11*11*4) dans la mémoire.

Type persone = Record

nom,prenom : String[20] ;
age : Byte ;
tel : Integer ;
End;
Var client,fournisseur : personne ;
Réservation de 94 octets (2*(2*21+1+4)) 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 renvérait 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 quelque soit la grosseur de la variable pointée, la place en mémoire du pointeur est toujours la même : 4 octets. Ces quatres octets correspondent à la taille mémoire nécessaire pour stocker l'adresse mémoire de la variable pointé. Mais qu'est-ce qu'est une adresse mémoire ? C'est en fait deux nombres de type Word (2 fois 2 octets font bien 4) qui représentent respectivement l'indice du segment de donnée utilisé et l'indice du premier octet servant à coder la variable à l'intérieur de ce même segment de donné (un segment étant un bloc de 65535 octets). Cette taille de segment implique qu'une variable ne peut pas dépasser la taille de 65535 octets, et que la taille de l'ensemble des variables globales ne peut pas dépasser 65535 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, ce qui représente une importante économie de mémoire permettant de manipuler un très grand nombre de données. Puisque la Pile normalement destinée aux variables des sous-programmes est trop petite (16 Ko), on utilise donc le Tas réservé au pointeur qui nous laisse jusqu'à 64 ko, soit quatre fois plus !

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

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 (à noté 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 quelque soit sa valeur occupe toujours le même espace. Le compilateur sachant à l'avance conbien 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.

Tout ça c'est très bien bien mais comment fait-on pour accéder au contenu de la variable pointée par le pointeu ? Il suffit de manipuler l'identificateur du pointeur à la fin duquel on rajoute un accent circonflexe en guise de variable pointée.
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 une type Tableau qui est un tableau de 100 réels. On déclare aussi un type de pointeur PTableau pointant vers le type Tableau. C'est-à-dire que dans toute variable de type PTableau, sera contenue 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. Quand à 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 réels. 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] (déclenche une erreur du compilateur) ne représente rien pas même l'adresse mémoire du dixième élément de P^.

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é 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 non pas pour la totalité) 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 absoluement indispensable de lui réserver dynamiquement de la mémoire comme suit :
Syntaxe :

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

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 vous voulez !).

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*6 = 600 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 donc 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 vraie pour les variables pointée par des pointeurs. Il est bien entendu 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.

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
Begin
For
j:=1 to Max Do GogoGadgetAuTableau^[i,j] := i+j ;
End;
x := GogoGadgetAuTableau^[Max,Max] * Sqr(Max) ;
WriteLn(Cos(
GogoGadgetAuTableau^[Max,Max])) ;
Dispose(
GogoGadgetAuTableau);
END.

Ce court exemple29c montre qu'on utilise une variable pointée par un pointeur comme n'importe quelle autre variable.

Note : un pointeur peut pointer vers n'importe quel type de variable sauf de type fichier (File Of, Text).

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é (appelé aussi enregistrement). La ligne d'instruction : Pixel2^ := Pixel1^ ; signifie qu'on égalise champ à champ les variables Pixel1 et Pixel2.
Si les symboles ^ avaient été ommis, cela n'aurait pas provoquer d'erreur mais cela aurait eut une tout autre signification : Pixel2 := Pixel1 ; signifie 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. Si l'on change la valeur d'un champ de l'une de ces deux variables, cela change automatiquement le même champ de l'autre variable !

Note : 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 éxactement vers la même variable. Toute modification de cette variable par l'intermédiaire de l'un des deux pointeurs se répercute sur l'autre.

Autre note : 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, un erreur fatale vous ramènera à 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) ;
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.

Const Max = 20 ;
Type
Station = Record

nom : String ;
liaisons : Array[1..10] Of Station ;
End ;
TabStation = Array[1..Max] Of Station ;
PTabStation = ^TabStation ;
Var France : PTabStation ;
France est un pointeur pointant vers un tableau d'enregistrement dont l'un des champs est un tableau et l'autre un enregistrement (récursif).

Pour alléger le code, on aurait pu faire plus court :
Const Max = 20 ;
Type
Station = Record

nom : String ;
liaisons : Array[1..10] Of Station ;
End ;
TabStation = Array[1..Max] Of Station ;
Var France : ^TabStation ;

Il existe des fonctions similaires au couple New et Dispose :
Syntaxes :

GetMem(Pointeur,Mémoire) ; Cette fonction réserve un nombre d'octets en mémoire égale à Mémoire au pointeur Pointeur. Mémoire correspond à la taille de la variable pointée par le pointeur Pointeur. FreeMem(Pointeur,Mémoire) ; Cette fonction supprime de la mémoire le pointeur Pointeur dont la variable pointée occupait Mémoire octets.

Note : Si vous utilisez New pour le pointeur P, il faudra lui associer Dispose et non pas FreeMem. De même, si vous utilisez GetMem pour le pointeur P, il faudra utiliser FreeMem et non pas Dispose.

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 remarquez que ce programme exemple29f est exactement le même que le exemple29e mis à part qu'il utilise le couple GetMem et FreeMem au lieu des traditionnels New et Dispose. C'est un peu moins sûr à 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 décidez de faire entrer au clavier la borne supérieur 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
WriteLn('
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.
Responsables bénévoles de la rubrique Pascal : Gilles Vasseur - Alcatîz -