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)

  1. Qual è l'esatto layout di memoria dell'oggetto di classe C.
  2. Voci delle tabelle virtuali per la classe C.
  3. Dimensioni (come restituite da sizeof) dell'oggetto delle classi A, B e C.(8, 8, 16 ??)
  4. 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!

È stato utile?

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 normale vLeft (ma con a vBottom layout), quindi il Top Il sottooggetto della classe base ora ha un file vLeft tipo dinamico;

  • IL vRight inizia la costruzione del suboggetto e il tipo dinamico della classe base cambia in vRight;Ma vRight non è derivato da vLeft, non ne sa nulla vLeft, così il vLeft la base ora è rotta;

  • quando il corpo del Bottom inizia il costruttore, i tipi di tutti i suboggetti si sono stabilizzati e vLeft è 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.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top