Jul 24, 2008

Les Pointeurs

Free Web Hosting, No Ads > CONTRIBUTE > Computers > Programming Languages > Assembler and Delphi

free web hosting

Les Pointeurs

remonit
Les pointeurs sont un élément très important dans la programmation. En Delphi, ils sont moins utilisés car le langage est pensé pour que l'on n'aie pas à s'en servir. Néanmoins, leur connaissance permet de mieux comprendre certains fonctionnements de Delphi et de découvrir d'autres méthodes de programmation.

L'aide de Delphi décrit très bien les pointeurs et suffit pour apprendre à les utiliser.

I Définition

Lorsque votre programme est exécuté, toutes les variables sont stockées en mémoire. Voici par exemple un programme qui définit 3 variables:

program Test;

var Variable1, Variable2: byte;
Variable3: integer;

begin

end.

On peut représenter, de façon simplifiée, la mémoire du programme comme ceci:
Case 1 Case 2 Case 3 Case 4 Case 5 Case 6
Variable1 Variable2 Variable3

Ici, les "cases" représentent la mémoire. Chaque case est utilisée par une variable. Chaque variable occupe plus ou moins de case. En fait, chaque case représente un octet; un byte utilise une place de 1 octet alors qu'un integer utilise une place de 4 octets. Cette représentation est simplifiée car les variables ne sont pas forcément côtes à côtes ni dans cet ordre.

Un pointeur est une variable qui tient sur 4 caractères, comme un integer. Il contient donc le même type de donnée. Mais le pointeur est utilisé pour stocker non pas une donnée, mais l'adresse d'une donnée, son "numéro de case" pour faire simple. Par exemple, si dans le tableau ci-dessus Variable3 était un pointeur qui contiendrait l'adresse de Variable2, il contiendrait le chiffre "2", numéro de la case "Case 2" (Encore une fois c'est une vision simplifiée de la mémoire).

En pratique, si Variable3 est un pointeur qui contient l'adresse de Variable2, on dit que Variable3 pointe sur Variable2. On peut recopier le tableau ci-dessus en inscrivant leurs valeurs:
Case 1 Case 2 Case 3 Case 4 Case 5 Case 6
Variable1 Variable2 Variable3
45 12 2

La troisième ligne contient la valeur inscrite dans les cases, donc la valeur des variables. Ici, Variable1 vaut 45, Variable2 vaut 12 et Variable3 vaut 2.

Variable3 pointe sur Variable2, et on pourra récupérer la valeur de Variable2 en passant par Variable3. Pour cela, on demande la valeur de la variable dont l'adresse est contenue dans Variable3. Cela est différent de simplement demander le contenu de Variable3, soit l'adresse contenue dans Variable3.

II Syntaxe des pointeurs

Reprenons l'exemple ci-dessus. On a décidé de modifier Variable3 pour qu'au lieu d'être un integer, il était un pointeur sur Variable2. Variable3 pointe donc sur un byte, puisque Variable2 est un byte. Voici le nouveau programme:

program Test;

var Variable1, Variable2: byte;
Variable3: ^byte;

begin
Variable3 := @Variable2;
end.

Le symbole ^ situé avant "byte" signifie que Variable3 est un pointeur sur un byte. Le symbole @ devant "Variable2" signifie "Adresse de Variable2". Ainsi, le code ci-dessus affecte Variable3 à l'adresse de Variable2. Il faut bien faire la différence entre l'adresse de Variable2, c'est-à-dire @Variable2, et la valeur de Variable2.

Maintenant, vous pouvez accéder au contenu de Variable2 à partir de Variable3. Voici comment:

program Test;

var Variable1, Variable2: byte;
Variable3: ^byte;

begin
Variable3 := @Variable2;
Variable1 := Variable3^;
end.

Le ^ après Variable3 indique qu'on accède à la valeur contenue à l'adresse contenue dans Variable3. Si vous oubliez le ^, cela aura un sens différent (vous aurez même une erreur de compilation, à priori) puisque sans le ^ vous n'accédez qu'à Variable3 et non pas à la variable pointée par Variable3. Dans l'exemple ci-dessus, ce que l'on a fait est simplement affecté Variable1 à Variable2 finalement.

Vous pouvez affecter un pointeur à un autre pointeur. Attention à ce que vous faites! Voici un exemple:

var Pointeur1, Pointeur2: ^integer;
Variable1: integer;

begin
Pointeur2 := @Variable1;
Pointeur1 := Pointeur2; // Ligne A.
Pointeur1 := @Pointeur2; // Ligne B.
end;

Ces deux lignes (A et cool.gif sont différentes! La ligne A fait que Pointeur1 pointe vers le même endroit que Pointeur2. Pointeur1 contiendra la même valeur que Pointeur2, c'est-à-dire l'adresse de Variable1. La ligne B fait que Pointeur1 pointe sur Pointeur2, donc Pointeur1 contiendra l'adresse de Pointeur2 et non pas l'adresse de Variable1.

Vous pouvez aussi créer une variable à partir d'un pointeur. Voici un exemple:

var Pointeur: ^integer;
begin
new(Pointeur);
Pointeur^:=5;
dispose(Pointeur);
end;

Ici, new permet de créer une variable integer et de mettre son adresse dans Pointeur. Après on peut modifier la variable ainsi créée avec Pointeur^. Dispose fait l'inverse de new: il supprime la variable de la mémoire. Si vous tentez d'accéder à Pointeur^ après dispose ou avant new, la variable n'étant pas créée, vous aurez une violation d'accès à l'exécution. Cela n'est bien sûr pas le cas si vous avez affecté le pointeur à une autre adresse précédemment (comme dans les exemples précédents).

Si vous faites un deuxième new sans dispose, vous aurez deux variables en mémoire et l'une d'elle sera perdue sans que vous puissiez la supprimer ni l'utiliser... à moins que vous ayez copié l'adresse avant le nouveau "new", dans un autre pointeur. Dans ce cas, chaque pointeur pointera vers des variables différentes.

Par l'intermédiaire des pointeurs, on peut donc, à l'exécution, créer d'autres variables. Le problème est qu'il faut toujours savoir où se trouvent ces variables, et pour cela un pointeur, dont l'adresse nous est connu puisqu'on a déjà déclaré ce pointeur, peut stocker l'adresse de la variable créée à l'exécution.

III Exemple avec des records

Souvent les pointeurs sont utilisés pour pointer sur des enregistrements (record). Voici un exemple:

type PPersonne = ^TPersonne;
TPersonne = record
Nom, Prenom: string[100];
Age: byte;
Index: integer;
end;

procedure ChangeAge(Personne: PPersonne; Age: byte);
begin
Personne^.Age := Age;
end;

procedure TForm1.Button1Click(Sender: TObject);
var Personne: TPersonne;
begin
Personne.Index := 1;
Personne.Nom := 'Rousseau';
Personne.Prenom := 'Jean-Jacques';
Personne.Age := 62;
ChangeAge(@Personne, 65);
showmessage(inttostr(Personne.Age));
end;

Ici, on a déclaré un type record qui contient quelques informations sur une personne (Nom, Prénom, Age, et un n° pour une base de donnée par exemple: Index). On a créé une procédure ChangeAge qui prend en paramètre un pointeur sur ce type record, cette procédure est capable de modifier l'âge de la personne pointée par ce pointeur passé en paramètre. C'est ce qu'on fait dans Button1Click: on crée une personne, on lui affecte un âge et d'autres propriétés. Puis on appelle ChangeAge en passant l'adresse de la variable Personne (@Personne) en paramètre. On affiche le nouvel âge de la personne (65) par un showmessage.

Quelques remarques:

* Dans ChangeAge, Personne est un pointeur sur un TPersonne. On peut accéder aux propriétés de la variable pointée par ce pointeur. Pour cela, on utilise encore le symbole ^. Ensuite on met le point (.) et le nom de la propriété, ici Age, comme pour tout record. Le ^ à la fin du pointeur transforme, en quelque sorte, ce pointeur en variable simple, et on peut ensuite utiliser cette variable comme d'habitude, donc pour un record on peut accéder à ses propriétés comme d'habitude. Il faut bien se rappeler que l'on accède à la variable dont l'adresse est contenue par le pointeur. Si l'on omet le ^, la variable ne sera plus un TPersonne mais un PPersonne, donc un pointeur, et l'on ne pourra pas utiliser le point (.) pour accéder à ses propriétés comme Age: en effet, un pointeur n'a pas ces propriétés... seule la variable sur laquelle il pointe peut en avoir.
* On a ici remplacé en quelque sorte le rôle de "var" dans les paramètres d'une procédure. On aurait aussi bien pu déclarer ChangeAge comme ceci:
procedure ChangeAge(var Personne: TPersonne; Age: byte);
Et de cette manière on n'aurait pas eu à se servir des pointeurs. Delphi aide le programmeur à se passer des pointeurs, et ceci en est un exemple.
* Ici la déclaration de PPersonne (le pointeur sur TPersonne) est placée juste avant la déclaration de TPersonne. Dans la déclaration de PPersonne, on fait donc appel à TPersonne qui n'est pourtant pas encore déclaré... c'est une particularité des pointeurs, qui peuvent être déclarés avant le type sur lequel ils pointent, à condition que le pointeur et le type pointé soient dans la même clause "type". Ainsi le code suivant ne marchera pas:
type PPersonne = ^TPersonne;
type TPersonne = record
Nom, Prenom: string[100];
Age: byte;
Index: integer;
end;
On voit souvent un type pointeur qui pointe sur le type record qui suit. C'est une habitude, lorsqu'on crée un type record, on crée souvent le pointeur correspondant. On le crée juste avant le type record, et par convention on remplace le "T" par un "P" dans le nom du type.
Pourquoi déclare-t-on le type pointeur avant le type record? Cela permet au record d'utiliser le type pointeur ainsi déclaré, afin que le record puisse avoir un pointeur sur une variable du même type que lui-même. Cela est utile dans certains cas.
* Si vous écrivez une procédure qui prend en paramètre un record afin de le lire (sans le modifier), vous serez tenté de déclarer cette procédure comme ceci:
procedure MaProcedure(MonRecord: TMonRecord);
Lorsque vous appellerez cette procédure, le paramètre passé sera entièrement recopié pour MaProcedure. Pour éviter cette copie, vous pouvez utiliser les pointeurs, et remplacer TMonRecord par un "PMonRecord", type pointeur qui pointe sur TMonRecord. Ainsi vous passerez en paramètre à la procédure non pas toute une copie du record, mais seulement son adresse, ce qui est plus rapide, et surtout plus propre dans le cas de gros records. Attention, si la procédure MaProcedure modifie la variable pointée (MonRecord^), la variable passée en paramètre sera modifiée, ce qui n'est pas le cas lors de la déclaration ci-dessus puisqu'une copie est effectuée pour la procédure.

IV Autre exemple

Si vous utilisez souvent les TList, vous avez pu remarquer que pour y stocker une variable de type byte, par exemple, il fallait déclarer une classe qui contiendrait ce byte. Pourtant, il y a plus simple. Le TList est un tableau de pointeurs, et vous pouvez y stocker un pointeur vers un byte:

var MaListe: TList;

procedure MaProcedure;
var MonByte: ^byte;
begin
new(MonByte);
MonByte^ := 45;
MaListe.Add(MonByte);
end;

A la sortie de MaProcédure, MonByte est perdu, c'est-à-dire que MonByte est supprimé de la mémoire. Mais attention, le byte créé par new existe encore. Puisqu'on l'a mis dans MaListe, on en a encore une trace. Il ne faut pas faire dispose(MonByte), car sinon MaListe contiendrait un byte qui n'existe plus en mémoire.

Autre exemple montrant un aspect très important concernant les pointeurs (lisez bien les commentaires):

var MaVariable: integer;
Pointeur1, Pointeur2: ^integer;
begin
MaVariable := 5; // On affecte MaVariable à 5.
Pointeur1 := @MaVariable; // Pointeur1 pointe sur MaVariable qui vaut 5.
Pointeur1^ := 10; // On modifie la variable pointée par Pointeur1, c'est-à-dire MaVariable, en lui affectant 10.
Pointeur2 := Pointeur1; // Pointeur2 pointe alors sur la même variable que Pointeur1 c'est-à-dire MaVariable.
Pointeur2^ := 50; // Ici je modifie la variable pointée par Pointeur2, c'est à dire Pointeur2^, qui est donc MaVariable. Pointeur1 et Pointeur2 pointant sur la même variable MaVariable, modifier Pointeur1^, Pointeur2^ ou MaVariable est équivalent, et après cette ligne on a donc Pointeur1^ = Pointeur2^ = MaVariable = 50...
end;

V Le type "Pointer"

Il existe un type pointeur qui peut pointer sur n'importe quoi, il s'agit du type Pointer. Il peut pointer sur un byte, un record, un integer, un string... n'importe quoi. Mais new et dispose ne fonctionneront pas avec, puisque new et dispose doivent savoir sur quel type de variable pointe le pointeur afin de créer ou de supprimer cette variable (en fait cela ne génère pas de message d'erreur à la compilation, mais je ne vois pas ce qu'on peut faire avec cette variable ainsi créée...).

Si vous avez besoin d'accéder à la variable pointée par une variable de type Pointer, il vous faudra effectuer un transtypage. Prenons l'exemple de la partie "Exemple avec les records". La procédure ChangeAge:

procedure ChangeAge(Personne: PPersonne; Age: byte);
begin
Personne^.Age := Age;
end;

Est équivalente à la suivante:

procedure ChangeAge(Personne: Pointer; Age: byte);
begin
PPersonne(Personne^).Age := Age;
end;

VI Attention avec la gestion de la mémoire

Lorsque vous affectez un pointeur à une variable, avec Pointeur := @Variable ou new(Pointeur), soyez sûr que le pointeur n'était pas déjà affecté à quelque chose avant. Si le pointeur pointait déjà sur une variable existante, et que cette variable n'est plus référencée par aucun pointeur, elle sera perdue. Pensez à utiliser dispose lorsque cela est nécessaire si vous n'avez plus besoin d'une variable créée avec new.

A l'inverse, lorsque vous accédez à une variable pointée (Pointeur^), vérifiez que le pointeur pointe bien sur une variable existante. Si vous copiez une adresse de variable d'un pointeur à un autre, par exemple: Pointeur1 := Pointeur2, vous devez savoir vers quoi pointe Pointeur2. S'il ne pointe pas sur une variable effectivement existante, Pointeur1^ sera inutilisable puisqu'il ne pointera sur rien lui non plus.
Si vous faites:
Pointeur1 := Pointeur2;
dispose(Pointeur2);
Alors Pointeur1 pointera sur la variable pointée par Pointeur2; mais comme vous faites dispose par la suite, Pointeur2 pointera vers une adresse où il n'y a plus de variable. Et comme Pointeur1 contient la même adresse (puisque vous venez de faire Pointeur1 := Pointeur2), Pointeur1 ne pointera lui aussi que sur du vide.

Si deux pointeurs pointent sur la même variable, par exemple avec:
Pointeur1 := @MaVariable;
Pointeur2 := @MaVariable;
Ou bien:
Pointeur1 := @MaVariable;
Pointeur2 := Pointeur1;
Ou bien encore:
new(Pointeur1); // Appelons MaVariable la variable ainsi créée, c'est-à-dire Pointeur1^, bien qu'elle n'aie en fait pas de nom.
Pointeur2 := Pointeur1;
Alors modifier Pointeur1^ modifiera MaVariable, et donc aussi Pointeur2^. De même, modifier MaVariable modifiera aussi Pointeur1^ et Pointeur2^, puisque tout cela aura la même signification. Par contre, modifier Pointeur1 ne modifie pas Pointeur2, ni Pointeur2^ ni MaVariable. C'est un des principaux intérêts des pointeurs.

Il existe un valeur, nil, qui, affectée à un pointeur, permet d'indiquer que ce pointeur ne pointe sur rien. Ainsi vous pouvez utiliser un if pour savoir si un pointeur pointe sur nil ou non; s'il pointe sur nil, vous êtes sûr qu'il n'y a pas de variable au bout. Par contre, s'il ne pointe pas sur nil, cela ne veut pas forcément dire qu'il y a effectivement une variable utilisable au bout... le pointeur contient bien une adresse, mais peut-être qu'à cette adresse, il n'y a rien!

Pour affecter un pointeur à nil:
Pointeur := nil;
Et pour tester s'il vaut nil:
if Pointeur = nil then
Cela parait évident, mais c'est pour éviter de voir quelque chose de complètement faux comme "Pointeur^ := nil" (à moins que Pointeur pointe sur un pointeur...).
Attention, lorsque vous affectez un pointeur à la valeur nil, la variable qu'il contenait avant sera peut-être perdue (si elle a été créée par un new et si aucun autre pointeur ne pointe vers elle). Faites dispose avant le nil.

VII Les classes

Il faut savoir que les classes sont des pointeurs. Lorsque vous déclarez une variable de type TForm par exemple:
var Form: TForm;
Vous avez en fait créé un pointeur sur un ensemble de données et de procédures formant une Form. La Form en elle-même (les données) n'a pas été créée. Pour la créer, il faut faire:
Form := TForm.Create(...)
Cette expression joue le même rôle que "new" avec les pointeurs: vous créez une instance de la classe TForm, à l'aide de "TForm.Create", puis vous affectez l'adresse de cette instance à la variable Form, grâce à "Form := ".

Les même précautions sont donc à prendre avec les classes qu'avec les pointeurs: si vous abandonnez une classe sans faire "Free" (l'équivalent de "dispose") vous perdrez l'espace qu'occupe la classe en mémoire. Vous ne pourrez plus accéder à cette classe alors qu'elle existe encore (à moins de posséder l'adresse de la variable dans un autre pointeur ou une autre variable de type TForm).
Si vous ne faites pas le Create, vous n'aurez pas de classe en mémoire et vous aurez une violation d'accès si vous tentez d'accéder au contenu de Form.
Si vous affectez une variable de type classe à une autre, par exemple:
Form1 := Form2;
Vous copiez en fait l'adresse de la seconde Form dans la première, et les deux variables pointent sur la même Form. Modifier Form1.Caption (par exemple) modifiera donc aussi Form2.Caption puisqu'il s'agit de la même variable.

Aussi, contrairement aux variables "classiques", si vous faites:

procedure MaProcedure(Form: TForm);
begin
Form.Caption := 'Coucou';
end;

Alors toute Form passée en paramètre à MaProcédure sera modifiée. Ca n'aurait pas été le cas si TForm n'avait pas été un pointeur: la variable aurait été recopiée et modifier Form dans MaProcedure n'aurait eu aucun effet sur la variable passée en paramètre.

Enfin, comme pour un pointeur, on peut affecter une classe à la valeur nil et cela aura le même rôle qu'avec un pointeur. Et, comme pour un pointeur, si vous affectez une classe à nil alors qu'elle contient une variable existante, cette variable risquera d'être perdue. Faites Free avant le nil.

Les classes sont donc des pointeurs, mais Delphi masque cet aspect pour vous faciliter la vie (et éviter, entre autres, tous les symboles @ et ^). Mais connaître cet aspect sur les classes vous donnera de gros avantages: vous comprendrez certaines de vos anciennes erreurs, et vous trouverez de nouveaux moyens d'utiliser ces classes.

Conclusion

Bien qu'il soit assez difficile de voir et maîtriser toutes les facettes des pointeurs simplement en lisant un article, si vous y arrivez de nombreuses possibilités s'offriront alors à vous en matière de programmation, notament au niveau de l'organisation de votre programme. Le parallèle établi avec les classes vous permettra de mieux comprendre leur fonctionnement.

 

 

 


Reply

LuciferStar
blink.gif

Bonjour!

Reply



Got an Opinion! Express your Views! (no registration):-
Add your Reply/ Opinion/ Views/ Comments/ Suggestion/ Questions/ Queries etc.
Posts with decent grammar & English will be accepted and please refrain from profanities.
For asking a Question, We recommend you to sign-up (for free) so that you can track the topic easily.

Nature of your Post*: Opinion/ Reply/ Comments
Question/Query
Feedback to us.
       
Name   Email
Title/Question*

(Maximum characters: 10,000)
You have characters left.
Confirm Code:

Recent Queries:-
  1. les pointeurs difficile - 56.88 hr back. (1)
  2. delphi tlist - 83.62 hr back. (2)
Similar Topics

Keywords : les pointeurs


    Looking for les, pointeurs

Searching Video's for les, pointeurs
advertisement



Les Pointeurs



 

 

 

 

ADD REPLY / Got an Opinion! Remove these ADs! RAPID SEARCH! Free Web Hosting [X]
Express your Opinions, Thoughts or Contribute more info. to help others.
Ask your Doubts & Queries to get answers, So that "Together We can help others!"
Register FREE for AD-FREE forum, Create your own topics, Ask Questions, track topics, setup subscriptions & notifications and Get a Free Website w/ Email and FTP.
500MB Space *No Ads*, CPanel, FTP, PHP, MySQL, EMails - 100% FREE