XI. Pointeurs▲
Nous passons aux choses sérieuses avec ce chapitre dédié à un sujet souvent décrit
comme complexe mais indispensable pour tout programmeur digne de ce nom. Les
pointeurs sont en effet constamment utilisés dans les projets les plus importants car
ils apportent des possibilités énormes et très diverses. Sans ce concept de
pointeurs, de nombreux problèmes ne pourraient pas être transcrits et résolus sur
ordinateur.
Ce chapitre commencera par vous expliquer ce que sont les pointeurs, puis diverses
utilisations des pointeurs seront passées en revue. Nous parlerons entre autres des
pointeurs de record et de transtypage dans ce chapitre.
Si vous êtes habitué au rythme de progression du guide, vous vous sentirez
probablement un peu secoué(e) dans ce chapitre, car la notion de pointeur est assez
délicate et demande de gros efforts d'apprentissage et d'assimilation au début. Dés
que vous aurez "compris le truc", ce que je m'engage à faciliter, tout ira mieux.
XI-A. Présentation de la notion de pointeur▲
La notion de base de pointeur est assez simple : il s'agit d'une variable qui stocke
l'emplacement en mémoire d'un autre élément (qui est le plus souvent une variable).
Cette première variable est dite de type pointeur, et est tout simplement nommée
"pointeur". Le type Pascal Objet utilisé pour le type pointeur est tout simplement :
pointer. Lorsqu'on voit cela pour la première fois, on peut douter de l'intérêt d'un
tel assemblage, mais rassurez-vous si c'est votre cas, vous changerez très vite
d'avis. L'autre élément dont nous parlions est de type indéterminé, ce qui nous
apportera quelques petits soucis, vite résolus au §XI-A-2.
Le contenu de cette variable "pointeur" importe en fait bien peu. Ce qui importe,
c'est l'élément "pointé" par la variable "pointeur". Une variable de type pointeur,
ou plus simplement un pointeur, contient une adresse. Une adresse est tout
simplement un nombre qui repère un emplacement en mémoire (la mémoire des
ordinateurs est découpée en octets. Ces octets sont numérotés, et une adresse est
tout simplement un numéro d'octet). Mais ne vous souciez pas trop de ces détails
techniques, sauf si vous comptez approfondir le sujet (ce qui ne se fera pas ici).
Voici un schémas illustrant la notion de pointeur et expliquant deux ou trois autres
choses sur la mémoire.
Si vous êtes curieux de savoir ce qu'est exactement une adresse, lisez le contenu de l'encadré ci-dessous, sinon, passez directement à la suite.
Tout sur les adresses mémoire :
Une adresse mémoire consiste en fait en un nombre. La convention adoptée partout
est de noter ce nombre en hexadécimal. Etant donné que la mémoire des ordinateurs
est aujourd'hui limitée à 4 gigaoctets, on a besoin d'un nombre entre 0 et 2^32-1,
soit entre 0 et FFFFFFFF en hexadécimal.
Si vous ne connaissez pas l'hexadécimal, sachez que c'est un système de numérotation
à 16 chiffres au lieu de 10 : les chiffres sont 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B,
C, D, E, F au lieu des 10 chiffres traditionnels de 0 à 9 ; un nombre hexadécimal
tel que 1F vaut 1 x 16 + 15 (valeur de F) = 31 en notation décimale classique.
En Pascal Objet, les nombres hexadécimaux sont notés avec le préfixe $. Le nombre
1F sera écrit : $1F.
Un tel nombre est stockable sur 4 octets (256 x 4 = 2^32), ce qui explique la taille
d'une variable pointeur, qui contient ce nombre. Ce nombre hexadécimal est nommé
adresse dans le cadre de l'utilisation avec les pointeurs. Une adresse permet donc
de repèrer un octet quelconque dans la mémoire par son numéro.
Petite précision enfin : lorsque l'élément pointé fait plus d'un seul octet, ce
qui est évidemment très souvent le cas, c'est l'octet de début (le premier de la
suite d'octets qui stockent l'élément) qui est pointé.
XI-A-1. nil▲
Le titre de ce paragraphe vous a certainement un peu étonné... non, il ne s'agit
pas du fleuve, mais bien encore des pointeurs !
Vous savez maintenant ce qu'est un pointeur : un élément qui stocke l'emplacement
d'un autre élément. Parfois, pourtant, un pointeur ne pointera rien du tout (aucun
emplacement en mémoire, donc rien). Ce cas particulier de pointeur, dont vous
découvrirez rapidement l'extrème utilité, est appelé pointeur nul, ou plus
simplement nil. nil est en fait la valeur d'un pointeur qui ne pointe
rien. Voici un extrait de code (il est inscrit, comme d'habitude, dans une
procédure répondant à l'événement OnClick d'un bouton) :
procedure
TfmPrinc.Button1Click(Sender: TObject);
var
Machin: pointer
;
begin
Machin := nil
;
end
;
Ce code, qui ne fait pas grand chose, illustre plusieurs choses. En tout premier,
on a déclaré une variable de type pointeur nommée "Machin" (vous voudrez bien
excuser mon manque d'inspiration). Cette variable est désormais une variable comme
les autres. L'unique instruction de la procédure est une affectation. On affecte
nil à la variable "Machin" (Vous remarquerez en passant que nil est un mot
réservé du langage Pascal Objet). Ceci prouve que nil est une valeur valable pour
un pointeur, puisqu'on l'affecte à une variable de type pointeur. Après cette
affectation, "Machin" est un pointeur nul, car sa valeur est nil : "Machin" ne
pointe nulle part.
Ce sera désormais une préoccupation à avoir en tête lorsque vous manipulerez un
pointeur : avant de vous soucier de l'élément pointé par le pointeur, il vous
faudra savoir si le pointeur pointe effectivement quelque chose. Le moyen pour
savoir cela sera de comparer le pointeur à nil. Si la comparaison donne "True",
c'est que le pointeur est nul, donc ne donne rien, et si elle donne "False", c'est
que le pointeur pointe quelque chose.
XI-A-2. Eléments pointés par un pointeur▲
Nous nous sommes surtout attardé pour l'instant sur les pointeurs et leur contenu.
Il est temps de nous occuper un peu des éléments pointés par les pointeurs, qui
sont tout de même importants.
Le type pointer utilisé jusqu'à présent a un inconvénient majeur : il déclare en
fait un pointeur vers n'importe quoi (une donnée de n'importe quel
type, ce type étant tout à fait inconnu, et c'est là le problème) : même si
on peut accèder à l'information pointée par le pointeur, il est impossible de
savoir de quel type est cette information, ce qui la rend inutilisable
concrètement. Etant donné que lorsqu'on utilise un pointeur, c'est le plus souvent
pour se servir de l'élément pointé et non du pointeur en lui-même, nous sommes en
face d'un problème grave : si un élément est effectivement pointé par un pointeur,
et qu'on ne sait pas si c'est une chaîne de caractères ou un entier, on va aux
devants de gros embêtements. Il va nous falloir un moyen de savoir de quel type de
données est l'information pointée.
La solution est trouvée en utilisant des types de pointeurs spécifiques aux
informations pointées. On utilisera dans la réalité des "pointeurs vers des
entiers" ou des "pointeurs vers des chaînes de caractères" plutôt que des
"pointeurs vers n'importe quoi". Le principe pour se débarrasser de l'ambiguité sur
l'élément pointé est en effet de dire tout simplement : ce type de pointeur
pointera toujours vers le même type de donnée (entier, chaîne de caractères,
...).
Pour utiliser ces différents types de pointeurs spécifiques, nous
utiliserons de vrais types de données Pascal Objet. Le type pointeur vers un
autre type de donnée pourra facilement être défini et utilisé dans tout programme.
Ces types de pointeurs ne souffreront pas de l'ambiguité sur le type des
données pointées car le type de la donnée pointée est déterminé à l'avance
par le type du pointeur.
Le type "pointeur vers type" s'écrit ^type en Pascal Objet. Il suffit de
précèder le nom du type de donnée pointée par un accent circonflexe pour déclarer
un pointeur vers ce type de donnée. Attention, le type en question ne peut PAS être
un type composé du genre "array[1..100] of byte" : il faut que le type soit un
identificateur. Pour résoudre ce problème, il faut passer par des déclarations de
types intermédiaires. Cela sera vu au §
XI-B-1 Par exemple, pour déclarer une variable de type "pointeur vers un
integer", il faudra utiliser le type ^integer (integer est bien un identificateur).
Pour vous en convaincre, substituez dans le code précédent le mot "pointer" par
"^integer" :
procedure
TfmPrinc.Button1Click(Sender: TObject);
var
Machin: ^integer
;
begin
Machin := nil
;
end
;
Dans cet exemple, la variable "Machin" est toujours un pointeur, même si elle n'est
pas déclarée avec le type "pointer". Elle est déclarée comme un pointeur vers un
entier. Le seul type possible pour l'élément pointé par "Machin" sera donc par
conséquent "integer".
Rassurez-vous, dans la pratique, on utilisera en fait assez peu cette forme de
déclaration. On déclarera plutôt un nouveau nom de type (section type), et
on déclarera ensuite les variables ou les paramètres en utilisant ce nouveau type.
Voici ce que cela donnera pour l'exemple ci-dessus :
type
PInteger = ^integer
;
var
Machin: PInteger;
begin
Machin := nil
;
end
;
Cet exemple déclare un type nommé "PInteger" qui est le type "pointeur vers un entier". Toute variable ou paramètre de ce type sera un pointeur vers un entier. Ce type est utilisé pour déclarer la variable "Machin", ce qui la déclare comme pointeur vers un entier, comme dans l'extrait de code précédent.
Remarque : notez la première lettre (P) du type PInteger : c'est une convention et un bon repère de nommer les types pointeurs en commençant par un P, contrairement aux autres types personnalisés qui sont commencés par la lettre T.
XI-A-3. Gestion de l'élément pointé par un pointeur▲
Jusqu'ici, nous nous sommes surtout intéressé aux pointeurs, au type de l'élément
pointé, mais pas encore à l'élément pointé lui-même. Cet élément, qui sera souvent
d'un type courant tel qu'un entier, une chaîne de caractères, un énuméré ou même
d'un type plus complexe comme un record (c.f. §
XI-A-4 Pointeurs de record), a une particularité par rapport aux données
classiquement stockées dans des variables : il est géré dynamiquement et
manuellement, c'est-à-dire qu'au fil de l'éxécution du programme, il nous
faudra nous occuper de tâches habituellement invisibles. Ces tâches sont assez
déroutantes pour les débutants mais vous vous y habituerez vite, c'est juste une
habitude à prendre.
L'élément pointé par un pointeur devra donc être géré manuellement. La gestion de
l'élément pointé se divise en deux tâches simples : son initialisation et sa
destruction.
Avant de pouvoir utiliser l'élément pointé, il faudra le créer. Cette étape
d'initialisation est indispensable, car elle réserve en fait de la place en mémoire
pour l'élément pointé par le pointeur, et fait pointer le pointeur vers cet espace
fraichement réservé. Utiliser un pointeur vers un espace non réservé (non
initialisé), c'est-à-dire un pointeur non créé, conduira inévitablement à un
plantage de l'application, voire parfois de Windows (sans vouloir vous effrayer
bien entendu, mais c'est la triste et dure vérité).
Une fois l'utilisation d'un pointeur terminée, il faudra penser à le
détruire, c'est-à-dire libèrer l'espace réservé à l'élément pointé par le
pointeur. Cette seconde étape, parfois oubliée, est indispensable lorsqu'on a
initialisé le pointeur. Oubliez ce genre d'étape et votre application viendra
s'ajouter à la liste des pollueurs : après leur passage, de la mémoire a disparu,
et cela entraîne une dégradation des performances, voire un plantage de Windows au
bout d'un certain temps.
Le schémas ci-dessous tente de résumer cette délicate notion de gestion de
l'élément pointé. Prenez le temps de bien le lire :
Dans ce schémas, les étapes 1, 3 et 5 sont des états du pointeur : les étapes 1 et 5 sont en fait identiques car dés lors que le pointeur est "détruit", il peut à nouveau être initialisé pour être réutilisé. Les étapes 2 et 4 vous sont dévolues. L'étape 2 est l'initialisation, qui précède l'utilisation. L'étape 4 est la destruction, qui suit l'utilisation. Toute utilisation de l'élément pointé est tout simplement impossible avant l'étape 2 et après l'étape 4 car cet élément pointé n'existe pas.
Remarque : ce schéma ne s'applique qu'aux pointeurs vers des types déterminés créés par les types ^type et non pas au type pointeur général créé par le type pointer.
Venons-en maintenant au code Pascal Objet proprement dit. L'initialisation d'un
pointeur se fait au moyen de la procédure "New". Cette procédure admet un unique
paramètre qui est le pointeur à initialiser. L'appel de cette procédure avec un
pointeur en paramètre correspond à l'étape 2 du schémas ci-dessus. Comme
initialisation et destruction vont de paire, voyons tout de suite la méthode de
destruction : elle s'effectue par l'appel de la procédure "Dispose". Cette
procédure, comme "New", prend un unique paramètre qui est le pointeur à "détruire".
L'appel de "Dispose" avec un pointeur en paramètre correspond à l'étape 4 du
schémas ci-dessus. "New" et "Dispose" ne peuvent initialiser et détruire que des
pointeurs vers des types déterminés, et non des pointeurs de type pointer.
L'exemple ci-dessous utilise les nouvelles notions :
procedure
TfmPrinc.Button1Click(Sender: TObject);
type
PInteger = ^integer
;
var
Machin: PInteger;
begin
new(Machin);
{ ici, on peut utiliser l'élément pointé par Machin }
dispose(Machin);
end
;
La procédure ci-dessus déclare "Machin" comme pointeur vers un entier. La première
instruction initialise "Machin". Avant cette instruction, l'élément pointé par
"Machin" n'existe même pas. Après l'appel à New, l'élément pointé par "Machin" est
utilisable (nous allons voir comment juste après). La deuxième instruction détruit
le pointeur : l'élément pointé est détruit et le pointeur ne pointe donc plus vers
cet élément qui a été libéré de la mémoire.
Nous allons maintenant voir comment utiliser l'élément pointé par un pointeur.
XI-A-4. Utilisation de l'élément pointé par un pointeur▲
Une fois un pointeur initialisé, il est enfin possible d'utiliser l'élément pointé. Ceci se fait en ajoutant le suffixe ^ (accent circonflexe) au pointeur (^ est appelé opérateur d'indirection car il permet de passer du pointeur à l'élément pointé). Supposons que nous ayons un pointeur vers un entier, nommé "Machin". Dans ce cas, "Machin^" désigne l'élément pointé, et est donc dans ce cas précis un entier. Il peut être utilisé comme toute variable de type entier, à savoir dans une affectation par exemple, ou en tant que paramètre. Voici un exemple :
procedure
TfmPrinc.Button1Click(Sender: TObject);
type
PInteger = ^integer
;
var
Machin: PInteger;
begin
new(Machin);
Machin^ := 1
; { affectation d'une valeur entière à un entier }
ShowMessage(IntToStr(Machin^)); { utilisation comme n'importe quel entier }
dispose(Machin);
end
;
Dans cet exemple, le pointeur est tout d'abord initialisé. Vient ensuite une
affectation. Comme "Machin" est de type pointeur vers un entier, "Machin^", qui est
l'élément pointé par le pointeur, est un entier, à qui on peut affecter une valeur
entière. L'instruction suivante transmet la valeur de l'élément pointé par
"Machin", soit toujours un entier, à IntToStr. Cette procédure, qui accepte les
entiers, ne fait évidemment pas de difficulté devant "Machin^", qui vaut 1 dans
notre cas. Le message affiché contient donc la chaîne "1". Enfin, le pointeur est
détruit.
Voilà, il n'y a rien de plus à savoir pour utiliser l'élément pointé : utilisez-le
lorsqu'il est initialisé, au moyen de l'opérateur d'indirection ^ et le tour est
joué. Pour les types d'éléments pointés les plus simples, c'est tout ce qu'il faut
savoir. Dans le paragraphe suivant, nous allons nous attarder sur les types plus
complexes comme les tableaux et les enregistrements (record) utilisés avec les
pointeurs.
XI-B. Etude de quelques types de pointeurs▲
Ce paragraphe est destiné à vous fournir une aide concernant des types plus complexes que les types simples tels que les entiers ou les chaînes de caractères : aucune règle particulière n'est à appliquer à ces éléments de types plus complexes, mais leur utilisation avec les pointeurs demande un peu plus d'entrainement et de dextérité. Je vous propose donc quelques explications et exercices, le sujet étant intarrissable.
XI-B-1. Pointeurs de tableaux▲
Intéressons-nous au cas où l'on doit utiliser un pointeur vers un tableau. Nous
évoquerons également les tableaux de pointeurs, bien entendu à ne pas confondre
avec les pointeurs de tableaux.
Voici un tableau :
array[1..10] of integer
Si vous suivez mal ce que j'ai dit plus tôt, un pointeur vers ce genre de tableau
serait du type :
^array[1..10] of integer
Ce qui n'est pas le cas car "array[1..10] of integer" n'est pas un identificateur ! Oubliez vite la ligne ci-dessus, elle ne sera pas acceptée par Delphi et provoquera une erreur. Voici un bloc type correct qui déclare un type pointeur vers un tableau :
type
TTabTest = array
[1
..10
] of
integer
;
PTabTest = ^TTabTest;
Tout élément déclaré de type PTabTest serait un pointeur vers un élément de type
TTabTest. Or TabTest étant un tableau, un élément de type PTabTest est bien un
pointeur vers un tableau.
Voici maintenant un exemple démontrant l'utilisation d'un tel pointeur de tableau :
procedure
TfmPrinc.Button1Click(Sender: TObject);
type
TTabTest = array
[1
..100
] of
integer
;
PTabTest = ^TTabTest;
var
References: PTabTest;
indx: integer
;
begin
New(References);
References^[1
] := 0
;
for
indx := 1
to
100
do
References^[indx] := Trunc(Random(10000
));
ShowMessage('Référence n°24 : '
+IntToStr(References^[24
]));
Dispose(References);
end
;
Cet exemple utilise un type tableau de 100 entiers nommé "TTabTest". Un type
pointeur vers ce tableau est ensuite déclaré et nommé "PTabTest". Le test utilise
deux variables : un entier et un pointeur de tableau de type PTabTest. La première
instruction initialise le pointeur. La deuxième instruction permet de voir comment
on accède à un élément du tableau pointé par "References" : on commence par accèder
à l'élément pointé, soît le tableau, grâce à l'opérateur d'indirection ^.
"References^" étant l'élément pointé par "References", c'est un tableau d'entiers
(type TTabTest). On peut donc accèder ensuite à sa première case en utilisant la
syntaxe habituelle des tableaux, à savoir en ajoutant le numéro de case entre
crochets. Ainsi, "References^[1]" est la première case du tableau pointé par
"References" et est donc de type entier.
L'instruction en elle-même est une affectation. Le tableau étant un tableau
d'entiers, chaque case du tableau est un entier et peut donc recevoir une valeur
entière. On affecte donc la valeur 0 à "References^[1]" qui est une case du
tableau. Cette instruction est en fait une initialisation de la case du tableau.
L'instruction suivante passe à la vitesse supérieure : on utilise une boucle
for pour initialiser directement TOUTES les cases du tableau pointé par
"References". A cet effet, une variable "indx" parcours les valeurs de 1 à 100 et
est utilisé comme numéro de case pour le tableau, ce qui permet donc de balayer une
à une toutes les cases du tableau. Une affectation est faite à chaque fois, et ce
avec une valeur aléatoire entière entre 0 et 10000.
L'instruction suivante (appel de ShowMessage) est un exemple d'utilisation de
tableau de pointeur : on transmet la valeur de la case 24 du tableau pointé, donc
un pointeur, à la fonction "IntToStr" qui la convertit en chaîne. Le reste est très
conventionnel et permet d'afficher un message convenable. Enfin, la dernière
instruction permet de "détruire" le pointeur (drôle de manière de dire les choses
puisque ce n'est pas le pointeur mais l'élément pointé qui est détruit,
conformément au schémas présenté plus haut).
XI-B-2. Pointeurs d'enregistrements (record)▲
Les pointeurs d'enregistrements sont tout simplement des types de pointeurs vers
des enregistrements (record). L'utilité de ces pointeurs est considérable et nul
doute que vous aurez de nombreuses occasions de vous en servir.
La déclaration d'un type pointeur d'enregistrement se fait comme pour tous les
types pointeurs vers un type particulier. Voici un exemple :
type
TSexePersonne = (spFeminin, spMasculin);
TPersonne = record
Nom,
Prenom,
CodePostal,
Adresse,
Ville: string
;
Sexe: TSexePersonne;
end
;
{ pointeur d'enregistrement }
PPersonne = ^TPersonne;
De tels pointeurs s'initialisent et se détruisent comme d'habitude avec New et
Dispose. Leur intérêt réside dans la gestion de la mémoire, sujet dont je vais
parler juste un petit peu. La mémoire que vous utilisez dans vos programmes
n'est pas illimitée : il faut éviter au maximum d'en utiliser de grosses
quantités sans passer par les pointeurs. Cette mémoire se décompose à peu près
en deux parties : la mémoire locale, et la mémoire globale. La première est
celle que vous utilisez lorsque vous déclarez une variable locale par exemple.
Cette mémoire est de taille très limitée. La mémoire globale, en revanche, est
virtuellement limitée à 4 gigaoctets et Windows s'occupe d'en fournir lorsque
c'est nécessaire. C'est là que les pointeurs interviennent : lorsque vous
déclarez par exemple 100 enregistrements de type TPersonne, ces enregistrements
sont stockés en mémoire locale, ce qui occupe pas mal de place. En utilisant
des pointeurs, seuls ces derniers sont stockés en mémoire locale, et les
éléments pointés le sont dans la mémoire globale.
Il faudra donc éviter cela :
array
[1
..1000
] of
TPersonne
au profit de cela :
array
[1
..1000
] of
PPersonne
la différence sst minime dans la déclaration, mais grande dans le fonctionnement et
l'utilisation. Cette dernière forme est à préfèrer dans beaucoup de cas, même si
l'utilisation de pointeurs rebute pas mal de programmeurs débutants.>
Venons-en à l'utilisation concrète de ces pointeurs d'enregistrements. En partant
du pointeur, il faut accèder à l'élément pointé en placant l'opérateur
d'indirection ^ . L'élément pointé étant un enregistrement, on peut accèder à un
membre en écrivant un point suivi du nom du membre.
Voici un rapide exemple démontrant cela :
procedure
TForm1.Button1Click(Sender: TObject);
var
PersTest: PPersonne;
begin
New(PersTest);
PersTest^.Nom := 'BEAULIEU'
;
PersTest^.Prenom := 'Frédéric'
;
PersTest^.Sexe := spMasculin;
ShowMessage(PersTest^.Nom + ' '
+ PersTest^.Prenom);
Dispose(PersTest);
end
;
Dans le code ci-dessus, une variable pointeur vers enregistrement est déclarée,
initialisée, utilisée puis détruite. L'utilisation consiste à affecter des valeurs
aux membres de l'enregistrement pointé par "PersTest". Vous pouvez au passage voir
comment cela se fait. Une autre utilisation est faite en affichant un message
comportant le nom et le prénom de la personne.
Il est également possible d'utiliser un bloc with avec de tels pointeurs,
puisque with a besoin d'un enregistrement pour fonctionner. Voici la
procédure réécrite avec un bloc with :
procedure
TForm1.Button1Click(Sender: TObject);
var
PersTest: PPersonne;
begin
New(PersTest);
with
PersTest^ do
begin
Nom := 'BEAULIEU'
;
Prenom := 'Frédéric'
;
Sexe := spMasculin;
ShowMessage(Nom + ' '
+ Prenom);
end
;
Dispose(PersTest);
end
;
Ce genre de bloc permet d'utiliser les pointeurs d'enregistrements à peu près comme
les enregistrements eux-mêmes, en ajoutant initialisation et destruction. Dans
l'exemple ci-dessus, PersTest est de type TPersonne, et convient donc bien là où on
doit avoir un enregistrement.
Les pointeurs d'enregistrements ouvrent la porte à des structures très évoluées
comme les listes chaînées ou les arbres. Ces deux notions sont des plus utiles pour
modèliser certains problèmes. Nous n'étudierons cependant pas ces notions ici, car
elles sont rendues non nécessaires par l'utilisation d'autres outils comme les
objets TList sur lesquels, par contre, nous reviendrons prochainement.
XI-C. Transtypage▲
Les pointeurs sont le domaine idéal pour commencer à vous parler de transtypage. Ne
vous laissez pas effrayer par ce mot, il signifie tout simplement "transformation de
type". Par transformation, on entend non pas une transformation de structure, mais
un passage d'un type à un autre, une conversion explicite ou implicite. Commençons
par les implicites.
Depuis longtemps, vous utilisez des transtypages implicites sans vous en rendre
compte. Le transtypage est une conversion de type : ce genre de conversion est très
utilisé dans Delphi de façon invisible, mais vous pourrez aussi vous servir de cette
possibilité.
Prenons l'exemple suivant :
procedure
TfmMain.btTestClick(Sender: TObject);
var
X: Word
;
begin
X := 123
;
ShowMessage(IntToStr(X));
end
;
Lorsque vous donnez X comme paramètre à IntToStr, vous donnez un élément de type
Word à une procédure qui accepte un paramètre de type Integer. Cette différence de
type est dans l'absolu interdite. Cela ne vous a jamais empèché de vivre jusqu'à
maintenant, et que cela ne change pas, car Pascal Objet est un langage évolué et
Delphi propose un compilateur puissant : lorsque vous donnez un paramètre du mauvais
type, le compilateur regarde s'il est possible de convertir ce que vous lui
présentez dans le bon type. Si c'est possible, le compilateur le fait parfois tout
seul, notamment dans le cas des nombres ou de certaines chaînes de caractères.
Aucune erreur n'est signalée, car Delphi fait les ajustements nécessaires dans votre
dos.
Prenons un autre exemple :
procedure
TfmMain.btTestClick(Sender: TObject);
var
S1: string
;
S2: string
[30
];
begin
S1 := 'Salut'
;
S2 := S1;
ShowMessage(S2);
end
;
Ici, l'exemple est plus frappant : dans l'affectation "S2 := S1;", on affecte à une
chaîne de longueur 30 une chaîne de longueur standard, c'est-à-dire illimitée sous
Delphi 5 ou plus. Que se passerait-il si la longueur de S1 étant 45 par exemple ?
Lorsqu'on voit une simple affectation, on ne se doute pas de ce qui se fera par
derrière : une troncature. S1 sera en effet tronquée si elle est trop longue pour
tenir dans S2. Lorsqu'on transmet S2 à ShowMessage, il y a encore divergence des
types car le type "string" est attendu alors qu'on transmet une variable de type
"string[30]". Là encore, une conversion (techniquement, un transtypage) sera
effectué.
Tout ce verbiage nous conduit à parler de transtypages, ceux-là explicites, que l'on
peut donc écrire dans le code Pascal Objet. La notion de transtypage est liée à la
notion de compatibilité de types. Certains types sont compatibles entre eux,
c'est-à-dire qu'un transtypage est possible de l'un à l'autre, mais d'autres sont
incompatibles et il n'y aurait aucune signification à effectuer de transtypages
entre de tels types (par exemple d'une chaîne de caractères à un booléen).
Tous les types pointeurs sont compatibles entre eux, ce qui fait que n'importe quel
pointeur peut être transtypé en n'importe quel autre type de pointeur. Ceci vient du
fait que seul le type de l'élément pointé différe entre les types de pointeurs, et
non la structure de stockages des pointeurs en mémoire.
Pour transtyper un élément, il faut écrire le nom du type à obtenir, suivi de
l'élément entre parenthèses (un peu comme si l'on appelait une fonction, ce qui
n'est absolument pas le cas). Les transtypages sont surtout utilisés avec les
composants qui permettent d'adjoindre aux éléments contenus un ou des pointeurs. Ces
pointeurs sont toujours de type "pointer", ce qui les rend impossibles à utiliser
directement pour pointer quelque chose. Il faudra transtyper ces pointeurs en un
type pointeur déclaré pour pouvoir accèder aux éléments pointés par le pointeur.
Pour des exemples de transtypages, referrez-vous au (futur) chapitre sur les types
abstraits de données, qui regorgera d'exemples et expliquera concrètement le
transtypage des pointeurs. Rassurez-vous entretemps car cette notion ne devrait pas
vous manquer jusque là.
XI-D. Conclusion▲
C'est déjà la fin de ce chapitre consacré aux pointeurs. Nombre de notions importantes liées aux pointeurs n'y ont pas été abordées, pour ne pas tomber dans l'excés ou le bourrage de crâne. Des notions complèmentaires et nettement plus intéressantes sont abordées au chapitre XV sur les types abstraits de données. Parmi ces notions, seront abordés le transtypage de données et le chaînage.