I. Introduction▲
Bonjour et bienvenue dans la 11e partie du curriculum de bas niveau C/C++. Il était temps ? Absolument !
La dernière fois, nous avons vu les bases de la Définition de Type Utilisateur : Comment les structures, les classes, et les unions sont organisées en mémoire ; et (quelques-unes) des implications de l'alignement mémoire sur ce tableau.
Dans la 11e partie, nous allons voir comment l'héritage affecte ce tableau, en particulier l'implication sur l'agencement mémoire des types dérivés et aussi leur comportement pendant la construction et la destruction.
Nous allons retirer l'héritage multiple et le mot clé virtual
de ce tableau pour commencer.
II. Avant que nous commencions▲
Je vais partir du principe que vous avez déjà lu les articles précédents de la série, mais je vais aussi mettre directement les liens sur tous les termes importants ou les concepts que vous pourriez avoir besoin de connaître pour comprendre ce que vous lisez. Je me rends utile comme ça.
Je vais faire une autre hypothèse importante, c'est que vous êtes déjà très familiarisé avec le langage C++ et à l'aise avec les fonctionnalités du langage que nous allons détailler, ainsi que les limitations d'utilisation liées à ces fonctionnalités, etc. Si j'ai besoin de démontrer quelque chose qui sorte de l'ordinaire, je l'expliquerai - ou, au minimum, je donnerai un lien vers l'explication.
Dans cette série, j'explique ce qui se produit avec un code simple et non optimisé de débogage Win32 généré par le compilateur VS 2010 - bien que les détails diffèrent sur d'autres plates-formes (et probablement avec les autres compilateurs) le balayage général du code devrait être fondamentalement le même - parce que c'est de l'assembleur qui a été généré par un compilateur C++ - et donc suivre les mêmes exemples donnés ici avec un débogueur de source / désassembleur de la plate-forme de votre choix devrait vous fournir le même aperçu que celui que nous avons ici.
Avec cela en tête, au cas où vous les auriez loupés, voici les liens vers les articles précédents de la série :
- Programme d'étude sur le C++ bas niveau n°1
- Programme d'étude sur le C++ bas niveau n°2
- Programme d'étude sur le C++ bas niveau n°3
- Programme d'étude sur le C++ bas niveau n°4
- Programme d'étude sur le C++ bas niveau n°5
- Programme d'étude sur le C++ bas niveau n°6
- Programme d'étude sur le C++ bas niveau n°7
- Programme d'étude sur le C++ bas niveau n°8
- Programme d'étude sur le C++ bas niveau n°9
- Programme d'étude sur le C++ bas niveau n°10
Je ne vais pas mentir - ce n'est pas facile à lire.
III. Classe contre Structure : un doux rappel▲
Les mots clés C++ struct
et class
définissent des types qui sont identiques au niveau de l'implémentation et de ce que vous pouvez faire avec eux (la seule différence est au niveau du langage : le niveau de visibilité par défaut s'il n'est pas spécifié est private
pour les classes, et public
pour les structures). Ainsi, tandis que je vais utiliser le mot clé class
tout au long de cet article, veuillez considérer comme acquis le fait que tout ce dont nous parlons ici s'applique également aux types définis en utilisant le mot clé struct
.
IV. Que se passe-t-il quand je dérive d'un autre type ?▲
Donc, qu'est-ce qui ce passe quand vous dérivez un type défini par l'utilisateur d'un autre type intégré ?
Clairement, les données membres que vous spécifiez dans les déclarations doivent aller quelque part, et il en va de même pour toutes celles spécifiées dans le(s) type(s) dont vous dérivez.
Au niveau du C++, il n'y a rien d'autre que la norme pour vous dire comment ça fonctionne - et rien d'autre que de regarder ce qui se passe avec le code généré par le compilateur que vous utilisez pour vous l'indiquer précisément.
Comme dans le dernier article, nous allons nous appuyer fortement sur le flag 007 secret du compilateur franchement génial /d1reportSingleClassLayout afin de nous dire exactement comment le (Visual Studio 2012 win32 x86) compilateur a décidé d'organiser nos structures d'exemples en mémoire.
Il est maintenant temps de regarder un exemple de code, et, plutôt que de vous laisser traverser le processus usuel et alambiqué de création d'un projet, j'en ai gentiment créé un pour vous.
Le fichier zip dans ce lien contient une solution VS2010 avec un seul projet et un fichier .cpp amoureusement initialisé pour lancer le code ci-dessous, qui est dans 00_Inheritance.cpp.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
class
CTestBase
{
public
:
int
_iA;
int
_iB;
}
;
class
CTestDerived : public
CTestBase
{
public
:
int
_iC;
int
_iD;
}
;
int
main(int
argc, char
*
argv[])
{
return
0
;
}
Quand vous compilez ce projet, vous devriez obtenir ce qui suit dans votre fenêtre de sortie de « Compilation » (la magie de /d1reportSingleClassLayout !) :
1> class CTestBase size(8):
1> +---
1> 0 | _iA
1> 4 | _iB
1> +---
1>
1> class CTestDerived size(16):
1> +---
1> | +--- (base class CTestBase)
1> 0 | | _iA
1> 4 | | _iB
1> | +---
1> 8 | _iC
1> 12 | _iD
1> +---
En regardant ça, cela devrait être assez évident que les données membres de CTestDerived ont juste été concaténées à la fin de l'agencement mémoire de CTestBase – et, plus important, que l'agencement mémoire de CTestBase à l'intérieur de CTestDerived est identique à cela quand elle n'est pas une classe de base.
C'est aussi simple que ça ! (Pour certaines définitions de « ça » et « simple ».)
Armé de cette information provenant de l'article précédent :
« Une garantie est donnée dans les spécifications des langages C et C++ sur le fait que l'adresse mémoire de chaque membre est plus grande que celle du membre déclaré avant lui (voir cet article de Stack Overflow, en anglais, pour plus de détails sur la formulation). »
Il est évident que depuis que CTestDerived hérite de tous les membres de CTestBase, ses membres doivent apparaître après ceux de CTestBase en mémoire.
Je me rappelle quand j'avais à me l'expliquer - peu après avoir commencé mon premier travail dans l'industrie en tant que jeune diplômé - j'ai eu comme un temps d'arrêt dans ma tête, parce que l'information que je venais de recevoir était tellement évidente que je ne pouvais pas croire que je ne l'avais jamais sue.
V. C'est aussi simple, pourquoi faire un article là-dessus ?▲
Bonne question !
Le fait que l'agencement mémoire d'un type est identique dans toutes les situations est requis par la norme (et de plus par logique) ; regardons pourquoi…
En premier lieu, téléchargez et ouvrez le fichier de projet VS2010 zippé ; il contient le code ci-dessous dans 01_InheritanceWhithFunctions.cpp :
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.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
class
CTestBase
{
public
:
int
_iA;
int
_iB;
CTestBase( int
iA, int
iB )
:
_iA( iA )
, _iB( iB )
{}
int
SumBase( void
)
{
return
_iA +
_iB;
}
}
;
class
CTestDerived
:
public
CTestBase
{
public
:
int
_iC;
int
_iD;
CTestDerived( int
iA, int
iB, int
iC, int
iD )
:
CTestBase ( iA, iB )
, _iC ( iC )
, _iD ( iD )
{}
int
SumDerived( void
)
{
return
_iA +
_iB +
_iC +
_iD;
}
}
;
int
main(int
argc, char
*
argv[])
{
CTestBase cTestBase ( argc, argc +
1
);
CTestDerived cTestDerived( argc, argc +
1
, argc +
2
, argc +
3
);
return
cTestBase.SumBase() +
cTestDerived.SumBase() +
cTestDerived.SumDerived();
}
Mettez un point d'arrêt sur l'instruction return
du main, puis compilez et lancez la configuration de compilation release.
La première chose à noter est que les agencements mémoire affichés par la fenêtre de sortie lors de la compilation ne sont pas affectés par l'ajout de ces fonctions.
C'est ce à quoi vous vous attendiez. Comme nous le savons, ces appels aux fonctions membres non virtuelles sont résolus lors de la compilation, tout comme les ordinaires non membres et les fonctions statiques membres.
Depuis que CTestDerived est dérivée de CTestBase, nous savons grâce à notre haut niveau de connaissance du C++ que nous pouvons appeler les deux fonctions sur une instance de CTestDerived ; ce que nous cherchons à l'heure actuelle, c'est comment cela est implémenté.
Quand le point d'arrêt est atteint, faites un clic droit et choisissez « Atteindre le code machine ».
J'ai collé la partie dont je souhaite discuter ci-dessous…
(N.B. : pour obtenir le même désassembleur que celui-ci, vous devriez avoir les Options d'Affichage suivantes cochées dans la fenêtre désassembleur : ‘Afficher le code source', ‘Afficher les numéros de lignes', ‘Afficher les adresses', et ‘Afficher les noms de symboles').
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
44
: return cTestBase.SumBase() +
cTestDerived.SumBase() +
cTestDerived.SumDerived();
0129109A
lea
ecx
,[cTestDerived]
0129109D
call
CTestDerived::SumDerived (1291060h
)
012910A2
lea
ecx
,[cTestDerived]
012910A5
mov
esi
,eax
012910A7
call
CTestBase::SumBase (1291020h
)
012910AC
lea
ecx
,[cTestBase]
012910AF
add
esi
,eax
012910B1
call
CTestBase::SumBase (1291020h
)
012910B6
pop
edi
012910B7
add
eax
,esi
Nous avons précédemment étudié que la convention d'appel win32 pour les fonctions membres (‘thiscall') passe this
aux fonctions membres dans le registre ecx
.
Corrélativement, vous noterez que les adresses de CTestBase et CTestDerived sont en train d'être stockées dans ecx
en utilisant lea
(‘load effective address') immédiatement avant d'appeler leur fonction membre.
En particulier, notez que l'adresse de CTestDerived est passée non altérer à l'intérieur de ecx
quand on appelle la fonction de la classe de base CTestBase::
SumBase. Rappelez-vous cela pour plus tard (et pour l'article suivant !).
Donc, allons regarder le désassembleur pour CTestBase::
SumBase et CTestDerived::
SumDerived. J'ai tendance à parcourir pas à pas le désassembleur et entrer dans celles-ci, mais positionner des points d'arrêt à l'intérieur de celles-ci est plus sûr.
2.
3.
4.
5.
6.
7.
14
: int
SumBase( void )
15
: {
16
: return _iA +
_iB;
01291020
mov
eax
,dword
ptr
[ecx
+
4
]
01291023
add
eax
,dword
ptr
[ecx
]
17
: }
01291025
ret
2.
3.
4.
5.
6.
7.
8.
9.
33
: int
SumDerived( void )
34
: {
35
: return _iA +
_iB +
_iC +
_iD;
01291060
mov
eax
,dword
ptr
[ecx
+
0Ch
]
01291063
add
eax
,dword
ptr
[ecx
+
8
]
01291066
add
eax
,dword
ptr
[ecx
+
4
]
01291069
add
eax
,dword
ptr
[ecx
]
36
: }
0129106B
ret
Nous pouvons voir que tous les déplacements depuis ecx
utilisés dans les deux fonctions correspondent à l'agencement mémoire que nous avons dans le résultat de la compilation pour le type auquel appartient la fonction.
Puisque _iA et _iB sont à la même position à l'intérieur de CTestBase et CTestDerived (c'est-à-dire 0 et 4 respectivement), CTestBase::
SumBase peut être appelé sans danger sur les instances de CTestDerived.
Nous savons déjà que c'est possible, depuis notre compréhension haut niveau du C++, mais maintenant nous connaissons l'implémentation détaillée qui rend cela possible.
Bien que les spécificités du désassembleur puissent probablement différer d'une plate-forme à une autre, les principes qui sous-tendent son fonctionnement eux ne le devraient pas.
VI. Résumé▲
Pour résumer ce que nous avons établi à ce jour :
- Dans une fonction membre, une donnée membre d'une classe est accédée via des déplacements depuis le pointeur
this
. - Ces déplacements sont constants à la compilation et sont cuits dans le code assembleur pour les fonctions membres.
- Cela signifie que l'agencement mémoire des membres d'une classe donnée doit toujours être identique ou les fonctions membres ne fonctionneront pas.
Si nous poursuivons cette logique, nous pouvons voir que :
- L'agencement mémoire de la classe B qui hérite d'une autre classe A doit contenir les membres de la classe A dans le même agencement mémoire que la classe A.
- L'agencement mémoire de n'importe quelle classe A est identique, indépendamment du fait que ce soit une instance de A, ou si elle est incluse dans la mémoire d'un type dérivé de A.
- Note : ce comportement est requis par la norme, et (plus significativement) par logique.
Finalement, il s'ensuit que (parce que chaque membre d'une structure doit avoir une adresse supérieure aux membres déclarés avant lui) :
- L'espace mémoire supplémentaire requis par la classe dérivée B sera concaténé à la fin de l'agencement mémoire de sa classe de base A.
C'est tout pour le moment - la fois prochaine nous verrons comment l'héritage multiple affecte ce tableau.
Je sais que c'est court, mais cela signifie juste que la fois prochaine arrivera plus rapidement.
VII. Épilogue - Pour ceux qui voudraient ce que j'ai changé dans les propriétés du projet▲
Il y a très peu de changements par rapport aux propriétés du projet console Win32 VS2010 par défaut dans le projet que j'ai zippé pour cet article.
Les changements permettent de faire en sorte que les optimisations de la configuration de compilation release laissent la structure du code tranquille (c'est-à-dire, ne pas dépouiller ou « plier » les fonctions pour réduire la taille de l'EXE, prévenir la transformation des fonctions en fonction inline), et préviennent l'insertion de code de « vérification débogue » étranger (rend l'appel des fonctions plus lent, et le code moins facile à suivre en désassembleur).
- Désactiver « Optimisation de l'ensemble du programme » (Propriété de configuration Général).
- Désactiver « Expansion des fonctions inline » (Propriété de configuration C/C++ Optimisation).
- Désactiver « Vérification de base à l'exécution » (Propriété de configuration C/C++ Génération de code).
- Se débarrasser des en-têtes précompilés pour rationaliser le nombre de fichiers (Propriété de configuration C/C++ En-têtes précompilés).
- Désactiver « Activation du repli COMDAT » (Propriété de configuration Éditeur de lien Optimisation).
Cela permet principalement à l'assembleur de la configuration release d'avoir la même structure que les appels de fonction WRT de débogage.
De plus, j'utilise le paramètre argc du main comme entrée du code, et retourne une valeur calculée à partir de cela de sorte que l'optimiseur ne puisse pas prendre des valeurs d'entrée et de sortie constantes.
Si vous utilisez des entrées constantes, ou si vous ne retournez pas une valeur calculée à partir de celles d'entrées alors il est assez difficile de convaincre l'optimiseur de ne pas optimiser l'EXE entier avec « return
0
»… ;)
VIII. Remerciements▲
Merci (encore) à Bruce - roi (ou au moins duc) des conseils et des examens par un pair.
Cet article est la traduction de l'article « C/C++ Low Level Curriculum Part 11: Inheritance » écrit en anglais par Alex Darby. Alex Darby a aimablement autorisé l'équipe C/C++ de Developpez.com à traduire et diffuser son article en français.
Nous tenons à remercier Winjerome et LittleWhite pour la relecture technique ainsi que ced et jpcheck pour la relecture orthographique de cette traduction.