Les pointeurs (POINTER)

Qu'est-ce que cette chose étrange ? Les pointeurs sont utilisés dans tous les langages de programmation, mais la plupart du temps ils le sont de façon transparente; sans qu'on s'en rende compte. Dans certains langage (dont le Pascal) on peut manipuler explicitement des pointeurs, et là y'a des notions à bien maîtriser pour ne pas s'y perdre.

Tout d'abord parlons un peu d'organisation mémoire...de façon simpliste, on peut dire que la mémoire de l'ordinateur est une sorte de grand tableau d'octets. Dès que votre programme déclare une variable, une partie de ce tableau est réservée au stockage de son contenu.
Par exemple
  • Un BYTE occupera une entrée du tableau (un octet)
  • Un WORD deux octets, donc deux entrées consécutives
  • etc...
Le programme repère la variable par son adresse, c'est à dire l'index du tableau à partir duquel est stocké la variable; un seul index suffit car les octets sont utilisés de façon linéaire; c'est à dire qu'un WORD occupe les octets "index" et "index+1".

Cet "index" est connu en pascal grâce à la fonction addr; par exemple addr(i) renvoie un entier long (integer) qui correspond à l'adresse de la variable "i" en mémoire; on peut également utiliser la notation @i.

Disons le tout de suite, c'est cette "adresse" qu'on appel "pointeur"...et alors, à quoi ça sert ?

Comme je viens de l'expliquer, l'ordinateur l'utilise en interne, il ne manipule pas les noms de variables mais uniquement leurs adresses...mais prenons un exemple de code qui exploite des pointeurs de façon transparente :

{
 Dans cet exemple, c'est l'adresse des variables "i1" et "i2" qui est passée à la fonction.
 la procédure "Traitement" incrémente donc l'entier stocké à l'adresse "i".
 Sans le mot clé "var", c'est le paramètre "i" qui serait incrémenté.
}
procedure Traitement(var i:integer);
 begin

  i:=i+1;
 end;

var
 i1:integer;
 i2:integer;
begin
 Traitement(i1);
 Traitement(i2);
end;
Reprenons cet exemple en révélant l'usage du pointer; pour cela on va déclarer un nouveau type, le type "pointeur sur un entier" (pinteger).
On parle alors de pointeur typé c'est à dire qu'on connaît le type de la variable sur laquelle on pointe (sinon on utilise le mot clé pointer qui ignore tout de l'adresse sur laquelle il pointe).
{
 Dans cet exemple, c'est l'adresse des variables "i1" et "i2" qui est passée à la fonction.
 la procédure "Traitement" incrémente donc l'entier stocké à l'adresse "i".
}
type
 PInteger=^Integer; { le "^" devant le type indique un pointeur typé }

procedure Traitement(i:PInteger);
 begin
  i^:=i^+1; { le "^" après la variable donne accès à la variable pointée }
 end;

var
 i1:integer;
 i2:integer;

begin
 Traitement(addr(i1));
 Traitement(@i2);
end;
Nous voilà au coeur du sujet !
  1. Déclaration d'un pointeur typé : PInteger
  2. Récupération de l'adresse d'une variable : addr ou @
  3. Modification d'une variable pointée avec ^
Remarquez que inc(i^) est tout aussi valide; il n'y a aucune différence entre une variable et son pointeur suivit du "^".

ATTENTION, si vous indiquez inc(i) c'est l'adresse stockée dans "i" qui est incrémentée ! et comme il s'agit d'un pointeur typé, "i" est incrémenté de la taille du type sur lequel il pointe, soit SizeOf(Integer)...
Sachant cela, et que les éléments d'un tableau se suivent en mémoire, voici un exemple d'incrémentation de pointeur :
{
 Dans cet exemple, on incrément chaque élément d'un tableau d'entiers
}
type
 PInteger=^Integer;

procedure Traitement(i:pinteger; count:integer);
 begin
  while count>0 do begin

   inc(i^); { incrément l'entier pointé }
   inc(i);  { incrément le pointeur pour pointer sur l'entier suivant }
   dec(count);
  end;
 end;

var
 ii:array[1..100] of integer;

begin
 Traitement(@ii[1],100); { on indique l'adresse du premier élément et le nombre total d'éléments }
 Traitement(@ii[5],50);  { on peut aussi traiter un sous-ensemble du tableau ! }
 Traitement(@ii[3],20);
end;

Allocation dynamique (GetMem et FreeMem)

On peut allez encore plus loin dans l'utilisation des pointeurs; jusqu'ici j'ai parlé de pointeurs sur des variables existante (déclarée explicitement), mais grâce aux pointeurs, on va pouvoir "allouer" dynamiquement de la mémoire.
L'exemple typique est celui des tableaux dynamiques :
{
 Nous voulons gérer un tableau d'enregistrements, mais sans prédéterminer le nombre total d'enregistrements.
}
type
 TEnr=record { exemple d'enregistrement }  
  Nom:string;
  Age:integer; 
 end;


{ Tableau d'enregistrements... }  
 TArrayOfEnr=array[0..0] of TEnr;
 // ATTENTION !
 // ce tableau de dimension bidon permet d'utiliser les variables de ce type comme un tableau.
 // si l'option RangeCheck est active, le programme peut provoquer une erreur d'exécution.
 // on peut déclarer ce tableau sous la forme "array[word] of TEnr" afin d'éviter l'erreur
 // mais dans ce cas Initialize() utilisé ci-dessous ne peut plus fonctionner car elle 
 // chercherait à initialiser systématiquement 65536 (un word) TEnr !
  

{ Saisie d'un enregistrement (pas de pointeur explicite) } 
procedure Saisir(Var Enr:TEnr);
 begin

  Write('Nom : '); ReadLn(Enr.Nom);
  Write('Age : '); ReadLn(Enr.Age);
 end;

var
 Enr:^TArrayOfEnr; { un pointeurs...vide à la déclaration }
 Max:integer;
 i:integer;

begin
 Write('Indiquez le nombre d''enregistrements :');
 ReadLn(max);
 GetMem(Enr,max*SizeOf(TEnr)); { allocation d'une zone mémoire suffisante pour contenir "max" TEnr } 
 Initialize(Enr^,max); { initialisation obligatoire car TEnr contient un (long)string } 
 for i:=0 to max-1 do begin

  Saisir(Enr^[i]); { saisir l'enregistrement numéro "i" } 
 end;
 for i:=0 to max-1 do begin
  with Enr^[i] do WriteLn(Nom,' à ',Age,' ans');
 end;
 Finalize(Enr^); { obligatoire car TEnr contient un (long)string } 
 FreeMem(Enr); { libération de la mémoire } 

end.
L'exemple utilise un champ Nom:string qui est une chaine "longue" allouée dynamiquement par Delphi.
il faut donc impérativement initialiser le tableau Enr avant de l'utiliser, et s'assurer de la suppression des chaines avant de le libérer.
NB: ce problème ne survient pas si on utilise les "nouveaux" tableaux dynamiques :
var
 Enr:array of TEnr; { sans dimension, ce tableau est "dynamique" }

begin
 Write('Indiquez le nombre d''enregistrements :');
 ReadLn(max);
 SetLength(Enr,max); { allocation d'une zone mémoire suffisante pour contenir "max" TEnr } 
 for i:=0 to max-1 do begin
  Saisir(Enr[i]); { saisir l'enregistrement numéro "i" } 
 end;
 for i:=0 to max-1 do begin

  with Enr[i] do WriteLn(Nom,' à ',Age,' ans');
 end;
 Enr:=nil; { libération de la mémoire } 
 // La dernière ligne est ici superflue, car Delphi libère automatiquement les tableaux dynamiques devenus inutiles ;D
end;


Précision: nil est la valeur particulière d'un pointeur non utilisé (c'est tout simplement 0). Notez qu'un pointeur qui n'est pas égal à nil n'est pas forcément valide : dans l'exemple ci-dessus, FreeMem(Enr) libère l'adresse mémoire pointée par "Enr"; si "Enr" contient toujours cette adresse, son utilisation n'est plus autorisée par le système d'exploitation ! L'utilisation d'un pointeur invalide provoque un plantage de l'application (GPF, écran bleu...). Il vous appartient de déterminer à tout moment si le contenu d'un pointeur est valide ou pas; une bonne pratique étant de forcer le pointeur à "nil" quand il n'est plus valide :
 if (Enr<>nil) then FreeMem(Enr);
 GetMem(Enr,max*SizEOf(TEnr));
 if (Enr=nil) then Halt; { Erreur d''allocation mémoire ! } 
 ...
 FreeMem(Enr); {  le test "if enr<>nil" n'est PLUS valide ! } 
 Enr:=nil: { le test "if enr<>nil" est de nouveau valide } 

Liste de pointeurs (TList)

Je ne peux pas parler des pointeurs sous Delphi sans parler de la class TList
la class TList permet de manipuler une liste de pointeurs, c'est à dire des éléments de nature quelconque.
La méthode Add() ajoute un pointeur à la liste et la méthode Remove() le supprime. De là, vous pouvez y stocker ce que vous voulez !

{
 Exemple d'utilisation de TList pour gérer une liste de TBitmap.
}

uses
 classes, sysutils, graphics;

Var
 List:TList;
 BMP :TBitmap;
 ff:TSearchRec;
 i:integer;

begin
 List:=TList.Create; { création d'une liste de pointeurs }

 if FindFirst('*.BMP',faAnyFile,ff)=0 then begin { recherche des fichiers .BMP }
  repeat
   BMP:=TBitmap.Create;       { création d'une class Bitmap }
   BMP.LoadFromFile(ff.Name); { lecture du fichier }

   List.Add(BMP);             { ajout du Bitmap à la liste des pointeurs }
  Until FindNext(ff)<>0;
  FindClose(ff);
 end; 

 { ... faire quelque chose de cette liste de bitmap... }

 for i:=0 to List.Count-1 do begin

  BMP:=TBitmap(List[i]); { récupère la class Bitmap }
  BMP.Free;              { libère le Bitmap, TList ne le fera pas pour nous !!! }
 end; 
 List.Free; { libère la liste des pointeurs  }
end.
Remarquez, que dans cette exemple, on n'utilise aucun symbole propre aux pointeurs (^ et @)...comme je l'explique dans le chapitre sur les class, celles-ci sont des pointeurs implicitement la syntaxe est alors simplifiée.