Layout degli oggetti in caso di funzioni virtuali ed ereditarietà multipla
-
19-09-2019 - |
Domanda
Recentemente in un'intervista mi è stato chiesto del layout degli oggetti con funzioni virtuali ed ereditarietà multipla coinvolte.
L'ho spiegato nel contesto di come viene implementato senza che sia coinvolta l'ereditarietà multipla (ad es.come il compilatore ha generato la tabella virtuale, inserire un puntatore segreto alla tabella virtuale in ogni oggetto e così via).
Mi sembrava che mancasse qualcosa nella mia spiegazione.
Quindi ecco le domande (vedi esempio sotto)
- Qual è l'esatto layout di memoria dell'oggetto di classe C.
- Voci delle tabelle virtuali per la classe C.
- Dimensioni (come restituite da sizeof) dell'oggetto delle classi A, B e C.(8, 8, 16 ??)
- Cosa succede se viene utilizzata l'ereditarietà virtuale.Sicuramente le dimensioni e le voci della tabella virtuale dovrebbero essere influenzate?
Codice di esempio:
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;
};
Grazie!
Soluzione
Il layout della memoria e la disposizione vtable dipendono dal compilatore. Usando il mio gcc per esempio, sembrano in questo modo:
sizeof(int) == 4
sizeof(A) == 8
sizeof(B) == 8
sizeof(C) == 20
Si noti che sizeof (int) e lo spazio necessario per il puntatore vtable può anche variare da compilatore a compilatore e una piattaforma all'altra. Il motivo per cui sizeof (C) == 20 e non 16 è che gcc conferisce 8 byte per l'A subobject, 8 byte per subobject B e 4 byte per la sua int c
utente.
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)
Utilizzando l'ereditarietà virtuale
class C : public virtual A, public virtual B
il layout modifiche
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)
Utilizzando gcc, è possibile aggiungere -fdump-class-hierarchy
per ottenere queste informazioni.
Altri suggerimenti
1 cosa aspettarsi con l'ereditarietà multipla è che il puntatore può cambiare in fase di lancio a una (in genere non prima) sottoclasse. Qualcosa che dovrebbe essere a conoscenza durante il debug e rispondendo alle domande di intervista.
Innanzitutto, una classe polimorfica ha almeno una funzione virtuale, quindi ha un vptr:
struct A {
virtual void foo();
};
è compilato in:
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;
}
Nota:Il C++ può essere compilato in un altro linguaggio di alto livello come C (come ha fatto cfront) o anche in un sottoinsieme C++ (qui C++ senza virtual
).metto __
nei nomi generati dal compilatore.
Tieni presente che questo è un semplicistico modello in cui RTTI non è supportato;i veri compilatori aggiungeranno i dati nella vtable da supportare typeid
.
Ora, una semplice classe derivata:
struct Der : A {
override void foo();
virtual void bar();
};
I sottooggetti della classe base non virtuali (*) sono sottooggetti come i sottooggetti dei membri, ma mentre i sottooggetti dei membri sono oggetti completi, ad es.il loro tipo reale (dinamico) è il tipo dichiarato, i sottooggetti della classe base non sono completi e il loro tipo reale cambia durante la costruzione.
(*) le basi virtuali sono molto diverse, così come le funzioni dei membri virtuali sono diverse dai membri non virtuali
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
};
Qui "prima posizione" significa che il membro deve essere il primo (gli altri membri potrebbero essere riordinati):si trovano all'offset zero, quindi possiamo reinterpret_cast
puntatori, i tipi sono compatibili;con un offset diverso da zero, dovremmo apportare modifiche al puntatore con l'aritmetica attivata char*
.
La mancanza di aggiustamenti potrebbe non sembrare un grosso problema in termini di codice generato (solo alcuni aggiungono istruzioni asm immediate), ma significa molto di più, significa che tali puntatori possono essere visti come aventi tipi diversi:un oggetto di tipo A__vtable*
può contenere un puntatore a Der__vtable
ed essere trattato come a Der__vtable*
o a A__vtable*
.Lo stesso oggetto puntatore funge da puntatore ad a A__vtable
nelle funzioni che trattano oggetti di tipo A
e come puntatore ad a Der__vtable
nelle funzioni che trattano oggetti di tipo 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);
}
Vedi che il tipo dinamico, come definito da vptr, cambia durante la costruzione quando assegniamo un nuovo valore a vptr (in questo caso particolare la chiamata al costruttore della classe base non fa nulla di utile e può essere ottimizzata, ma non lo è t il caso di costruttori non banali).
Con eredità multipla:
struct C : A, B {};
UN C
l'istanza conterrà a A
e un B
, come quello:
struct C {
A base__A; // primary base
B base__B;
};
Si noti che solo uno di questi sottooggetti della classe base può avere il privilegio di trovarsi all'offset zero;questo è importante in molti modi:
La conversione di puntatori in altre classi di base (upcast) avrà bisogno di un aggiustamento;al contrario, i rialzati necessitano degli aggiustamenti opposti;
Ciò implica che quando si effettua una chiamata virtuale con un puntatore di classe base, il
this
ha il valore corretto per l'ingresso nel sovraccarico di classe derivata.
Quindi il seguente codice:
void B::printaddr() {
printf ("%p", this);
}
void C::printaddr () { // overrides B::printaddr()
printf ("%p", this);
}
può essere compilato
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));
}
Vediamo il C__B__printaddr
il tipo dichiarato e la semantica sono compatibili con B__printaddr
, quindi possiamo usare &C__B__printaddr
nella tabella v di B
; C__printaddr
non è compatibile ma può essere utilizzato per le chiamate che coinvolgono a C
oggetti o classi derivate da C
.
Una funzione membro non virtuale è come una funzione gratuita che ha accesso a elementi interni.Una funzione membro virtuale è un "punto di flessibilità" che può essere personalizzato mediante override.la dichiarazione della funzione membro virtuale gioca un ruolo speciale nella definizione di una classe:come gli altri membri fanno parte del contratto con il mondo esterno, ma allo stesso tempo fanno parte di un contratto con la classe derivata.
Una classe base non virtuale è come un oggetto membro in cui possiamo perfezionare il comportamento tramite l'override (possiamo anche accedere ai membri protetti).Per il mondo esterno, l'eredità per A
In Der
implica che esisteranno conversioni implicite da derivata a base per i puntatori, che a A&
può essere legato a a Der
valore, ecc.Per ulteriori classi derivate (derivate da Der
), significa anche che le funzioni virtuali di A
sono ereditati nel Der
:funzioni virtuali in A
può essere sovrascritto in ulteriori classi derivate.
Quando una classe viene ulteriormente derivata, ad esempio Der2
è derivato da Der
, conversioni implicite a puntatori di tipo Der2*
A A*
viene eseguito semanticamente nel passaggio:innanzitutto, una conversione in Der*
è convalidato (il controllo di accesso alla relazione di ereditarietà di Der2
da Der
viene controllato con le consuete regole pubblico/protetto/privato/amico), quindi il controllo degli accessi di Der
A A
.Una relazione di ereditarietà non virtuale non può essere perfezionata o sovrascritta nelle classi derivate.
Le funzioni dei membri non virtuali possono essere chiamate direttamente e i membri virtuali devono essere chiamati indirettamente tramite vtable (a meno che il tipo di oggetto reale non sia noto al compilatore), quindi il virtual
La parola chiave aggiunge un riferimento indiretto all'accesso alle funzioni dei membri.Proprio come per i membri della funzione, il virtual
la parola chiave aggiunge un riferimento indiretto all'accesso agli oggetti di base;proprio come per le funzioni, le classi base virtuali aggiungono un punto di flessibilità nell'ereditarietà.
Quando si esegue l'ereditarietà multipla non virtuale, ripetuta:
struct Top { int i; };
struct Left : Top { };
struct Right : Top { };
struct Bottom : Left, Right { };
Ce ne sono solo due Top::i
sottooggetti in Bottom
(Left::i
E Right::i
), come con gli oggetti membri:
struct Top { int i; };
struct mLeft { Top t; };
struct mRight { mTop t; };
struct mBottom { mLeft l; mRight r; }
Nessuno si sorprende che ce ne siano due int
sottomembri (l.t.i
E r.t.i
).
Con funzioni virtuali:
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)
significa che vengono chiamate due diverse funzioni virtuali (non correlate). foo
, con voci vtable distinte (entrambi poiché hanno la stessa firma, possono avere un overrider comune).
La semantica delle classi base non virtuali deriva dal fatto che l'ereditarietà di base, non virtuale, è una relazione esclusiva:la relazione di ereditarietà stabilita tra Left e Top non può essere modificata da un'ulteriore derivazione, da qui il fatto che esista una relazione simile tra Right
E Top
non può influenzare questa relazione.In particolare, significa questo Left::Top::foo()
può essere sovrascritto Left
e dentro Bottom
, Ma Right
, con cui non ha alcun rapporto di successione Left::Top
, non è possibile impostare questo punto di personalizzazione.
Le classi base virtuali sono diverse:un'eredità virtuale è una relazione condivisa che può essere personalizzata nelle classi derivate:
struct Top { int i; virtual void foo(); };
struct vLeft : virtual Top { };
struct vRight : virtual Top { };
struct vBottom : vLeft, vRight { };
Qui, questo è solo un sottooggetto della classe base Top
, solo uno int
membro.
Implementazione:
Lo spazio per le classi base non virtuali viene allocato in base a un layout statico con offset fissi nella classe derivata.Da notare che il layout di una classe derivata è incluso nel layout di altre classi derivate, quindi la posizione esatta dei sottooggetti non dipende dal tipo reale (dinamico) dell'oggetto (proprio come l'indirizzo di una funzione non virtuale è una costante ).OTOH, la posizione dei sottooggetti in una classe con ereditarietà virtuale è determinata dal tipo dinamico (così come l'indirizzo dell'implementazione di una funzione virtuale è noto solo quando è noto il tipo dinamico).
La posizione del suboggetto verrà determinata in fase di esecuzione con vptr e vtable (il riutilizzo del vptr esistente implica meno spazio in più) o un puntatore interno diretto al suboggetto (più sovraccarico, meno indirette necessarie).
Poiché l'offset di una classe base virtuale è determinato solo per un oggetto completo e non può essere conosciuto per un dato tipo dichiarato, una base virtuale non può essere allocata con offset zero e non è mai una base primaria.Una classe derivata non riutilizzerà mai il vptr di una base virtuale come proprio vptr.
In termini di possibile traduzione:
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);
}
Per un oggetto di tipo noto l'accesso alla classe base avviene tramite vLeft__complete
:
struct a_vLeft {
vLeft m;
};
void f(a_vLeft &r) {
Top &t = r.m; // upcast
printf ("%p", &t);
}
è tradotto in:
struct a_vLeft {
vLeft__complete m;
};
void f(a_vLeft &r) {
Top &t = r.m.Top__base;
printf ("%p", &t);
}
Ecco il vero tipo (dinamico) di r.m
è noto, così come la posizione relativa del suboggetto in fase di compilazione.Ma qui:
void f(vLeft &r) {
Top &t = r; // upcast
printf ("%p", &t);
}
il tipo reale (dinamico) di r
non è noto, quindi l'accesso avviene tramite vptr:
void f(vLeft &r) {
int off = r.__vptr->Top__offset;
char *p = reinterpret_cast<char*> (&r) + off;
printf ("%p", p);
}
Questa funzione può accettare qualsiasi classe derivata con un layout diverso:
// 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;
};
Si noti che il vLeft
la classe base si trova in una posizione fissa in a vBottom__subobject
, COSÌ vBottom__subobject.__ptr
viene utilizzato come vptr per l'intero vBottom
.
Semantica:
La relazione di ereditarietà è condivisa da tutte le classi derivate;ciò significa che il diritto di prelazione è condiviso, quindi vRight
può sovrascrivere vLeft::foo
.Ciò crea una condivisione delle responsabilità: vLeft
E vRight
devono essere d'accordo su come personalizzarli 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
Qui vediamo un conflitto: vLeft
E vRight
cercare di definire il comportamento dell'unica funzione virtuale foo, e vBottom
la definizione è errata per mancanza di un overrider comune.
struct vBottom : vLeft, vRight {
override void foo(); // reconcile vLeft and vRight
// with a common overrider
};
Implementazione:
La costruzione di classi con classi base non virtuali comporta la chiamata ai costruttori della classe base nello stesso ordine fatto per le variabili membro, cambiando il tipo dinamico ogni volta che si inserisce un ctor.Durante la costruzione, i sottooggetti della classe base si comportano davvero come se fossero oggetti completi (questo è vero anche con sottooggetti astratti completi della classe base impossibili:sono oggetti con funzioni virtuali (pure) indefinite).Le funzioni virtuali e RTTI possono essere richiamate durante la costruzione (tranne ovviamente le funzioni virtuali pure).
La costruzione di una classe con basi non virtuali è più complicata:durante la costruzione, il tipo dinamico è il tipo della classe base, ma il layout della base virtuale è ancora il layout del tipo più derivato che non è ancora stato costruito, quindi abbiamo bisogno di più vtables per descrivere questo stato:
// 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
};
Le funzioni virtuali sono quelle di vLeft
(durante la costruzione, la vita dell'oggetto vBottom non è iniziata), mentre le posizioni delle basi virtuali sono quelle di a vBottom
(come definito nel vBottom__complete
tradotto ha obiettato).
Semantica:
Durante l'inizializzazione è ovvio che bisogna fare attenzione a non utilizzare un oggetto prima che sia inizializzato.Poiché C++ ci fornisce un nome prima che un oggetto venga completamente inizializzato, è facile farlo:
int foo (int *p) { return *pi; }
int i = foo(&i);
o con il puntatore this nel costruttore:
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)) { }
};
È abbastanza ovvio che qualsiasi utilizzo di this
nella ctor-init-list deve essere attentamente controllato.Dopo l'inizializzazione di tutti i membri, this
può essere passato ad altre funzioni e registrato in qualche set (fino all'inizio della distruzione).
Ciò che è meno ovvio è che quando si costruisce una classe che coinvolge basi virtuali condivise, i sottooggetti smettono di essere costruiti:durante la costruzione di a vBottom
:
prima si costruiscono le basi virtuali:Quando
Top
è costruito, è costruito come un soggetto normale (Top
non sa nemmeno che è una base virtuale)quindi le classi base vengono costruite in ordine da sinistra a destra:IL
vLeft
il suboggetto viene costruito e diventa funzionale come un oggetto normalevLeft
(ma con avBottom
layout), quindi ilTop
Il sottooggetto della classe base ora ha un filevLeft
tipo dinamico;IL
vRight
inizia la costruzione del suboggetto e il tipo dinamico della classe base cambia in vRight;MavRight
non è derivato davLeft
, non ne sa nullavLeft
, così ilvLeft
la base ora è rotta;quando il corpo del
Bottom
inizia il costruttore, i tipi di tutti i suboggetti si sono stabilizzati evLeft
è di nuovo funzionale.
Non sono sicuro di come questa risposta possa essere considerata come una risposta completa senza menzionare l'allineamento o i bit di riempimento.
Vorrei fornire un po' di informazioni sull'allineamento:
"Un indirizzo di memoria a si dice allineato a n byte quando a è un multiplo di n byte (dove n è una potenza di 2).In questo contesto un byte è la più piccola unità di accesso alla memoria, cioèogni indirizzo di memoria specifica un byte diverso.Un indirizzo allineato a n byte avrebbe log2(n) zeri meno significativi se espresso in binario.
La dicitura alternativa b-bit allineato designa un indirizzo allineato a b/8 byte (es.64 bit allineati corrispondono a 8 byte allineati).
Un accesso alla memoria si dice allineato quando il dato a cui si accede è lungo n byte e l'indirizzo del dato è allineato di n byte.Quando un accesso alla memoria non è allineato, si dice che sia disallineato.Si noti che per definizione gli accessi alla memoria dei byte sono sempre allineati.
Un puntatore di memoria che fa riferimento a dati primitivi lunghi n byte si dice allineato se può contenere solo indirizzi allineati a n byte, altrimenti si dice non allineato.Un puntatore di memoria che fa riferimento a un aggregato di dati (una struttura di dati o un array) è allineato se (e solo se) ogni dato primitivo nell'aggregato è allineato.
Si noti che le definizioni sopra presuppongono che ciascun dato primitivo sia lungo una potenza di due byte.Quando questo non è il caso (come nel caso della virgola mobile a 80 bit su x86) il contesto influenza le condizioni in cui il dato è considerato allineato o meno.
Le strutture dati possono essere archiviate in memoria nello stack con una dimensione statica nota come limitata o nell'heap con una dimensione dinamica nota come illimitata." - da Wiki...
Per mantenere l'allineamento, il compilatore inserisce bit di riempimento nel codice compilato di un oggetto struttura/classe."Sebbene il compilatore (o l'interprete) alloca normalmente singoli elementi di dati sui confini allineati, le strutture di dati hanno spesso membri con requisiti di allineamento diversi.Per mantenere il corretto allineamento, il traduttore normalmente inserisce ulteriori dati membri senza nome in modo che ciascun membro sia correttamente allineato.Inoltre la struttura dati nel suo insieme può essere riempita con un membro finale senza nome.Ciò consente a ciascun membro di una serie di strutture di essere correttamente allineato.........
Il riempimento viene inserito solo quando un membro della struttura è seguito da un membro con un requisito di allineamento maggiore o alla fine della struttura" - Wiki
Per avere maggiori informazioni su come lo fa GCC, guarda
http://www.delorie.com/gnu/docs/gcc/gccint_111.html
e cerca il testo "basic-align"
Veniamo ora a questo problema:
Usando la classe di esempio, ho creato questo programma per un compilatore GCC in esecuzione su Ubuntu a 64 bit.
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;
}
E il risultato per questo programma è il seguente:
4
8
4
16
16
32
4
8
8
Ora lasciatemelo spiegare.Poiché sia A che B hanno funzioni virtuali, creeranno VTABLE separate e VPTR verrà aggiunto rispettivamente all'inizio dei loro oggetti.
Quindi l'oggetto della classe A avrà un VPTR (che punta alla VTABLE di A) e un int.Il puntatore sarà lungo 8 byte e l'int sarà lungo 4 byte.Quindi prima della compilazione la dimensione è di 12 byte.Ma il compilatore aggiungerà 4 byte extra alla fine di int a come bit di riempimento.Quindi dopo la compilazione, la dimensione degli oggetti di A sarà 12+4 = 16.
Allo stesso modo per gli oggetti di classe B.
Ora l'oggetto di C avrà due VPTR (uno per ogni classe A e classe B) e 3 int (a,b,c).Quindi la dimensione avrebbe dovuto essere 8 (VPTR A) + 4 (int a) + 4 (padding bytes) + 8 (VPTR B) + 4 (int b) + 4 (int c) = 32 byte.Quindi la dimensione totale di C sarà di 32 byte.