Question

On m'a récemment demandé dans une interview au sujet de la mise en page d'objets avec des fonctions virtuelles et l'héritage multiple impliquées.
Je l'ai expliqué dans le contexte de la façon dont il est mis en œuvre sans héritage multiple impliqué (à savoir comment le compilateur a généré la table virtuelle, insérez un pointeur secret pour la table virtuelle dans chaque objet et ainsi de suite).
Il me semblait qu'il y avait quelque chose qui manquait dans mon explication.
Voici donc les questions (voir exemple ci-dessous)

  1. Quelle est la mise en page de la mémoire exacte de l'objet de la classe C.
  2. entrées de tables virtuelles pour la classe C.
  3. Dimensions (comme retourné par sizeof) d'objet de classes A, B et C (8, 8, 16 ??)
  4. Et si l'héritage virtuel est utilisé. Certes, les tailles et les entrées de table virtuelle doivent être affectées?

Exemple de code:

class A {  
  public:   
    virtual int funA();     
  private:  
    int a;  
};

class B {  
  public:  
    virtual int funB();  
  private:  
    int b;  
};  

class C : public A, public B {  
  private:  
    int c;  
};   

Merci!

Était-ce utile?

La solution

La mise en page de la mémoire et la mise en page vtable dépendent de votre compilateur. En utilisant mon gcc par exemple, ils ressemblent à ceci:

sizeof(int) == 4
sizeof(A) == 8
sizeof(B) == 8
sizeof(C) == 20

Notez que sizeof (int) et l'espace nécessaire pour le pointeur vtable peut également varier d'un compilateur de compilateur et plate-forme pour la plate-forme. La raison pour laquelle sizeof (C) == 20 et non 16 est que gcc donne 8 octets pour la sous-objet A, 8 octets pour le B subobject et 4 octets pour sa int c membre.

Vtable for C
C::_ZTV1C: 6u entries
0     (int (*)(...))0
4     (int (*)(...))(& _ZTI1C)
8     A::funA
12    (int (*)(...))-0x00000000000000008
16    (int (*)(...))(& _ZTI1C)
20    B::funB

Class C
   size=20 align=4
   base size=20 base align=4
C (0x40bd5e00) 0
    vptr=((& C::_ZTV1C) + 8u)
  A (0x40bd6080) 0
      primary-for C (0x40bd5e00)
  B (0x40bd60c0) 8
      vptr=((& C::_ZTV1C) + 20u)

Utilisation de l'héritage virtuel

class C : public virtual A, public virtual B

les modifications de mise en page à

Vtable for C
C::_ZTV1C: 12u entries
0     16u
4     8u
8     (int (*)(...))0
12    (int (*)(...))(& _ZTI1C)
16    0u
20    (int (*)(...))-0x00000000000000008
24    (int (*)(...))(& _ZTI1C)
28    A::funA
32    0u
36    (int (*)(...))-0x00000000000000010
40    (int (*)(...))(& _ZTI1C)
44    B::funB

VTT for C
C::_ZTT1C: 3u entries
0     ((& C::_ZTV1C) + 16u)
4     ((& C::_ZTV1C) + 28u)
8     ((& C::_ZTV1C) + 44u)

Class C
   size=24 align=4
   base size=8 base align=4
C (0x40bd5e00) 0
    vptridx=0u vptr=((& C::_ZTV1C) + 16u)
  A (0x40bd6080) 8 virtual
      vptridx=4u vbaseoffset=-0x0000000000000000c vptr=((& C::_ZTV1C) + 28u)
  B (0x40bd60c0) 16 virtual
      vptridx=8u vbaseoffset=-0x00000000000000010 vptr=((& C::_ZTV1C) + 44u)

En utilisant gcc, vous pouvez ajouter -fdump-class-hierarchy pour obtenir ces informations.

Autres conseils

1 chose à attendre avec l'héritage multiple est que votre pointeur peut changer lors de la coulée à un (généralement pas d'abord) la sous-classe. Quelque chose vous devriez être au courant pendant le débogage et répondre à des questions d'entrevue.

En premier lieu, une classe polymorphe présente au moins une fonction virtuelle, de sorte qu'il a une VPTR:

struct A {
    virtual void foo();
};

est compilé à:

struct A__vtable { // vtable for objects of declared type A
    void (*foo__ptr) (A *__this); // pointer to foo() virtual function
};

void A__foo (A *__this); // A::foo ()

// vtable for objects of real (dynamic) type A
const A__vtable A__real = { // vtable is never modified
    /*foo__ptr =*/ A__foo
};

struct A {
    A__vtable const *__vptr; // ptr to const not const ptr
                             // vptr is modified at runtime
};

// default constructor for class A (implicitly declared)
void A__ctor (A *__that) { 
    __that->__vptr = &A__real;
}

Remarque: C ++ peut être compilé à un autre langage de haut niveau comme C (comme cfront a) ou même à un sous-ensemble C ++ (C ++ ici sans virtual). Je mets __ dans les noms générés par le compilateur.

Notez que ceci est un simpliste modèle où RTTI est pas pris en charge; réels compilateurs ajouter des données dans le vtable pour soutenir typeid.

Maintenant, une simple classe dérivée:

struct Der : A {
    override void foo();
    virtual void bar();
};

classe de base non virtuelle (*) sont sous-objets comme sous-objets sous-objets membres, mais en sous-objets membres sont des objets complets, soit. leur véritable (dynamique) type est leur type déclaré, sous-objets de classe de base ne sont pas complets, et leur véritable changement de type pendant la construction.

(*) bases virtuelles sont très différentes, comme des fonctions membres virtuelles sont différentes des membres non virtuels

struct Der__vtable { // vtable for objects of declared type Der
    A__vtable __primary_base; // first position
    void (*bar__ptr) (Der *__this); 
};

// overriding of a virtual function in A:
void Der__foo (A *__this); // Der::foo ()

// new virtual function in Der:
void Der__bar (Der *__this); // Der::bar ()

// vtable for objects of real (dynamic) type Der
const Der__vtable Der__real = { 
    { /*foo__ptr =*/ Der__foo },
    /*foo__ptr =*/ Der__bar
};

struct Der { // no additional vptr
    A __primary_base; // first position
};

Ici « première position » signifie que le membre doit être le premier (les autres membres pourraient être commandés): ils sont situés à l'offset zéro afin que nous puissions reinterpret_cast pointeurs, les types sont compatibles; à l'offset non nul, nous aurions à faire des ajustements de pointeur avec l'arithmétique sur char*.

L'absence d'ajustement peut ne pas sembler une grosse affaire en terme de code généré (quelques-unes ajouter des instructions asm immédiates), mais cela signifie beaucoup plus que cela, cela signifie que ces pointeurs peuvent être considérés comme ayant différents types: un objet de type A__vtable* peut contenir un pointeur vers Der__vtable et être traité comme étant soit un Der__vtable* ou un A__vtable*. Le même objet de pointeur sert de pointeur vers une A__vtable des fonctions relatives à des objets de type A et en tant que pointeur vers une Der__vtable des fonctions relatives à des objets de type Der.

// default constructor for class Der (implicitly declared)
void Der__ctor (Der *__this) { 
    A__ctor (reinterpret_cast<A*> (__this));
    __this->__vptr = reinterpret_cast<A__vtable const*> (&Der__real);
}

Vous voyez que le type dynamique, tel que défini par le VPTR, les changements au cours de la construction que nous attribuons une nouvelle valeur à la VPTR (dans ce cas particulier l'appel au constructeur de la classe de base ne fait rien utile et peut être optimisé loin, mais il est pas le cas avec les constructeurs non triviales).

Avec l'héritage multiple:

struct C : A, B {};

Une instance de C contiendra un A et un B, comme ça:

struct C {
    A base__A; // primary base
    B base__B;
};

Notez que seul un de ces sous-objets de la classe de base peut avoir le privilège de siéger à l'offset zéro; ce qui est important à bien des égards:

  • conversion de pointeurs vers d'autres classes de base (upcasts) aura besoin d'un ajustement; à l'inverse, upcasts besoin des ajustements opposés;

  • cela signifie que lorsque vous faites un appel virtuel avec une classe de base pointeur, le this a la valeur correcte pour l'entrée dans le dérivé overrider classe.

le code suivant:

void B::printaddr() {
    printf ("%p", this);
}

void C::printaddr () { // overrides B::printaddr()
    printf ("%p", this);
}

peut être compilé à

void B__printaddr (B *__this) {
    printf ("%p", __this);
}

// proper C::printaddr taking a this of type C* (new vtable entry in C)
void C__printaddr (C *__this) {
    printf ("%p", __this);
}

// C::printaddr overrider for B::printaddr
// needed for compatibility in vtable
void C__B__printaddr (B *__this) {
    C__printaddr (reinterpret_cast<C*>(reinterpret_cast<char*> (__this) - offset__C__B));
}

Nous voyons le type déclaré de C__B__printaddr et la sémantique sont compatibles avec B__printaddr, afin que nous puissions utiliser &C__B__printaddr dans le vtable de B; C__printaddr n'est pas compatible, mais peut être utilisé pour les appels impliquant un objets C ou classes dérivées de C.

Une fonction membre non virtuel est une fonction libre qui a accès à des choses interne. Une fonction de membre virtuel est « point de flexibilité » qui peut être personnalisé en remplaçant. déclaration de fonction membre virtuel joue un rôle particulier dans la définition d'une classe:. comme les autres membres, ils font partie du contrat avec le monde extérieur, mais en même temps, ils font partie d'un contrat avec classe dérivée

Une classe de base non virtuelle est comme un objet membre où l'on peut affiner le comportement par impérieuses (nous pouvons également accéder aux membres protégés). Pour le monde extérieur, lahéritage pour A dans Der implique que implicites conversions à base dérivées existeront pour les pointeurs, qu'un A& peut être lié à un lvalue de Der, etc. Pour les classes plus dérivées (dérivées de Der), cela signifie aussi que les fonctions virtuelles de A sont hérités dans la Der:. fonctions virtuelles dans A peuvent être remplacées dans des classes plus dérivées

Lorsqu'une classe est en outre dérivée, disons Der2 est dérivé d'Der, les conversions implicites un des pointeurs de type Der2* à A* est sémantiquement effectuée à l'étape: d'abord, une conversion à Der* est validée (le contrôle d'accès à la relation d'héritage de Der2 de Der est vérifié avec le public / protégé / règles privées / ami d'habitude), le contrôle d'accès des Der à A. Une relation d'héritage non virtuel ne peut pas être raffiné ou redéfinie dans les classes dérivées.

Les membres non membres virtuels fonctions peuvent être appelées directement et virtuels doivent être appelés indirectement via le vtable (sauf si le type réel de l'objet se trouve être connu par le compilateur), de sorte que le mot-clé virtual ajoute une indirection à accéder aux membres des fonctions. Tout comme pour les membres de la fonction, le mot-clé virtual ajoute une indirection à l'accès aux objets de base; comme pour les fonctions, les classes de base virtuelles ajoutent un point de flexibilité dans l'héritage.

En faisant non virtuelle, répétée, héritage multiple:

struct Top { int i; };
struct Left : Top { };
struct Right : Top { };
struct Bottom : Left, Right { };

Il y a seulement deux sous-objets Top::i dans Bottom (et de Left::i Right::i), comme des objets membres:

struct Top { int i; };
struct mLeft { Top t; };
struct mRight { mTop t; };
struct mBottom { mLeft l; mRight r; }

Personne est surprise qu'il y a deux sous-éléments de int (l.t.i et de r.t.i).

Avec des fonctions virtuelles:

struct Top { virtual void foo(); };
struct Left : Top { }; // could override foo
struct Right : Top { }; // could override foo
struct Bottom : Left, Right { }; // could override foo (both)

cela signifie qu'il ya deux différents (non reliés) fonctions virtuelles appelées foo, avec des entrées distinctes vtable (tant qu'ils ont la même signature, ils peuvent avoir un overrider commun).

La sémantique des classes de base non virtuelles suit du fait que l'héritage de base, non virtuelle, est une relation exclusive: la relation d'héritage établi entre gauche et supérieur ne peut être modifié par une nouvelle dérivation, donc le fait qu'une relation similaire existe entre Right et Top ne peut pas affecter cette relation. En particulier, cela signifie que Left::Top::foo() peut être redéfinie dans Left et Bottom, mais Right, qui n'a aucune relation d'héritage avec Left::Top, ne peut pas définir ce point de personnalisation.

classes de base virtuel sont différentes: un héritage virtuel est une relation partagée qui peut être personnalisé dans les classes dérivées:

struct Top { int i; virtual void foo(); };
struct vLeft : virtual Top { }; 
struct vRight : virtual Top { };
struct vBottom : vLeft, vRight { }; 

Ici, ce n'est qu'une classe de base sous-objet Top, un seul membre de int.

Mise en œuvre:

Salle pour les classes de base non virtuelles sont attribuées en fonction d'un modèle statique avec des décalages fixes dans la classe dérivée. Notez que la mise en page d'une classe dérivée est le inclus dans la présentation de la classe plus dérivée, de sorte que la position exacte de sous-objets ne dépend pas du type réel de l'objet (dynamique) (comme l'adresse d'une fonction non virtuelle est une constante ). OTOH, la position de sous-objets dans une classe avec l'héritage virtuel est déterminé par le type dynamique (comme l'adresse de la mise en œuvre d'une fonction virtuelle est connu que lorsque le est connu type dynamique).

L'emplacement de sous-objet sera déterminé lors de l'exécution avec le VPTR et la vtable (réutilisation du VPTR existant implique moins de frais généraux de l'espace), ou un pointeur interne direct vers le sous-objet (plus les frais généraux, moins indirections nécessaire).

Parce que l'offset d'une classe de base virtuelle est déterminée uniquement pour un objet complet, et ne peut pas être connu pour un type déclaré donné, une base virtuelle ne peut être attribuée à l'offset zéro et n'est jamais une base primaire . Une classe dérivée ne sera jamais réutiliser le VPTR d'une base virtuelle comme son propre VPTR.

En terme de traduction possible:

struct vLeft__vtable { 
    int Top__offset; // relative vLeft-Top offset
    void (*foo__ptr) (vLeft *__this); 
    // additional virtual member function go here
};

// this is what a subobject of type vLeft looks like
struct vLeft__subobject { 
    vLeft__vtable const *__vptr;
    // data members go here
};

void vLeft__subobject__ctor (vLeft__subobject *__this) { 
    // initialise data members
}

// this is a complete object of type vLeft 
struct vLeft__complete {
    vLeft__subobject __sub;
    Top Top__base;
}; 

// non virtual calls to vLeft::foo
void vLeft__real__foo (vLeft__complete *__this);

// virtual function implementation: call via base class
// layout is vLeft__complete 
void Top__in__vLeft__foo (Top *__this) {
    // inverse .Top__base member access 
    char *cp = reinterpret_cast<char*> (__this);
    cp -= offsetof (vLeft__complete,Top__base);
    vLeft__complete *__real = reinterpret_cast<vLeft__complete*> (cp);
    vLeft__real__foo (__real);
}

void vLeft__foo (vLeft *__this) {
    vLeft__real__foo (reinterpret_cast<vLeft__complete*> (__this));
}

// Top vtable for objects of real type vLeft
const Top__vtable Top__in__vLeft__real = { 
    /*foo__ptr =*/ Top__in__vLeft__foo 
};

// vLeft vtable for objects of real type vLeft
const vLeft__vtable vLeft__real = { 
    /*Top__offset=*/ offsetof(vLeft__complete, Top__base),
    /*foo__ptr =*/ vLeft__foo 
};

void vLeft__complete__ctor (vLeft__complete *__this) { 
    // construct virtual bases first
    Top__ctor (&__this->Top__base); 

    // construct non virtual bases: 
    // change dynamic type to vLeft
    // adjust both virtual base class vptr and current vptr
    __this->Top__base.__vptr = &Top__in__vLeft__real;
    __this->__vptr = &vLeft__real;

    vLeft__subobject__ctor (&__this->__sub);
}

Pour un objet de type connu, l'accès à la classe de base est par vLeft__complete:

struct a_vLeft {
    vLeft m;
};

void f(a_vLeft &r) {
    Top &t = r.m; // upcast
    printf ("%p", &t);
}

est traduit:

struct a_vLeft {
    vLeft__complete m;
};

void f(a_vLeft &r) {
    Top &t = r.m.Top__base;
    printf ("%p", &t);
}

Ici, le réel (dynamique) type de r.m est connu et est donc la position relative du sous-objet est connu au moment de la compilation. Mais ici:

void f(vLeft &r) {
    Top &t = r; // upcast
    printf ("%p", &t);
}

le type de r ne sait pas réel (dynamique), de sorte que l'accès se fait par l'VPTR:

void f(vLeft &r) {
    int off = r.__vptr->Top__offset;
    char *p = reinterpret_cast<char*> (&r) + off;
    printf ("%p", p);
}

Cette fonction peut accepter toute classe dérivée avec une autre mise en page:

// this is what a subobject of type vBottom looks like
struct vBottom__subobject { 
    vLeft__subobject vLeft__base; // primary base
    vRight__subobject vRight__base; 
    // data members go here
};

// this is a complete object of type vBottom 
struct vBottom__complete {
    vBottom__subobject __sub; 
    // virtual base classes follow:
    Top Top__base;
}; 

Notez que la classe de base de vLeft est à un emplacement fixe dans un vBottom__subobject, de sorte vBottom__subobject.__ptr est utilisé comme VPTR pour l'ensemble vBottom.

Sémantique:

La relation d'héritage est partagé par toutes les classes dérivées; cela signifie que le droit de passer outre est partagée, de sorte que vRight peut remplacer vLeft::foo. Cela crée un partage des responsabilités: vLeft et vRight doivent se mettre d'accord sur la façon dont ils personnalisent Top:

struct Top { virtual void foo(); };
struct vLeft : virtual Top { 
    override void foo(); // I want to customise Top
}; 
struct vRight : virtual Top { 
    override void foo(); // I want to customise Top
}; 
struct vBottom : vLeft, vRight { };  // error

Ici, nous voyons un conflit: vLeft et vRight cherchent à définir le comportement de la seule fonction virtuelle foo, et la définition de vBottom est en erreur, faute d'un commun overrider

.
struct vBottom : vLeft, vRight  { 
    override void foo(); // reconcile vLeft and vRight 
                         // with a common overrider
};

Mise en œuvre:

La construction de la classe avec des classes de base non virtuelles avec des classes de base non virtuels consiste à appeler les constructeurs de classe de base dans le même ordre que celui fait pour les variables membres, la modification du type dynamique à chaque fois que nous entrons dans une cteur. Pendant la construction, la classe de base agissent vraiment comme des sous-objets si elles étaient des objets complets (ce qui est vrai même avec des sous-objets de classe de base abstraite complets impossible: ce sont des objets avec des fonctions virtuelles non définies (pures)). Les fonctions virtuelles et RTTI peuvent être appelés pendant la construction (à l'exception bien sûr des pures fonctions virtuelles).

La construction d'une classe avec des classes de base non virtuelles avec des bases virtuelles est plus complexe : lors de la construction, le type dynamique est le type de classe de base, mais la mise en page de base virtuelle est toujours la mise en page le type le plus dérivé qui n'a pas encore construit, nous avons donc besoin de plus vtables pour décrire cet état:

// vtable for construction of vLeft subobject of future type vBottom
const vLeft__vtable vLeft__ctor__vBottom = { 
    /*Top__offset=*/ offsetof(vBottom__complete, Top__base),
    /*foo__ptr =*/ vLeft__foo 
};

Les fonctions virtuelles sont celles de vLeft (pendant la construction, la durée de vie de l'objet vBottom n'a pas encore commencé), tandis que les emplacements de base virtuels sont celles d'un vBottom (tel que défini dans le vBottom__complete objecté convertis aux).

Sémantique:

Lors de l'initialisation, il est évident que nous devons faire attention de ne pas utiliser un objet avant qu'il ne soit initialisés. Parce que C ++ nous donne un nom avant qu'un objet soit entièrement initialisé, il est facile de le faire:

int foo (int *p) { return *pi; }
int i = foo(&i); 

ou avec le ce pointeur dans le constructeur:

struct silly { 
    int i;
    std::string s;
    static int foo (bad *p) { 
        p->s.empty(); // s is not even constructed!
        return p->i; // i is not set!
    }
    silly () : i(foo(this)) { }
};

Il est assez évident que toute utilisation de this dans le cteur-init liste doit être soigneusement vérifié. Après l'initialisation de tous les membres, this peut être transmis à d'autres fonctions et enregistrées dans un certain ensemble (jusqu'à la destruction commence).

Ce qui est moins évident est que lorsque la construction d'une classe virtuelle partagée bases impliquant, en cours de construction sous-objets arrêtent: lors de la construction d'un vBottom:

  • d'abord les bases virtuelles sont construits: lorsque Top est construit, il est construit comme un sujet normal (Top ne sait même pas que ce soit la base virtuelle)

  • puis les classes de base sont construites de gauche à droite oRDONNANCE: le sous-objet de vLeft est construit et devient fonctionnel en tant que vLeft normal (mais avec une disposition de vBottom), de sorte que le sous-objet de classe de base de Top dispose désormais d'un type dynamique de vLeft;

  • la construction de sous-objet vRight commence, et le type dynamique des changements de classe de base à vRight; mais vRight ne provient pas de vLeft, ne sait rien de vLeft, de sorte que la base de vLeft est maintenant brisée;

  • lorsque le corps du constructeur de Bottom commence, les types de tous les sous-objets se sont stabilisés et vLeft est à nouveau fonctionnel.

Je ne sais pas comment cette réponse peut être considérée comme une réponse complète sans la mention de bits d'alignement ou de bourrage.

Permettez-moi de donner un fond de bits d'alignement:

"Une adresse mémoire a, est dit être aligné n-octet lorsque a est un multiple de n octets (où n est une puissance de 2). Dans ce contexte, un octet est la plus petite unité d'accès à la mémoire, à savoir chaque adresse de mémoire spécifie un autre octet. une adresse alignée n octets aurait log2 (n) des zéros les moins significatifs lorsqu'il est exprimé en binaire.

La formulation alternative b bits alignés désigne une adresse alignée b / 8 octets (ex. Est de 8 octets alignés 64 bits alignés).

Un accès à la mémoire est dit être alignés lorsque la donnée en cours d'accès est n octets de long et l'adresse de référence est aligné sur n octets. Quand un accès à la mémoire n'a pas été aligné, il est dit être mal aligné. Notez que, par accès mémoire octet de définition sont toujours alignés.

Un pointeur de mémoire qui se réfère à des données primitive qui est n octets est dit être alignés si elle est autorisée seulement pour contenir des adresses qui sont alignés n-octet, sinon il est considéré comme non aligné. Un pointeur de mémoire qui se rapporte à aligner un agrégat de données (une structure ou matrice de données) si (et seulement si) chaque donnée primitive dans l'agrégat est aligné.

A noter que les définitions ci-dessus supposent que chaque donnée primitive est une puissance de deux octets. Lorsque cela est pas le cas (comme avec 80 bits à virgule flottante sur 86) le contexte influe sur les conditions dans lesquelles la donnée est considérée comme aligné ou non.

Les structures de données peuvent être stockées dans la mémoire sur la pile avec une taille statique connu sous le nom borné ou sur le tas d'une taille dynamique connue sous le nom bornes « -. De Wiki ...

Pour maintenir l'alignement, compilateur insère des bits de remplissage dans le code compilé d'une structure / objet de classe. " Bien que le compilateur (ou interprète) alloue normalement des éléments de données individuelles sur les limites alignées, des structures de données ont souvent des membres avec différentes exigences d'alignement. Pour maintenir un alignement correct où le traducteur insère normalement des membres de données anonymes supplémentaires de sorte que chaque élément est correctement aligné. En outre, la structure de données dans son ensemble peut être rembourré avec un dernier membre sans nom. Ceci permet à chaque membre d'un réseau de structures à être correctement aligné. .... ....

Padding est insérée seulement quand un élément de structure est suivie par un élément à une exigence d'alignement plus grand ou à la fin de la structure » - Wiki

Pour plus d'informations sur la façon dont il le fait GCC, s'il vous plaît regarder

http://www.delorie.com/gnu/docs/gcc/ gccint_111.html

et rechercher le texte "-align de base"

Maintenant passons venu à ce problème:

Utilisation de la classe par exemple, j'ai créé ce programme pour un compilateur GCC en cours d'exécution sur un Ubuntu 64 bits.

int main() {
    cout << "!!!Hello World!!!" << endl; // prints !!!Hello World!!!
    A objA;
    C objC;
    cout<<__alignof__(objA.a)<<endl;
    cout<<sizeof(void*)<<endl;
    cout<<sizeof(int)<<endl;
    cout<<sizeof(A)<<endl;
    cout<<sizeof(B)<<endl;
    cout<<sizeof(C)<<endl;
    cout<<__alignof__(objC.a)<<endl;
    cout<<__alignof__(A)<<endl;
    cout<<__alignof__(C)<<endl;
    return 0;
}

Et le résultat de ce programme est comme suit:

4
8
4
16
16
32
4
8
8

Maintenant, permettez-moi de l'expliquer. Comme les deux A & B ont des fonctions virtuelles, ils vont créer VTables séparés et VPTR seront ajoutés au début de leurs objets, respectivement.

Par conséquent l'objet de la classe A aura une VPTR (pointant vers le VTABLE de A) et un int. Le pointeur sera de 8 octets de long et l'int sera de 4 octets de long. Par conséquent, avant de compiler la taille est de 12 octets. Mais le compilateur ajoutera 4 octets supplémentaires à la fin de int un sous forme de bits de remplissage. Par conséquent, après compilation, la taille des objets de A sera de 12 + 4 = 16.

De même pour les objets de la classe B.

Maintenant objet de C aura deux VPTRs (un pour chaque classe A et de classe B) et 3 ints (a, b, c). Ainsi, la taille aurait dû être 8 (VPTR A) + 4 (int a) + 4 (octets de remplissage) + 8 (VPTR B) + 4 (int b) + 4 (int c) = 32 octets. Ainsi, la taille totale de C sera de 32 octets.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top