I) GENERALITES SUR LES POINTEURS

 

PREAMBULE :

Lorsqu'on déclare dans un programme en pascal une variable, c'est pour permettre au compilateur de réserver de la place en mémoire pour cette variable. Autrement dit, la réservation de place en mémoire se fait avant le début de l'exécution du programme, ce qui peut poser un problème : imaginez que vous ayez par exemple besoin de deux tableaux ne pouvant tenir simultanément en mémoire ; vous n'en avez par ailleurs pas besoin simultanément, mais les déclarations se font en début de programme...

Aucune solution???

Vous allez me dire que ce qui serait bien, c'est de pouvoir allouer de la mémoire d'abord au premier tableau, puis libérer la place quand il ne sert plus afin de la réserver pour le deuxième tableau...

Hé bien tout ceci est possible grâce aux pointeurs... Cela porte le doux nom de...

 

1) Allocation dynamique de la mémoire

Bon, il est temps de commencer à parler un peu technique... Ce que j'ai décris plus haut est effectivement possible mais nécessite tout de même un peu de préparation...en effet, il faut "typer" le pointeur avant de pouvoir l'utiliser ; voici comment procéder :

(je donne l'exemple et j'explique ensuite !)

        TYPE Mon_Type_de_donnée = INTEGER ;   (* par exemple ! *)
             Pointeur_sur_mon_type = ^Mon_type_de_donnée;

        VAR ptr:Pointeur_sur_mon_type;

Ce que nous avons fait jusqu'à présent n'est que donner un type d'objet sur lequel le pointeur va pointer... des entiers en l'occurence... notez que Mon_type_de_donnée pourrait aussi bien être du type ARRAY ou encore RECORD (mais on verra ça plus loin...) Attention au "^" qui est très important... il indique qu'il s'agit d'un type sur lequel on va pointer et non un type que l'on va utiliser...

Ptr est donc un pointeur destiné à pointer sur un entier; Il est important de comprendre que ptr n'est pas un entier mais en quelque sorte une "flèche" qui va pointer sur une zone de la mémoire contenant un entier. Voyons comment on l'utilise...

        BEGIN
   1       ptr:=NIL;
   2       new(ptr);
   3       writeln('donnez un entier:');
   4       readln(ptr^);
   5       writeln('vous avez donné :',ptr^);
   6       dispose(ptr);
   7       ptr:=NIL;
        END.

 

COMMENTAIRES :

La 1ère ligne est plus ou moins inutile mais une bonne habitude à prendre est d'initialiser ses pointeurs à NIL ; Les pointeurs non initialisés sont dits "pointeurs errants" et peuvent provoquer des plantages si on les utilise... NIL est concrètement une zone réservée de la mémoire qui ne contient rien.

La 2ème ligne crée une nouvelle variable dynamique à une adresse qu'elle se débrouille pour trouver puis elle affecte au pointeur ptr cette adresse afin que nous puissions utiliser la variable dynamique qui s'y trouve. C'est l'instruction NEW qui se charge de tout ça. Notez que c'est cette instruction qui est l' "instruction magique"... C'est elle comme vous le voyez qui permet de réserver de la mémoire alors que le programme est déjà lancé (d'où le nom d'Allocation Dynamique de la Mémoire...).

Pour utiliser une métaphore, on peut dire que NEW crée une boite de la taille d'un entier et indique sa position grâce au pointeur ptr. Notez qu'à partir du moment où le pointeur se voit affecter une adresse par NEW, il n'est plus considéré comme pointeur errant, d'où la quasi-inutilité de la ligne 1.

Nous apprenons dans les 4ème et 5ème lignes comment utiliser la variable fraîchement créée sur laquelle pointe ptr... Il suffit pour cela de la déréférencer en lui adjoignant le symbole ^.

Lorsqu'on déréférence un pointeur, ce n'est plus le pointeur que l' on considère, mais la variable sur laquelle il pointe. Pour reprendre la métaphore précédente, ptr indique la position de la boite et ptr^ désigne le contenu de la boite... Ce concept de déréférenciation est très important et est source de nombreuses erreurs pour les débutants (qui essayeraient par exemple d'affecter une valeur entière au pointeur au lieu de l'affecter à la variable dynamique désignée par le pointeur), alors relisez ce paragraphe jusqu'à l'avoir bien compris.

Si vous avez compris, vous voyez alors que la 4éme ligne affecte un entier à la variable dynamique pointée (syn. de désignée) par ptr, et que la 5ème ligne lit son contenu. Une variable dynamique se comporte exactement comme une variable classique si ce n'est sa syntaxe et sa façon d'y accéder un peu 'zarb'.

IMPORTANT : NE JAMAIS DEREFERENCER UN POINTEUR VALANT NIL (cela provoque immanquablement une erreur!)

La ligne 6 libère l'espace alloué en ligne 2 par NEW grâce à l'instruction DISPOSE. A partir de ce moment, la variable dynamique pointée par ptr n'existe plus... elle est tout simplement détruite. Ce qu'elle contenait est par conséquent perdu et ptr pointe à nouveau n'importe où...d'où l'utilité de le réaffecter à NIL en ligne 7.

Pour finir, quelques exemples de syntaxes selon les types des variables sur lesquelles un pointeur ptr pointe :

      Si ptr pointe sur une variable dynamique de type ARRAY[1..9],
      il faudra faire ptr^[15] pour accéder au 15ème élément du
      tableau sur lequel ptr pointe..

      Si ptr pointe sur une variable dynamique de type

      Record
        x,y:integer;
      end;

      il faudra faire ptr^.x et ptr^.y pour accéder aux différents
      champs de la structure.

 

2) Mais où sont elles stockées, ces variables dynamiques?

Bonne question, je me remercie de l'avoir posée... Les variables dynamiques sont stockées sur le tas (HEAP en anglais) à ne pas confondre avec la pile (STACK en anglais) (La pile sert à gérer les appels et les retours des fonctions et procédures récursives). Allez d'ailleurs voir dans les options du Pascal (TP ou BP) la section memory ; vous constaterez que la place maximum allouée au tas est de 640Ko (tiens, ça ne vous rappelle rien?), ce qui laisse tout de même de la marge... mais ce n'est pas une raison pour la gaspiller, ce qui veut dire qu'il faut utiliser DISPOSE sur les variables dont on ne se sert pas le plus rapidement possible.

Je tiens à attirer votre attention sur la façon dont le tas est géré : supposons que vous remplissiez le tas avec des SHORTINT (codés sur 8 bits) puis que vous en supprimiez un sur deux... Essayez ensuite de créer une variable dynamique de type INTEGER... ça ne marchera pas pour cause de manque de mémoire... et pourtant, me direz-vous, 320Ko devraient suffire! Pourtant non... Le problème vient du fait que les données du tas doivent être disposées de façon continue et ne peuvent être fragmentées (donc hors de question de couper un integer de 16 bits en deux morceaux de 8 bits !). Ce qu'il faudrait faire est une sorte de défragmentation... Mon Pascal ne gère pas ça automatiquement mais peut-être que certains compilateurs le font... Je vous laisse le soin de faire une routine pour gérer ça... (il suffit en gros de reprendre toutes les variables, les détruire, puis les recréer...).

Je n'aborderai pas dans cet article les tests de mémoire disponible sur le tas... sachez seulement qu'il existe deux fonctions utiles, une qui donne la mémoire totale disponible (MemAvail), et une autre qui donne le plus grand bloc continu disponible (MaxAvail)... à utiliser dans les traitements d'erreurs avant d'utiliser NEW(pointeur), qui peut provoquer une erreur si la mémoire disponible est insuffisante.

 

3) Mise en garde contre les passages de paramètres

Comme vous le savez, il existe deux façons de passer des paramètres à une procédure ou fonction : Soit on les passe par adresse, soit on les passe par valeur. Par adresse (dans le cas où on utilise VAR avant le paramètre), le contenu de la variable peut être modifié par la fonction. Par valeur, une copie du paramètre est faite, et c'est sur cette copie que la fonction travaille, la valeur de l'original ne pouvant donc être modifiée.

Le passage de pointeurs comme paramètres ne déroge pas à cette règle mais les conséquences sont plus subtiles et peuvent causer des erreurs pénibles à détecter. Ceci est dû à l'erreur fréquente citée plus haut. Même si on passe un pointeur par valeur, c'est une copie du pointeur qui est faite, et non pas une copie de la variable dynamique sur laquelle il pointe ; ce qui provoque la chose suivante : il est tout à fait possible de modifier la valeur d'une variable dynamique dont le pointeur à été passé par valeur.

        EX :    Program erreur_fréquente;

                TYPE : Mon_type_...
                       Pointeur_sur... (* cf plus haut *)

                procedure modifie_par_surprise(ptr:Pointeur_sur_mon_type);
                begin
                  ptr^:=ptr^+1;
                end;

                var pointeur : Pointeur_sur_mon_type;

                begin
                  new(pointeur);
                  pointeur^:=5
                  writeln('Valeur de la variable avant :',pointeur^);
                  modifie_par_surprise(pointeur);
                  writeln('Valeur de la variable après :',pointeur^);
                  dispose(pointeur);
                  pointeur:=NIL;
                end.

Le programme donnera comme valeur successivement 5 et 6... Comme quoi il faut plutôt être prudent...