Cours de Turbo Pascal 7


précédentsommairesuivant

Chapitre 24 - Pointeurs

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.

La déclaration d'un pointeur ne réserve aucune mémoire pour la variable pointée.
Cette mémoire, il faudra l'allouer dynamiquement
(plus loin dans ce chapitre).

Jusqu'alors nous avons 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 :

 
Sélectionnez

Var Somme : Integer;
   { Réservation de 2 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 6 octets (3 * 2) dans la mémoire }

Var Tab1, Yab2 : Array [0..10,0..10] of Integer;
   { Réservation de 484 octets (2 * 11 * 11 * 2) dans la mémoire }

Type Personne = Record
                  Nom, Prenom : String [20];
                  Age : Byte;
                  Tel : Integer;
                end;
Var Client, Fournisseur : Personne;
   { Réservation de 90 octets (2 * (2 * 21 + 1 + 2)) 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 renverrait 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, quelle que soit la taille 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ée.
Mais qu'est-ce qu'est-ce qu'une adresse mémoire ? C'est en fait un enregistrement comprenant deux nombres de type Word (2 fois 2 octets font bien 4), qui représentent respectivement l'adresse du segment de donnée utilisé et l'indice (le déplacement) du premier octet servant à coder la variable à l'intérieur de ce même segment (un segment étant un bloc de 65536 octets). Cette taille de segment implique qu'une variable ne peut pas dépasser la taille de 65536 octets, et que la taille de l'ensemble des variables globales ne peut pas dépasser 65536 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 dans la zone mémoire des variables globales, mais plutôt dans le tas. Puisque la pile, normalement destinée aux variables des sous-programmes, est trop petite (16 Ko), on utilise donc le tas, dont la taille peut atteindre 640 Ko.

Déclaration

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 :

 
Sélectionnez

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 (à noter 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, quelle que soit sa valeur, occupe toujours le même espace. Le compilateur sachant à l'avance combien 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.

Accès à la variable pointée

Tout ça c'est très bien mais comment fait-on pour accéder au contenu de la variable pointée par le pointeur ? Il suffit d'utiliser l'identificateur du pointeur à la fin duquel on rajoute un accent circonflexe.

Exemple :

 
Sélectionnez

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.

 
Sélectionnez

Type Tableau = Array [1..100] Of Real;
PTableau = ^Tableau;
Var P : PTableau;

Ici, on déclare un type Tableau qui est un tableau de 100 Real. On déclare aussi un type de pointeur PTableau pointant vers le type Tableau. C'est-à-dire que toute variable de type PTableau, contiendra 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. Quant à 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 Real. 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] est une opération invalide, qui déclenche une erreur du compilateur.

New et Dispose

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ée 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 pas forcément pour toute la durée de celui-ci) 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 absolument indispensable de lui réserver dynamiquement de la mémoire comme suit :

 
Sélectionnez

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 :

 
Sélectionnez

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 l'on veut !).

Une variable allouée dans le tas par New contient n'importe quoi, jusqu'à ce que lui ait affecté une valeur !

 
Sélectionnez

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 * 2 = 200 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 affecter des valeurs à la variable comme suit :

 
Sélectionnez

GogoGadgetAuTableau^[i,j] := 3;

Toutes les opérations possibles concernant les affectations de variables, ou leur utilisation dans des fonctions, sont valables pour les variables pointées par des pointeurs.

Il est bien entendu impossible de travailler sur la variable pointée par le pointeur sans l'avoir auparavant allouée !

 
Sélectionnez

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

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

 
Sélectionnez

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é (enregistrement). La ligne d'instruction Pixel2^ := Pixel1^; signifie qu'on égalise champ à champ les variables Pixel1 et Pixel2.

Si les symboles ^ avaient été omis, cela n'aurait pas provoqué d'erreur mais cela aurait eu une tout autre signification : Pixel2 := Pixel1; signifierait 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.

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.

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, une erreur fatale vous rappellera à l'ordre !

 
Sélectionnez

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);
    { Seul l'élément d'indice 1 dans le tableau est initialisé }
    { Les éléments 2 à 10 contiennent n'importe quoi (rappel) }    
  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 :

 
Sélectionnez

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. }

GetMem et FreeMem

Il existe des procédures similaires au couple New et Dispose :

 
Sélectionnez

GetMem(Pointeur,Taille_Memoire);

Cette procédure réserve un nombre d'octets en mémoire égal à Taille_Memoire au pointeur Pointeur. Taille_Memoire est donc la taille de la variable pointée par le pointeur Pointeur.

 
Sélectionnez

FreeMem(Pointeur,Taille_Memoire);

Cette fonction supprime de la mémoire la variable pointée par le pointeur Pointeur, qui occupait Taille_Memoire octets.

Si vous utilisez New pour allouer une variable dynamique, il faudra utiliser Dispose et non pas FreeMem pour la désallouer.
De même, si vous utilisez GetMem pour l'allouer, il faudra utiliser FreeMem et non pas Dispose pour la désallouer.

 
Sélectionnez

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 remarqué que ce programme Exemple29f est exactement le même que Exemple29e, mis à part qu'il utilise le couple GetMem et FreeMem au lieu des traditionnels New et Dispose. C'est un peu moins pratique à 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 vous décidez de faire entrer au clavier la borne supérieure du tableau.
Voir le programme suivant :

 
Sélectionnez

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
  Write('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.

Nil

Nous l'avons vu en long et en large, un pointeur contient une adresse. Mais comment indiquer qu'il ne contient... aucune adresse ? Il existe une adresse qui ne pointe vers rien : Nil (on rencontre souvent NULL dans d'autres langages). Il peut en effet être utile de tester qu'un pointeur pointe vers quelque chose ou bien vers rien, par exemple lorsque l'on travaille avec des listes chaînées. Pour schématiser la structure d'une liste chaînée, il s'agit d'une chaîne d'éléments contenant non seulement une valeur mais aussi l'adresse de l'élément suivant.

Exemple :

 
Sélectionnez

Type pElement = ^tElement;
     tElement = Record
                  Valeur : Integer;   { Valeur de l'élément }
                  Suivant : pElement; { Pointe vers l'élément suivant dans la liste chaînée }
                end;

Le dernier élément de la liste chaînée doit pointer vers... rien. On lui affectera donc la valeur Nil :

 
Sélectionnez

Var Element : pElement;

BEGIN
  ...
  New(Element);
  Element^.Valeur := ...;
  Element^.Suivant := Nil;
  ...
END.

Pour tester qu'un pointeur pointe vers quelque chose ou rien, c'est tout simple :

 
Sélectionnez

if p = Nil
   then
     { Pointe vers rien }
   else
     ...

précédentsommairesuivant

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2001-2013 Hugo Etievant. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.