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...
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;
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;
- Déclaration d'un pointeur typé :
PInteger
- Récupération de l'adresse d'une variable : addr ou @
- Modification d'une variable pointée avec ^
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.
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 TListla 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.