Licence CC BY-NC-SA

Les pointeurs I : allocation dynamique

Voici très certainement LE chapitre de la troisième partie du cours. Pourquoi ? Eh bien parce que c'est, à n'en pas douter le plus dur de tous. Les pointeurs constituent une véritable épine dans le pied de tout jeune programmeur. Non pas qu'ils soient compliqués à manipuler, non bien au contraire ! Vous connaissez déjà les opérations qui leur sont applicables. Non. Le souci, c'est qu'ils sont difficiles à conceptualiser, à comprendre.

Comment ça marche ? Je pointe sur quoi en ce moment ? À quoi ça sert ? C'est un pointeur ou un objet pointé ? Je mets un pointeur ou pas ? Au secours ! :colere: Voilà résumé en quelques questions tout le désarroi qui vous submergera sûrement en manipulant les pointeurs. C'est en tous cas celui qui m'a submergé lorsque j'ai commencé à les utiliser et ce, jusqu'à obtenir le déclic. Et, rassurez-vous, je compte bien prendre le temps de vous expliquer pour que vous aussi vous ayez le déclic à votre tour.

Ce chapitre étant long et compliqué il a donc été divisé en deux. La première moitié (celle que vous lisez actuellement) se chargera de vous expliquer les bases : qu'est-ce qu'un pointeur ? Comment cela marche-t-il ? À quoi cela ressemble-t-il en Ada ? La seconde moitié aura pour but de couvrir toutes les possibilités offertes par le langage Ada en la matière. Aussi compliqués soient-ils (et même si le langage Ada fait moins appel à eux que d'autres comme le C/C++), les pointeurs nous seront très utiles par la suite notamment pour le chapitre 10 et la partie V. N'hésitez pas à relire ce double chapitre, si besoin est, pour vous assurer que vous avez assimilé toutes les notions.

Mémoire, variable et pointeur

Notre premier objectif sera donc de comprendre ce qu'est un pointeur. Et pour cela, il faut avant tout se rappeler ce qu'est une variable !

On va pas tout reprendre à zéro quand même ? o_O

Et si ! Ou presque. Reprenons calmement et posons-nous la question suivante : que se passe-t-il lorsque l'on écrit dans la partie déclarative la ligne ci-dessous et à quoi cela sert-il ?

1
MaVariablePerso : integer ;

En écrivant cela, nous indiquons à l'ordinateur que nous aurons besoin d'un espace en mémoire suffisamment grand pour accueillir un nombre de type integer, c'est-à-dire compris entre - 2 147 483 648 et + 2 147 483 647. Mon ordinateur va devoir réquisitionner une zone mémoire pour ma variable MaVariablePerso. Et pour la retrouver, cette zone mémoire disposera d'un adresse, par exemple le n°6025. Ainsi, à cette adresse, je suis sûr de trouver MaVariablePerso et rien d'autre ! Il ne peut y avoir qu'une seule variable par adresse.

Schéma de la mémoire après déclaration d'une variable

Ainsi, si par la suite je viens à écrire « MaVariablePerso := 5 ; », alors l'emplacement mémoire n°6025 qui était encore vide (ou qui contenait d'anciennes données sans aucun rapport avec notre programme actuel) recevra la valeur 5 qu'il conservera jusqu'à la fin du programme.

Schéma de la mémoire après affectation d'une valeur à notre variable

En revanche, nous ne pourrons pas écrire « MaVariablePerso := 5.37 ; » car 5.37 est un float, et un float n'est pas codé de la même manière par l'ordinateur qu'un integer ou un character. Donc l'ordinateur sait qu'à l'adresse n°6025, il ne pourra enregistrer que des données de type integer sous peine de plantage. L'avantage des langages dits de haut niveau (tels Ada), c'est qu'ils s'occupent de gérer la réquisition de mémoire, d'éviter en amont de mauvaises affectations et surtout, ces langages nous permettent de ne pas avoir à nous tracasser de l'adresse mémoire utilisée (le n°6025). Il nous suffit d'indiquer le nom de cet emplacement (MaVariablePerso) pour pouvoir y accéder en écriture ou/et en lecture.

Or, il est parfois utile de passer non plus par le nom de cet emplacement, mais par son adresse, et c'est là qu'interviennent les pointeurs aussi appelés accesseurs en Ada! Au lieu de dire « je veux modifier/lire la variable qui s'appelle MaVariablePerso », nous devrons dire « je veux modifier/lire ce qui est écrit à l'adresse n°6025 ».

Pour comprendre ce qu'est un pointeur, mettons-nous dans la peau de l'inspecteur Derrick (la classe ! :soleil: Non ?). Il sait que le meurtrier qu'il recherche vit à l'adresse 15 rue Mozart, bâtiment C, 5ème étage appartement 34. Mais il ne connaît pas son nom. Que fait-il ? Soit il imprime quelque part dans son cerveau l'adresse du criminel (vue l'adresse, ça sera pas facile, mais… c'est Derrick ! :soleil: ), soit il utilise un bon vieux post-it pour la noter (moins glamour mais plus sérieux vu son âge). Ensuite, il se rend à l'adresse indiquée, fait ce qu'il a à faire (interrogatoire, arrestation…) termine l'enquête et youpi, il peut jeter son post-it car il a résolu une nouvelle enquête.

Et c'est quoi le pointeur dans cette histoire ? o_O

Eh bien mon pointeur c'est : le post-it ! Ou le coin du cerveau de Derrick qui a enregistré cette adresse si compliquée ! Traduit en langage informatique, cela signifie qu'une adresse mémoire est trop compliquée pour être retenue ou utilisée par l'utilisateur, mieux vaut l'enregistrer quelque part, et le plus sérieux, c'est d'enregistrer cette adresse dans une bonne vieille variable tout ce qu'il y a de plus classique (ou presque). Et c'est cette variable, contenant une adresse mémoire intéressante qui est notre pointeur.

L'adresse mémoire n°3073 est enregistrée dans une variable.

Sur mon schéma, le pointeur est donc la case n°1025 ! Cette case contient l'adresse (le n°3073) d'un autre emplacement mémoire. Un pointeur n'est donc rien de plus qu'une variable au contenu inutilisable en tant que tel puisque ce contenu n'est rien d'autre que l'adresse mémoire d'informations plus importantes. Le pointeur n'est donc pas tant intéressant par ce qu'il contient que par ce qu'il pointe.

Nous verrons par la suite que c'est tout de même un poil plus compliqué.

Le type access

Déclarer un pointeur

Le terme Ada pour pointeur est ACCESS ! C'est pourquoi on parle également d'accesseur. Malheureusement, comme pour les tableaux, il n'est pas possible d'écrire :

1
Ptr : access ;

Toujours pour éviter des problèmes de typage, il nous faut définir auparavant un type T_Pointeur indiquant sur quel type de données nous souhaitons pointer. Pour changer, choisissons que le type T_Pointeur pointera sur… des integer ! Oui je sais, c'est toujours des integer. :) Nous allons donc déclarer notre type de la manière suivante :

1
type T_Pointeur is access Integer ;

Puis nous pourrons déclarer des variable Ptr1, Ptr2 et Ptr3 de type T_pointeur :

1
Ptr1, Ptr2, Ptr3 : T_Pointeur ;

Que contient mon pointeur ?

En faisant cette déclaration, le langage Ada initialise automatiquement vos pointeurs (tous les langages ne le font pas, il est alors important de l'initialiser soi-même). Mais attention ! Ils ne pointent sur rien pour l'instant (il ne faudrait pas qu'ils pointent au hasard sur des données de type float, character ou autre qui n'auraient pas été effacées). Ces pointeurs sont donc initialisés avec une valeur nulle : NULL. C'est comme si nous avions écrit :

1
Ptr1, Ptr2, Ptr3 : T_Pointeur := Null ;

Autrement dit, ils sont vides !

Bon alors maintenant, comment on leur donne une valeur utile ?

Ooh ! Malheureux ! Vous oubliez quelque chose ! La valeur du pointeur n'est pas utile en tant que telle car elle doit correspondre à un adresse mémoire qui, elle, nous intéresse ! Or le souci, c'est que cet emplacement mémoire n'est pas encore créé ! À quoi bon affecter une adresse à notre pointeur s'il n'y a rien à l'adresse indiquée? La première chose à faire est de créer cet emplacement mémoire (et bien entendu, de le lier par la même occasion à notre pointeur). Pour cela nous devons écrire :

1
Ptr1 := new integer ;

Ainsi, Ptr1 « pointera » sur une adresse qui elle, comportera un integer !

Où dois-je écrire cette ligne ? Dans la partie déclaration ?

Non, surtout pas ! Je sais que c'est normalement dans la partie déclarative que se font les réquisitions de mémoire, mais nous n'y déclarerons que le type T_Pointeur et nos trois pointeurs Ptr1, Ptr2 et Ptr3. La ligne de code précédente doit s'écrire après le BEGIN, dans le corps de votre procédure ou fonction. C'est ce que l'on appelle l'allocation dynamique. Ce procédé permet, pour simplifier, de «créer et supprimer des variables n'importe quand durant le programme». Nous entrerons dans les détails durant l'avant-dernière sous-partie, rassurez-vous. Retenez simplement qu'à la déclaration, nos pointeurs ne pointent sur rien et que pour leur affecter une valeur, il faut créer un nouvel emplacement mémoire.

Une dernière chose, pas la peine d'écrire des Put(Ptr1) ou Get(Ptr1), car je vous ai un petit peu menti au début de ce chapitre, le contenu de Ptr1 n'est pas un simple integer (ce serait trop simple), l'adresse contenue dans notre pointeur est un peu plus compliquée et d'ailleurs, vous n'avez pas besoin de la connaître, la seule chose qui compte c'est qu'elle pointe bien sur une adresse valide.

Comment accéder aux données ?

Et mon pointeur, il pointe sur quoi ? :-°

Ben… pour l'instant sur pas grand chose à vrai dire puisque l'emplacement mémoire est vide. :( Bah oui, un pointeur ça pointe et puis c'est tout… ou presque ! L'emplacement mémoire pointé n'a pas de nom (ce n'est pas une variable), il n'est donc pas possible pour l'heure de le modifier ou de lire sa valeur. Alors comment faire ? Une première méthode serait de revoir l'affectation de notre pointeur ainsi :

1
Ptr1 := new integer'(124) ;

Ainsi, Ptr1 pointera sur un emplacement mémoire contenant le nombre 124. Le souci, c'est qu'on ne va pas réquisitionner un nouvel emplacement mémoire à chaque fois alors que l'on pourrait réutiliser celui existant ! Voici donc une autre façon :

1
2
Ptr2 := new integer ; 
Ptr2.all := 421 ;

Eh oui, je vous avais bien dit que les pointeurs étaient un poil plus compliqué. Ils comportent une « sorte de composante » comme les types structurés ! Sauf que ALL a beau ressemblé à une composante, ce n'est pas une composante du pointeur, mais la valeur contenue dans l'emplacement mémoire pointé !

Il existe une troisième façon de procéder, si l'on souhaite avoir un pointeur sur une variable ou une constante déjà connue. Mais nous verrons cette méthode plus loin, à la fin de ce chapitre.

Opérations sur les pointeurs

Quelles opérations peut-on donc faire avec des pointeurs ? Eh bien je vous l'ai déjà dit : pas grand chose de plus que ce que vous savez déjà faire ! Voire même moins de choses. Je m'explique : vous pouvez tester l'égalité (ou la non égalité) de deux pointeurs, effectuer des affectations… et c'est tout. Oh, certes vous pouvez effectuer toutes les opérations que vous souhaitez sur Ptr1.all puisque ce n'est pas un pointeur mais un integer ! Mais sur Ptr1, (et je ne me répète pas pour le plaisir) vous n'avez le droit qu'au test d'égalité (et non-égalité) et à l'affectation ! Vous pouvez ainsi écrire :

1
2
if Ptr1 = Ptr2
   then ...

Ou :

1
2
if Ptr1 /= Ptr2
   then ...

Mais surtout pas :

1
2
if Ptr1 > Ptr2
   then ...

Ça n'aurait aucun sens ! En revanche vous pouvez écrire « Ptr1 := Ptr2 ; ». Cela signifiera que Ptr1 pointera sur le même emplacement mémoire que Ptr2 ! Cela signifie aussi que l'emplacement qu'il pointait auparavant est perdu : il reste en mémoire, sauf qu'on a perdu son adresse ! Cet emplacement sera automatiquement supprimé à la fin de notre programme.

Ce que je vous dis là est une mauvaise pratique : si l'emplacement est perdu, il n'empêche qu'il prend de la place en mémoire pour rien. Il serait bon de le supprimer immédiatement pour libérer de la place en mémoire pour d'autres opérations.

Autre problème : créer deux pointeurs sur une même donnée (comme nous l'avons fait dans le dernier exemple) est à la fois inutile, ou plutôt redondant (on utilise deux pointeurs pour faire la même chose, c'est du gaspillage de mémoire) et surtout dangereux ! Via le premier pointeur, on peut modifier la donnée. Et, sans que le second n'ait été modifié, la valeur sur laquelle il pointera ne sera plus la même. Exemple :

1
2
3
4
5
Ptr2.all := 5 ;         --Ptr2 pointe sur "5"
Ptr1 := Ptr2 ;          --On modifie l'adresse contenue dans Ptr1 ! ! !
Ptr1.all := 9 ;         --Ouais ! Ptr1 pointe sur "9" maintenant ! 
                        --Sauf que du coup, Ptr2 aussi puisqu'ils 
                        --pointent sur le même emplacement ! ! !

Donc, évitez ce genre d'opération autant que possible. Préférez plutôt :

1
2
3
4
5
6
Ptr2.all := 5 ;         --Ptr2 pointe sur "5"
Ptr1.all := Ptr2.all ;  --Ptr1 pointe toujours sur la même adresse
                        --On a simplement modifié le contenu de cette adresse
Ptr1.all := 9 ;         --Maintenant Ptr1 pointe sur "9" ! 
                        --Mais comme Ptr1 et Ptr2 pointent sur 
                        --deux adresses distinctes, il n'y a pas de problème.

Une erreur à éviter

Voici un code dangereux (pourquoi à votre avis ?) :

1
2
if ptr /= null and ptr.all> 0
   then ...

Eh bien parce que si ptr ne pointe sur rien (s'il vaut NULL), lorsque vous effectuerez ce test, le programme testera également si Ptr.all est positif (supérieur à 0). Or si Ptr ne pointe sur rien du tout alors Ptr.all n'existe pas ! ! ! Nous devrons donc prendre la précaution suivante (comme nous l'avions vu avec les tableaux) :

1
2
if ptr /= null and then ptr.all> 0
   then ...

Libération de la mémoire

Un programme (un peu) gourmand

Avant de commencer cette partie, je vais vous demander de copier et de compiler le code ci-dessous :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
with ada.text_io ;    use ada.Text_IO ; 

procedure aaa is
   type T_Pointeur is access Integer ; 
   P : T_Pointeur ; 
   c : character ;
begin
   for i in 1..8 loop
      for j in 1..10**i loop
         P := new integer'(i*j) ; 
      end loop ; 
      get(c) ; skip_line; 
   end loop ; 
end aaa ;

Comme vous pouvez le voir, ce code ne fait rien de bien compliqué : il crée un pointeur auquel il affecte 10 valeurs successives, puis 100 ($10^2 = 100$), puis 1 000 ($10^3 = 1000$), puis 10 000 ($10^4 = 10000$)… et ainsi de suite. Mais l'intérêt de ce programme n'est pas là. Nous allons regarder sa consommation mémoire. Pour cela, ouvrez un gestionnaire de tâche :

  • si vous êtes sous Windows : appuyez sur les touches Alt+Ctrl+Suppr puis cliquez sur l'onglet Processus. Le programme apparaîtra en tête de liste.
  • si vous êtes sous Linux ou Mac : un gestionnaire (appelé Moniteur d'activité sous Mac) est généralement disponible dans votre liste de programmes, mais vous pouvez également ouvrir une console et taper la commande top.

Lancez votre programme aaa.exe et admirez la quantité de mémoire utilisée : environ 780 Ko au démarrage mais après chaque saisie du caractère C, la mémoire utilisée augmente de manière exponentielle (quadratique en fait :D ). Voici une capture d'écran avec 200 Mo de mémoire utilisée par ce «petit» programme (mais on peut faire bien mieux).

Consommation mémoire du programme aaa

Mais comment se fait-il qu'un si petit programme puisse utiliser autant de mémoire ?

C'est ce que nous allons expliquer maintenant avant de fournir une solution à ce problème qui ne nous est jamais arrivé avec nos bonnes vieilles variables.

Un problème de mémoire

Reprenons nos schémas précédents car ils étaient un peu trop simplistes. Lorsque vous lancez votre programme, l'ordinateur va réserver des emplacements mémoire. Pour le code du programme tout d'abord, puis pour les variables que vous aurez déclaré entre IS et BEGIN. Ainsi, avant même d'effectuer le moindre calcul ou affichage, votre programme va réquisitionner la mémoire dont il aura besoin : c'est à cela que sert cette fameuse partie déclarative et le typage des variables. On dit que les variables sont stockées en mémoire statique, car la taille de cette mémoire ne va pas varier.

À cela, il faut ajouter un autre type de mémoire : la mémoire automatique. Au démarrage, l'ordinateur ne peut connaître les choix que l'utilisateur effectuera. Certains choix ne nécessiteront pas d'avantage de mémoire, d'autres entraîneront l'exécution de sous-programmes disposant de leurs propres variables ! Le même procédé de réservation aura alors lieu dans ce que l'on appelle la pile d'exécution : à noter que cette pile a cette fois une taille variable dépendant du déroulement du programme et des choix de l'utilisateur.

Mémoire statique et pile d'exécution

Mais ce procédé a un défaut : il gaspille la mémoire ! Toute variable déclarée ne disparaît qu'à la fin de l'exécution du programme ou sous-programme qui l'a généré. Ni votre système d'exploitation ni votre compilateur ne peuvent deviner si une variable est devenue inutile. Elle continue donc d'exister malgré tout. À l'inverse, ce procédé ne permet pas de déclarer des tableaux de longueur indéterminée : il faut connaître la taille avant même l'exécution du code source ! Contraignant. :colere2:

C'est pourquoi existe un dernier type de mémoire appelé Pool ou Tas. Ce pool, comme la pile d'exécution, n'a pas de taille prédéfinie. Ainsi, lorsque vous déclarez un pointeur P dans votre programme principal, l'ordinateur réserve suffisamment d'emplacements en mémoire statique pour pouvoir enregistrer une adresse. Puis, lorsque votre programme lit :

1
P := new integer ; --suivie éventuellement d'une valeur

Celui-ci va automatiquement allouer, dans le Pool, suffisamment d'emplacements mémoires pour enregistrer un Integer. On parle alors d'allocation dynamique. Donc si nous répétons cette instruction (un peu comme nous l'avons fait dans le programme donné en début de sous-partie), l'ordinateur réservera plusieurs emplacements mémoire dans le Pool.

Allocation dynamique de mémoire

À noter qu'avec un seul pointeur (en mémoire statique) on peut réserver plusieurs adresses dans le pool (mémoire dynamique) !

Résolution du problème

Unchecked_Deallocation

Il est donc important lorsque vous manipuler des pointeurs de penser aux opérations effectuées en mémoire par l'ordinateur. Allouer dynamiquement de la mémoire est certes intéressant mais peut conduire à de gros gaspillages si l'on ne réfléchit pas à la portée de notre pointeur : tant que notre pointeur existe en mémoire statique, les emplacements alloués en mémoire dynamique demeurent et s'accumulent.

La mémoire dynamique n'était pas sensée régler ce problème de gaspillage justement ?

Bien sûr que oui. Nous disposons d'une instruction d'allocation de mémoire (NEW), il nous faut donc utiliser une instruction de désallocation. Malheureusement celle-ci n'est pas aisée à mettre en œuvre et est risquée. Nous allons devoir faire appel à une procédure : Ada.Unchecked_Deallocation ! Mais attention, il s'agit d'une procédure générique (faite pour tout type de pointeur) qui est donc inutilisable en tant que telle. Il va falloir lui créer une sœur jumelle, que nous appellerons Free, prévue pour notre type T_Pointeur. Voici le code corrigé :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
with ada.text_io ;    use ada.Text_IO ; 
with Ada.Unchecked_Deallocation ; 

procedure aaa is
   type T_Pointeur is access Integer ; 
   P : T_Pointeur ; 
   c : character ;
   procedure free is new Ada.Unchecked_Deallocation(Integer,T_Pointeur) ; 
begin
   for i in 1..8 loop
      for j in 1..10**i loop
         P := new integer'(i*j) ; 
         free(P) ; 
      end loop ; 
      get(c) ; skip_line; 
   end loop ; 
end aaa ;

Vous remarquerez tout d'abord qu'à la ligne 2, j'indique avec WITH que mon programme va utiliser la procédure Ada.Unchecked_Deallocation (pas de USE, cela n'aurait aucun sens puisque ma procédure est générique). Puis, à la ligne 8, je crée une procédure Free à partir de Ada.Unchecked_Deallocation : on dit que l'on instancie (mais nous verrons tout cela plus en détail dans la partie IV). Pour cela, il faut préciser tout d'abord le type de donnée qui est pointé puis le type de Pointeur utilisé.

Si nous avions deux types de pointeurs différents, il faudrait alors instancier deux procédures Free différentes.

Enfin, à la ligne 13, j'utilise ma procédure Free pour libérer la mémoire du Pool. Mon pointeur P ne pointe alors plus sur rien du tout et il est réinitialisé à Null. Vous pouvez compiler ce code et le tester : notre problème de «fuite de mémoire» a disparu !

Risques inhérents à la désallocation

Il est toutefois risqué d'utiliser Ada.Unchecked_Deallocation et la plupart du temps, je ne l'utiliserai pas. Imaginez que vous ayez deux pointeurs P1 et P2 pointant sur un même espace mémoire :

1
2
3
4
5
6
...
   P1 := new integer'(15) ; 
   P2 := P1 ; 
   free(P1) ; 
   Put(P2.all) ; 
...

Le compilateur considèrera ce code comme correct mais que se passera-t-il ? Lorsque P1 est désalloué à la ligne 4, il est remis à Null mais cela signifie aussi que l'emplacement vers lequel il pointait disparaît. Et P2 ? Il pointe donc désormais vers un emplacement mémoire qui a disparu et il ne vaut pas Null ! Voilà donc pourquoi on évitera la désallocation lorsque cela est possible.

Une autre méthode

Je veux bien, mais tu nous exposes un problème, tu nous donne la solution et ensuite tu nous dis de ne surtout pas l'employer ! Je fais quoi moi ? o_O

Une autre solution consiste à réfléchir (oui, je sais c'est dur :p ) à la portée de notre type T_pointeur. En effet, lorsque le bloc dans lequel ce type est déclaré (la procédure ou la fonction le plus souvent) se termine, le type T_pointeur disparaît et le pool de mémoire qu'il avait engendré disparaît lui aussi. Ainsi, pour éviter tout problème avec Ada.Unchecked_Deallocation, il peut être judicieux d'utiliser un bloc de déclaration avec l'instruction DECLARE.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
with ada.text_io ;    use ada.Text_IO ; 

procedure aaa is
   c : character ;
begin
   for i in 1..8 loop
      for j in 1..10**i loop
         declare
            type T_Ptr is access Integer ; 
            P : T_Ptr ; 
         begin
            P := new integer'(i*j) ; 
         end ; 
      end loop ; 
      get(c) ; skip_line; 
   end loop ; 
end aaa ;

Exercices

Exercice 1

Énoncé

Créez un programme Test_Pointeur qui :

  • crée trois pointeurs Ptr1, Ptr2 et Ptr3 sur des integer;
  • demande à l'utilisateur de saisir les valeurs pointées par Ptr1 et Ptr2;
  • enregistre dans l'emplacement mémoire pointé par Ptr3, la somme des deux valeurs saisies précédemment.

Je sais, c'est bidon comme exercice et vous pourriez le faire les yeux fermés avec des variables. Sauf qu'ici, je vous demande de manipuler non plus des variables, mais des pointeurs ! :p

Solution

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
WITH Ada.Text_IO ;                USE Ada.Text_IO ; 
WITH Ada.Integer_Text_IO ;        USE Ada.Integer_Text_IO ; 
WITH Ada.Unchecked_Deallocation ; 

procedure Test_Pointeur is
   type T_Pointeur is access Integer ; 
   procedure Free is new Ada.Unchecked_Deallocation(Integer, T_Pointeur) ; 
   Ptr1, Ptr2, Ptr3 : T_Pointeur ; 
begin
   Ptr1 := new Integer ; 
   Put("Quelle est la premiere valeur pointee ? ") ; 
   get(Ptr1.all) ; skip_line ; 

   Ptr2 := new Integer ; 
   Put("Quelle est la seconde valeur pointee ? ") ; 
   get(Ptr2.all) ; skip_line ; 

   Ptr3 := new Integer'(Ptr1.all + Ptr2.all) ; 
   Free(Ptr1) ;                                      --on libère nos deux premiers pointeurs
   Free(Ptr2) ; 
   Put("Leur somme est de ") ; 
   Put(Ptr3.all) ; 
   Free(Ptr3) ;                                      --on libère le troisième, mais est-ce vraiment utile ? ^^
end Test_Pointeur ;

Exercice 2

Énoncé

Deuxième exercice, vous allez devoir créer un programme Inverse_pointeur qui saisit une première valeur pointée par Ptr1, puis une seconde pointée par Ptr2 et qui inversera les pointeurs. Ainsi, à la fin du programme, Ptr1 devra pointer sur la seconde valeur et Ptr2 sur la première. Attention, les valeurs devront être saisies une et une seule fois, elles seront ensuite fixées !

Une indication : vous aurez sûrement besoin d'un troisième pointeur pour effectuer cet échange.

Solution

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
WITH Ada.Text_IO ;                USE Ada.Text_IO ; 
WITH Ada.Integer_Text_IO ;        USE Ada.Integer_Text_IO ; 
WITH Ada.Unchecked_Deallocation ; 

procedure Inverse_Pointeur is
   type T_Pointeur is access Integer ; 
   procedure Free is new Ada.Unchecked_Deallocation(Integer,T_Pointeur) ; 
   Ptr1, Ptr2, Ptr3 : T_Pointeur ; 
begin
   Ptr1 := new Integer ; 
   Put("Quelle est la premiere valeur pointee ? ") ; 
   get(Ptr1.all) ; skip_line ; 

   Ptr2 := new Integer ; 
   Put("Quelle est la seconde valeur pointee ? ") ; 
   get(Ptr2.all) ; skip_line ; 

   Ptr3 := Ptr1 ; 
   Ptr1 := Ptr2 ; 
   Ptr2 := Ptr3 ; Free(Ptr3) ;             --Attention à ne pas libérer Ptr1 ou Ptr2 !

   Put("Le premier pointeur pointe sur : ") ; 
   Put(Ptr1.all) ; Free(Ptr1) ; 
   Put(" et le second sur : ") ; 
   Put(Ptr2.all) ; Free(Ptr2) ; 
end Inverse_Pointeur ;

Ce bête exercice d'inversion est important. Imaginez qu'au lieu d'inverser des Integer, il faille inverser des tableaux d'un bon million d'éléments (des données financières ou scientifiques par exemple), chaque élément comportant de très nombreuses informations. Cela peut être extrêmement long d'inverser chaque élément d'un tableau avec l'élément correspondant de l'autre. Avec des pointeurs, cela devient un jeu d'enfant puisqu'il suffit d'échanger deux adresses, ce qui ne représente que quelques octets.

Vous êtes parvenus à la fin de la première moitié de ce double-chapitre. Comme je vous l'avais dit en introduction, si des doutes subsistent, n'hésitez pas à relire ce chapitre ou à poster vos questions avant que les difficultés ne s'accumulent. La seconde moitié du chapitre sera en effet plus complexe. Elle vous apprendra à créer des pointeurs sur des variables, des constantes, des programmes… ou à créer des fonctions ou procédures manipulant des pointeurs. Le programme est encore chargé.


En résumé :

  • Avant d'utiliser un pointeur, vous devez indiquer le type de données qui sera pointé avec le terme ACCESS.
  • Un pointeur est une variable contenant l'adresse d'un emplacement mémoire : vous devez distinguer le pointeur et l'élément pointé.
  • Modifier la valeur d'un pointeur peut engendrer la perte de ces données. Pour éviter cela utilisez un autre pointeur ou détruisez ces données à l'aide de Unchecked_Deallocation().
  • Les pointeurs sont des variables très légères, mais qui exigent d'avantage de réflexion quant à vos instructions et variables afin d'éviter un encombrement inutile de la mémoire, la perte des données ou la modification impromptue des variables.