[delphi] déclarer et utiliser les pointeurs

[delphi] déclarer et utiliser les pointeurs

Tutoriel lié à celui de GrandVizir : http://codes-sources.commentcamarche.net/faq/177-delphi-declarer-et-utiliser-les-types

Introduction

Les pointeurs sont très utiles. Ils permettent d'accélerer ou d'alléger certains processus.
Comme vous le savez, à chaque fois que vous déclarez une variable, une taille est automatiquement allouée dans la mémoire. Chaque variable peut être considérée comme une "boîte" qui contient l'information que l'on a stocké. Ces petites "boîtes" sont placées quelque part dans la mémoire, elles ont une adresse.
Le principe du pointeur c'est de gérer soi-même la mémoire, au lieu de laisser le compilateur le faire.Lorsque vous déclariez votre variable, vous mettiez votre information dans une "boîte" sans vous souciez ou elle se trouvait dans la mémoire. Cette fois, vous allez gérer son emplacement et sa taille.
Un pointeur est une variable spéciale qui contient une adresse. On pourrait comparer un pointeur à un raccourci. Imaginez que vous ayez une liste de 40 divx, faisant 1 go chacun, que vous voulez trier. La premiere solution consiste à trier naturellement ces 40 fichiers, ce qui mettrait un temps fou. La deuxième serait de créer un raccourci pour chaque divx, et de trier, non pas les divx directement, mais les raccourcis de ceux-ci. Ainsi en très peu de temps, on a trié notre liste (même si on n'a pas trié directement la liste, le résultat est le même).

On pourrai donc voir les pointeurs comme des "flèches" qui pointent sur des "boîtes".

Déclaration de type pointeur simple

Pour déclarer un pointeur rien de plus simple. Il suffit de mettre un accent circonflexe devant le type pointé.

pChiffre = ^Integer;
pChaine = ^string;
etc...

Ce type de pointeur ne peux pointer que devant le type qui lui est associé.
Lorsque vous allez créer votre pointeur, celui-ci ne pointera sur rien. Vous devez donc lui dire sur quoi pointer.

Exemple :

procedure Main();
var
  Chiffre1: ^integer;
  I: integer;
begin
  I:=12;
  Chiffre1:=nil; // ne pointe sur rien
  Chiffre1:=@I;
  WriteLn(Chiffre1^); // Affichera 12
  WriteLn(I); // Affichera 12
end;

Analysons l'exemple précédent.
On commence par déclarer un pointeur sur entier (Chiffre1) et un entier (I). Puis on attribue la valeur de 12 à I. Chiffre1 ne pointe au début sur rien (nil). Puis on récupère l'adresse de I, grâce à l'opérateur "@". Maintenant notre "flèche" chiffre1 pointe sur la "boîte" I. Enfin, on aimerait se servir du joli pointeur que l'on a créé, alors on va afficher la valeur de I, indirectement. On déréférence le pointeur, c'est à dire que le pointeur va se comporter comme s'il était la "boîte" pointée (avec le signe"^").
A présent, on sait utiliser les pointeurs comme raccourci. Maintenant, on va voir comment allouer de la mémoire, c'est à dire se servir des pointeurs sans utiliser de variables déjà existante. Vous allez me dire, s'il n'y a pas de variable à pointer, comment faire ? Et bien c'est simple, on peut utiliser un pointeur pour créer une "boite" dans la mémoire.
Pour allouer de la mémoire plusieurs solutions.
On utilise la fonction GetMem. Vous donnez le nom du pointeur, et la taille de la "boîte".
Exemple :

GetMem(Chiffre1,50);

Voila.. Vous venez de créer un espace pour mettre une valeur. Maintenant, on sait que toute les variables ne prennent pas toute la même place en mémoire. On va donc allouer de la mémoire en fonction de la taille de la variable.
Exemple :

GetMem(Chiffre1,SizeOf(integer));

Voila, on est sur d'occuper exactement la taille nécessaire.
Toutefois je vous conseille d'utiliser la fonction "New" qui se charge de tout.
Exemple :

New(Chiffre1);

Cela revient au même, mais c'est plus simple à utiliser, non ?

Notre "boîte"est créée. Il ne reste qu'à la remplir. Pour cela, on déréférence le pointeur, et on fait comme si le pointeur était une variable normale.
Exemple :

Chiffre1^:=52;

Il reste une notion importante, si on alloue un espace mémoire, il faut ABSOLUMENT le libérer quand on a finit de s'en servir.
Pour cela, plusieurs méthodes, en fonction de la méthode utilisée.

Exemple :
GetMem(Chiffre1,50); => FreeMem(Chiffre1,50);
GetMem(Chiffre1,SizeOf(integer)); => FreeMem(Chiffre1,SizeOf(integer));
New(Chiffre1); => Dispose(Chiffre1);

Je vous conseille fortement d'utiliser "new" et "dispose".

Exemple récapitulatif :

procedure Main();
var
  Chiffre1: ^integer;
  Chiffre2: pChiffre;
  Chaine1 : pChaine;
  I: integer;
  s: string;
begin
  new(Chiffre1);// Allocation mémoire
  new(Chiffre2);
  new(Chaine1);

  s:='bonjour';
  I:=7;
  Chiffre1^:=5;
  WriteLn(Chiffre1^);// affichera 5
  Chiffre1^:=I;
  WriteLn(Chiffre1^);// affichera 7
  Chiffre2^:=I;
  WriteLn(Chiffre2^);// affichera 7
  Chaine1^:='b';
  WriteLn(Chaine1^);// affichera 'b'
  Chaine1^:=s;
  WriteLn(Chaine1^);// affichera 'bonjour'

  dispose(Chiffre1);// Désallocation mémoire
  dispose(Chiffre2);
  dispose(Chaine1);
end;

Ainsi on remarque que le type "^integer" et "pChiffre" pointe sur la même chose.
Pourtant, ceci: "Chiffre2:=Chiffre1" ne fonctionnera pas. En effet, bien que ces deux types de pointeurs pointent sur la même chose, ils ne sont pas identiques pour autant.
Alors, me direz-vous, pourquoi créer par exemple le type pChiffre et ne pas laisser ^integer ?
La réponse est simple, pour les passage en paramètres dans les fonctions et procédures.
Ceci ne fonctionnera pas :

procedure Test(Pointeur:^integer);

Par contre ceci, oui :

procedure Test(Pointeur:pChiffre);

Voila tout l'intérêt de créer ses propres types pointés.

Ensuite on peut remplir les paramètres de la fonction test comme ceci :

Test(Chiffre1);// Ne fonctionnera pas, les types étant différents
Test(Chiffre2);// L'adresse de 7 (fonctionne)
Test(@I);// L'adresse de 7 (fonctionne)
Test(@7);// Ne fonctionnera pas, 7 n'étant pas une variable

(Je rappelle que l'opérateur @ renvoie l'adresse d'une variable).

Enfin, lorsque vous utilisez des pointeurs, essayez de les utiliser comme suit :

procedure Main();
var
  Chiffre2: pChiffre;
begin
  new(Chiffre2);// Allocation mémoire
   Try
     I:=7;
     WriteLn(I);// affichera 7
     WriteLn(Chiffre2^);// affichera 7
   Finally
     dispose(Chiffre2);// Désallocation mémoire
   end;
end;

Avec cette structure vous êtes sur que votre pointeur sera bien détruit, même en cas d'erreur.

Les erreurs classiques

Il y a certaines erreurs qui reviennent souvent. Par exemple, n'essayez jamais de désallouer un pointeur nul.

Exemple :

procedure Main();
var
  Chiffre2: pChiffre;
begin
  new(Chiffre2);// Allocation mémoire
  Chiffre2:=nil;// ne pointe sur rien
   Try
     I:=7;
     WriteLn(I);// affichera 7
     WriteLn(Chiffre2^);// PLANTE
   Finally
     dispose(Chiffre2);// PLANTE
   end;
end;

En passant, lorsque l'on a fait un "Chiffre2:=nil;" on a perdu, dans la mémoire, la "boîte" que l'on pointait. Comme on a perdu l'adresse, on ne pourra plus jamais la retrouver. Une partie de la mémoire sera donc occupée pour rien. (Pas de panique, un petit reboot et c'est réglé).

Deuxième erreur fréquente, n'oubliez pas d'allouer avant de manier un pointeur.

Exemple :

procedure Main();
var
  Chiffre2: pChiffre;
begin
   Try
     I:=7;
     WriteLn(I);// affichera 7
     WriteLn(Chiffre2^);// PLANTE
   Finally
     dispose(Chiffre2);// PLANTE
   end;
end;

Maintenant vous allez me dire, oui mais dans ton premier exemple tu n'alloues pas.

Rappel (1er exemple) :

procedure Main();
var
  Chiffre1: ^integer;
  I: integer;
begin
  I:=12;
  Chiffre1:=nil; // ne pointe sur rien
  Chiffre1:=@I;
  WriteLn( Chiffre1^ ); // Affichera 12
  WriteLn( I ); // Affichera 12
end;

C'est pas tout à fait pareil, dans mon premier exemple, je n'ai rien à allouer parce que les variables existent déjà. Ici, la variable I existe, je veux stocker dans la "boîte" I, je n'ai donc pas besoin de créer une nouvelle "boîte".

Enfin, dernière erreur (la plus fréquente), n'oubliez pas de désallouer un pointeur.

Exemple :

procedure Main();
var
  Chiffre2: pChiffre;
begin
  New(Chiffre1);
  I:=7;
  WriteLn(I);// affichera 7
  WriteLn(Chiffre2^);// Affiche 7
end;

Le pire dans cette erreur, c'est que ça ne fait pas planter l'application. De plus, Delphi désalloue tout seul les pointeurs si vous oubliez de le faire, mais ne prenez pas de mauvaises habitudes...

Déclaration de type pointeur plus évoluée

On peut bien évidemment pointer sur des structures plus évoluées (Tableau,enregistrement, classe, etc..).

pTab = ^TTab;
TTab = Array of Array of integer;

pRecord = ^TRecord;
TRecord = record
 Truc:string;
 Machin:integer;
end;

Créer un tableau de pointeur :

TTabPointeur = Array of pChiffre;// Tableau de pointeur pointant sur des integer

Ou même pointer sur un pointeur (bien que je n'ai jamais trouvé une utilité à ceci) :

pPointeur = ^pChiffre;
pChiffre = ^integer;

ATTENTION : ne pas confondre "pointeur sur tableau" et "tableau de pointeur".
Un pointeur sur tableau est un pointeur unique qui pointe sur un tableau.
Il se référence comme ceci :

Tab^[0]

Un tableau de pointeur est un tableau qui contient des pointeurs.
Il se référence comme ceci :

Tab[0]^

On peut évidemment déclarer un type "Pointeur sur tableau de pointeur".
Il se référence comme ceci :

Tab^[0]^

Le pointeur neutre

Dernier type de pointeur : le pointeur neutre. Ce type de pointeur, peut pointer sur tout.
On le déclare comme ceci:

Pointeur = Pointer;

Toutefois, ce type de pointeur ne peut être déréférencé.
ceci est valide :

Pointeur:=Chiffre2; // (avec Chiffre2^:=5)

ceci est invalide :

Pointeur^:=5;

En effet, il faut transtyper ce type.
ceci est valide :

 WriteLn(pChiffre(Pointeur)^); // affiche 5

ceci est invalide :

WriteLn(Pointeur^);

Je vous déconseille d'utiliser ce type de pointeur. En effet, une erreur de pointeur est vite arrivée et il sera difficile de la trouver.

Les pointeurs sur fonctions/procédures

On peut pointer aussi sur des fonctions ou des procédures.
<cod pascal>pFunc = function:integer;// Pointe sur des fonctions
//qui ne prennent rien en paramètre
//mais retournent un type integer
pProc = procedure; // Pointe sur des procédures qui ne prennent rien en paramètre

pFunc2 = function(s:string):integer;// Pointe sur des fonctions
//qui prennent une chaine en paramètre
//et retournent un type integer
pProc2 = procedure(I:integer);// Pointe sur des procédures qui prennent
//un entier en paramètre</code>

Sans le savoir lorsque vous remplissez un événement dans l'inspecteur d'objet (par exemple l'événement "OnClick") c'est un pointeur sur procédure que vous utilisez.

OnClick = Procedure(Sender: TObject);

Pour utiliser nos procédures pointées, il faut faire ceci :

type
  pFunc2 = function(s:string):integer;

function MyStrToInt(s:string):integer;
begin
   Result:=StrToInt(s);
end;

function MyStrToInt2x(s:string):integer;
begin
   Result:=2*StrToInt(s);
end;

procedure Main();
var
  Func: pFunc2;
  Resultat:integer;
begin
  Func:=MyStrToInt;
  Resultat:=Func('13');
  WriteLn(Resultat);// affiche 13
  Func:=MyStrToInt2x;
  Resultat:=Func('13');
  WriteLn(Resultat);// Affiche 26
end;

Cela permet aussi d'avoir indirectement un tableau de procédures ou de fonctions.

TTabFunc = Array of pFunc2;

Les listes chainées

Enfin, je terminerai sur les listes chainées. Une liste chainée est comparable à un tableau.
Elle se construit avec un enregistrement. Le principe est celui ci : chaque "case" de ce "tableau" contient l'adresse de la prochaine "case". D'une certaine manière cela revient à gérer soit même son tableau.
On la déclare comme ceci :

pListe = ^TListe;
TListe = record
  Truc: integer;
  ...
  Suivant: pListe;
end;

Pour savoir quand on atteint la fin du tableau, on regarde si la variable "Suivant" vaut "nil". Il existe des variantes comme les listes doublement chainées (chaque case possède l'adresse de la case suivante et de la case précédente), les listes circulaires (la dernière case pointe sur la première), et les listes circulaires doublement chainées.

Manipulation

Accéder à l'élément n

Pour lire un élément, il suffit de le déréférencer. Mais avant il faut déjà placer le pointeur dessus.

1er élément : Pointeur^
2eme élément : Pointeur^.Suivant^
3eme élément : Pointeur^.Suivant^.Suivant^

Vous imaginez bien que si la liste comporte 120 éléments, on ne va pas écrire "Suivant^.Suivant^...".
On va donc utiliser une boucle.

Exemple :

begin
 For I:=0 to 2 do
   Pointeur:=Pointeur^.Suivant;

 // accès 3ème élément
 WriteLn(Pointeur^);
end;

Ainsi on atteint le 3eme élément du tableau. En contrepartie, on perdu toutes les informations précédentes (on ne peut plus retourner en arrière).
ATTENTION : le "danger" avec les listes chainées c'est de perdre la "tête", c'est à dire perdre les précédents éléments de la liste. Cela arrive fréquemment. Pour parcourir votre liste chainée, créez un pointeur temporaire.

Il faut donc toujours l'écrire comme ceci :

Exemple :

var
 TmpPointeur:pListe;
begin
TmpPointeur:=Pointeur;
 For I:=0 to 2 do
   TmpPointeur:=TmpPointeur^.Suivant;

 // accès 3ème élément
 WriteLn(TmpPointeur^);
end;

 

Là, c'est parfait, on accède au 3ème élément, sans perdre des informations.

Insertion d'élément

Pour insérer un élément, le principe est le suivant : On crée une "boîte" pointée par un pointeur temporaire. On fait pointer notre "boîte" sur la "boîte" suivante de la "boîte" précédente. On fait maintenant pointer la "boîte" précédente sur notre "boîte". Et voila, on a ajouté notre élément. Vous comprenez maintenant l'utilité des listes chainées par rapport aux tableaux. Dans un tableau, il aurait fallu décaler certains éléments vers la droite, en liste chainée, c'est immédiat.

Exemple d'ajout en 3ème position :

var
 TmpPointeur,PointeurCreation:pListe;
begin
New(PointeurCreation);
TmpPointeur:=Pointeur;
 // acces 2eme élément
 For I:=0 to 1 do
   TmpPointeur:=TmpPointeur^.Suivant;
 // On fait pointer notre "boîte" sur la "boîte" suivante de la "boîte" précédente.
 PointeurCreation^.Suivant:=TmpPointeur^.Suivant;
 // On fait maintenant pointer la "boîte" précédente sur notre "boîte".
 TmpPointeur^.Suivant:=PointeurCreation;
end;

 

Suppression d'élément

Pour supprimer un élément, le principe est proche de l'insertion d'élément. On se place sur l'élément précédant l'élément à supprimer. Puis on pose un pointeur temporaire sur cet élément. On fait pointer l'élément précédent sur l'élément suivant l'élément à supprimer. Enfin, on détruit l'élément à supprimer.

Exemple de suppression de la 3ème position :

var
 TmpPointeur,SupprPointeur:pListe;
begin
TmpPointeur:=Pointeur;
 // On se place sur l'élément précédent l'élément à supprimer.
 For I:=0 to 1 do
   TmpPointeur:=TmpPointeur^.Suivant;
 // Puis on pose un pointeur temporaire sur cet élément.
 SupprPointeur:=TmpPointeur^.Suivant;
 // On fait pointer l'élément précédent sur l'élément suivant l'élément à supprimer.
 TmpPointeur^.Suivant:=SupprPointeur^.Suivant;
 // Enfin, on détruit l'élément à supprimer.
 Dispose(SupprPointeur);
end;

 

Conclusion

Pour des exemples un peu plus concret, allez voir ici:
http://codes-sources.commentcamarche.net/source/32957-tutorial-listes-chainees

Ce document intitulé « [delphi] déclarer et utiliser les pointeurs » issu de CodeS SourceS (codes-sources.commentcamarche.net) est mis à disposition sous les termes de la licence Creative Commons. Vous pouvez copier, modifier des copies de cette page, dans les conditions fixées par la licence, tant que cette note apparaît clairement.
Rejoignez-nous