Après toutes ces observations sur lhéritage des classes, le lecteur se demande peut-être « comment ça marche » . Il nest pas nécessaire de le savoir en pratique (il suffit de savoir que ça marche en effet), mais cela peut être utile à loccasion. Nous expliquons ci-après comment Turbo C++ implante les classes (il peut y avoir des variations selon les compilateurs) ; pourquoi les instances de classes contenant des méthodes virtuelles prennent deux octets de mémoire de plus que les autres ; pourquoi certaines opérations sont impossibles.
Prenons dabord le cas simple suivant :
class A { int a1; public : // ... méthodes };class B : A { int b1; public : // ... méthodes };
La configuration en mémoire dune instance de B
est alors la suivante (chaque petit carré représente un octet) :
La partie grisclair représente ce qui est hérité de la classe A
, tandis que la partie blanche indique ce qui est défini directement dans B
.
Si une méthode de A
est appelée, elle reçoit comme les autres ladresse de lobjet par lintermédiaire du pointeur this
. Or, vu lordre dans lequel les champs sont placés, ce pointeur indique en mémoire la partie « instance de A
» de lobjet ; de ce fait, les méthodes de A
fonctionnent exactement de la même façon sur une instance de A
ou de B
, et nutilisent dans ce dernier cas que la partie gris clair.
Compliquons un peu les choses en supposant que B
a des méthodes virtuelles :
class B : A { int b1; public : virtual void b2(); virtual void b3(); void b4(); };
Dans ce cas, les instances de B
sont représentées différemment en mémoire :
Chaque instance contient à présent un pointeur caché sur une table fixe (il en existe une seule pour toute la classe B
) qui contient les adresses des méthodes virtuelles. Lorsque le compilateur rencontre un appel à b2
par exemple, il regarde ce pointeur caché dans this
(ou dans lobjet qui appelle b2
), puis laugmente dautant que nécessaire (ici 0) pour être sur la bonne méthode ; ayant ainsi ladresse de la méthode, il ne reste plus quà y sauter.
Ce processus sappelle lien dynamique ou lien tardif (en anglais late binding). On notera que les méthodes non virtuelles en sont exclues (b4
).
Nous allons voir comment cela fonctionne plus exactement en imaginant une nouvelle classe :
class C : B { int c1; public : void b2(); void b3(); virtual void c2(); };
Cette classe recouvre les deux méthodes virtuelles de B
. Une instance de C
a lallure suivante :
Le pointeur caché na maintenant plus la même valeur que dans les instances de B
: il pointe sur une nouvelle table particulière à C
, et dans laquelle les adresses des méthodes recouvertes figurent à la place de celles de B
. Lorsque le compilateur rencontrera un appel à b2 avec une instance de C
, il ira chercher dans cette table-ci (sans le savoir, car il exécute exactement le même travail quavant), et passera donc dans la bonne méthode recouverte C::b2. Ceci explique le fonctionnement des méthodes virtuelles : le pointeur caché est identique pour deux instances dune même classe, mais différent pour deux classes distinctes ; de ce fait, il caractérise la classe à laquelle appartient linstance, et permet donc de choisir la bonne méthode.
Compliquons encore le jeu avec un héritage multiple :
class D : B { int d1; public : virtual void d2(); void b3(); };class E : D, C { int e1; public : void b3(); void c2(); };
Lallure dune instance de D
est la suivante :
On remarque que, comme la classe D
na pas recouvert la méthode virtuelle b2, cest ladresse de B::b2 qui figure en première place dans la table.
Jusquà présent nous navons augmenté la taille des objets que de deux octets au maximum. Il nen est plus de même avec lhéritage multiple. Voici lallure en mémoire dune instance de E
:
Dans ce cas, il y a deux pointeurs cachés, dans chacune des deux instances de base C
et D
contenues dans E
. Les tables sur lesquelles ils pointent sont semblables à ce quelles étaient dans C
et D
, sauf que les méthodes recouvertes de E
y remplacent celles de C
ou D
. On notera que la méthode E::b3 figure deux fois dans la table de E
, parce quelle recouvre à la fois C::b3 et D::b3.
Lorsquon appelle une méthode de D
avec une instance de E
, ce nest pas le pointeur this
qui est passé, mais celui que nous avons nommé « this bis
» , qui correspond au début de D
en mémoire.
Lorsquon utilise un héritage virtuel, la situation est beaucoup plus complexe. Supposons que les classes C
et D
aient été déclarées en héritage virtuel de B
:
class C : virtual B { ... } class D : virtual B { ... }
Dans ce cas, lallure dune instance de C
en mémoire est bien différente :
Trois pointeurs sont ajoutés ; le premier, en tête, indique lemplacement dans linstance du début de la partie héritée de B
(cet emplacement variera dans les classes dérivées de C
). À la fin de lobjet, un pointeur désigne une table formellement identique à celle de B
, mais avec les adresses des méthodes virtuelles recouvertes. Au milieu, un troisième pointeur donne les adresses des méthodes virtuelles de C
(dans lordre de déclaration) ; les méthodes b2
et b3
sont ici remplacées par b2
* et b3
*, qui sont identiques à ceci près quun petit bout de code avant fait remplacer this
par le pointeur de tête, afin que les méthodes aient la bonne adresse.
Lallure de D
est assez semblable, sauf que b2
, qui nest pas recouverte dans D
, napparaît pas dans la première table :
On a à présent ceci dans E
:
La partie initiale correspond à C
; elle comprend le pointeur de tête sur la base héritée de B
, le champ c1
et un pointeur sur les trois méthodes virtuelles de C
, toutes trois recouvertes dans E
; notons que la partie B
de C
nexiste plus (pas de duplication). La suite correspond à D
: on trouve le pointeur de tête sur la partie B
, le champ d1
et un pointeur sur les deux méthodes virtuelles de D
, dont une recouverte dans E
(b3
). Vient ensuite le nouveau champ e1
; puis enfin la partie héritée de B
, en un seul exemplaire, avec à la fin un pointeur sur les méthodes virtuelles de B
, toutes deux modifiées dans E
(une directement par E
, lautre indirectement par C
). Tout cela est compliqué par le fait que les méthodes doivent être augmentées de petits bouts de code destinés à récupérer la bonne adresse de this
. Ladresse « this bis
» est celle qui est utilisée pour les méthodes de D
.
On retiendra surtout quil sagit dun processus complexe, dans lequel il est préférable de ne pas intervenir, et que la taille des objets est difficile à prévoir (utiliser lopérateur sizeof
pour la connaître).
Précédent | Sommaire | Suivant |