Frage

Ich wurde kürzlich in einem Interview nach Objektlayout mit virtuellen Funktionen und mehreren Vererbung gefragt.
Ich habe es im Zusammenhang mit der Implementierung ohne multiple Vererbung erklärt (dh wie der Compiler die virtuelle Tabelle generierte, einen geheimen Zeiger in die virtuelle Tabelle in jedem Objekt usw. einfügen).
Es schien mir, dass meine Erklärung etwas fehlte.
Hier sind also Fragen (siehe Beispiel unten)

  1. Was ist das genaue Speicherlayout des Objekts der Klasse C C.
  2. Virtuelle Tabelleneinträge für Klasse C C.
  3. Größen (wie nach Größe von Größe des Objekts der Klassen A, B und C. (8, 8, 16 ??)
  4. Was ist, wenn eine virtuelle Vererbung verwendet wird? Sicherlich sollten die Größen und virtuellen Tabelleneinträge betroffen sein?

Beispielcode:

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;  
};   

Vielen Dank!

War es hilfreich?

Lösung

Das Speicherlayout und das vtable -Layout hängen von Ihrem Compiler ab. Mit meinem GCC zum Beispiel sehen sie so aus:

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

Beachten Sie, dass die Größe von (int) und der für den vtable Zeiger benötigte Raum auch von Compiler zu Compiler und Plattform zu Plattform variieren können. Der Grund, warum sizeof (c) == 20 und nicht 16 ist, dass GCC ihm 8 Bytes für das A -SubObject, 8 Bytes für das B -SubObject und 4 Bytes für sein Mitglied gibt int c.

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)

Verwenden der virtuellen Vererbung

class C : public virtual A, public virtual B

Das Layout ändert sich zu

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)

Mit GCC können Sie hinzufügen -fdump-class-hierarchy um diese Informationen zu erhalten.

Andere Tipps

1 Sache, die Sie mit mehreren Vererbung erwarten können, ist, dass sich Ihr Zeiger beim Gießen in eine (normalerweise nicht erste) Unterklasse ändern kann. Etwas, das Sie beim Debuggen und Beantworten von Interviewfragen bewusst sein sollten.

Erstens hat eine polymorphe Klasse mindestens eine virtuelle Funktion, so dass sie ein VPTR hat:

struct A {
    virtual void foo();
};

wird zusammengestellt zu:

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;
}

Bemerkung: C ++ kann mit C (wie CFRONT) oder sogar mit einer C ++ - Subset (hier C ++ ohne CFRONT) zusammengestellt werden virtual). ich lege __ In Compiler generierte Namen.

Beachten Sie, dass dies a ist simpel Modell, bei dem RTTI nicht unterstützt wird; Reale Compiler werden Daten in die VTABLE zur Unterstützung hinzufügen typeid.

Jetzt eine einfache abgeleitete Klasse:

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

Nicht virtuelle (*) Unterobjekte der Basisklasse sind Unterobjekte wie die SuboBoDjects von Mitgliedern, während die Mitglieder-Unterobjekte jedoch vollständige Objekte sind, dh. Ihr realer (dynamischer) Typ ist ihr deklarierter Typ, die Unterobjekte der Basisklasse sind nicht vollständig und ihre reale Artänderung während der Konstruktion.

(*) Virtuelle Basen sind sehr unterschiedlich, wie virtuelle Mitgliederfunktionen unterscheiden sich von nicht virtuellen Mitgliedern

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
};

Hier "Erste Position" bedeutet, dass das Mitglied der erste sein muss (andere Mitglieder könnten neu bestellt werden): Sie befinden sich bei Offset Null, damit wir können reinterpret_cast Zeiger, die Typen sind Verträge; Bei Offset von ungleich Null müssten wir Zeigeranpassungen mit Arithmetik vornehmen char*.

Die mangelnde Anpassung scheint keine große Sache im Begriff des generierten Codes zu sein (nur einige fügen unmittelbare ASM -Anweisungen hinzu), aber es bedeutet viel mehr als das, dies bedeutet, dass solche Zeiger als unterschiedliche Typen angesehen werden können: ein Objekt vom Typ Art vom Typ A__vtable* kann einen Zeiger auf enthalten Der__vtable und als entweder a behandelt werden Der__vtable* oder ein A__vtable*. Das gleiche Zeigerobjekt dient als Zeiger auf a A__vtable in Funktionen, die sich mit Objekten vom Typ befassen A und als Zeiger auf a Der__vtable in Funktionen, die sich mit Objekten vom Typ befassen 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);
}

Sie sehen, dass sich der dynamische Typ, wie vom VPTR definiert, während der Konstruktion ändert, wenn wir dem VPTR einen neuen Wert zuweisen (in diesem speziellen Fall tut der Aufruf an den Basisklassenkonstruktor nichts Nützliches und kann verschenkt werden, aber es ist nicht ' t der Fall mit nicht trivialen Konstruktoren).

Mit mehreren Vererbung:

struct C : A, B {};

EIN C Die Instanz enthält a A und ein B, so wie das:

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

Beachten Sie, dass nur einer dieser Unterobjekte der Grundklasse das Privileg haben kann, bei Offset Null zu sitzen. Dies ist in vielerlei Hinsicht wichtig:

  • Die Umwandlung von Zeigern auf andere Basisklassen (Upcasts) erfordert eine Anpassung. Umgekehrt benötigen Upcasts die entgegengesetzten Anpassungen.

  • Dies impliziert, dass bei einem virtuellen Anruf mit einem Basisklassenzeiger die this hat den richtigen Wert für die Eingabe in der abgeleiteten Klasse Overrider.

Also der folgende Code:

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

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

kann zusammengestellt werden

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));
}

Wir sehen die C__B__printaddr Deklarierte Art und Semantik sind Verträge mit B__printaddr, so können wir verwenden &C__B__printaddr im vtable von B; C__printaddr ist nicht kompatibel, kann aber für Anrufe mit a verwendet werden C Objekte oder Klassen abgeleitet von C.

Eine nicht virtuelle Mitgliedsfunktion ist wie eine kostenlose Funktion, die Zugriff auf interne Dinge hat. Eine virtuelle Mitgliederfunktion ist "Flexibilitätspunkt", die durch Überschreiben angepasst werden kann. Die Erklärung der virtuellen Mitgliederfunktion spielt eine besondere Rolle in der Definition einer Klasse: Wie andere Mitglieder sind sie Teil des Vertrags mit der externen Welt, aber gleichzeitig sind sie Teil eines Vertrags mit abgeleiteter Klasse.

Eine nicht virtuelle Basisklasse ist wie ein Mitgliedsobjekt, bei dem wir das Verhalten durch Überschreibung verfeinern können (wir können auch auf geschützte Mitglieder zugreifen). Für die Außenwelt die Erbe für A in Der impliziert, dass implizite Umbauten für Zeiger existieren werden, dass a A& kann an a gebunden werden Der lvalue usw. für weitere abgeleitete Klassen (abgeleitet von Der) bedeutet es auch, dass virtuelle Funktionen von A werden in der vererbt Der: virtuelle Funktionen in A kann in weiteren abgeleiteten Klassen überschrieben werden.

Wenn eine Klasse weiter abgeleitet ist, sagen wir Der2 ist abgeleitet von Der, implizite Konvertierungen und Zeiger des Typs Der2* zu A* wird semantisch im Schritt durchgeführt: Erstens eine Konvertierung zu Der* wird validiert (die Zugriffskontrolle für die Vererbungsbeziehung von Der2 aus Der wird mit den üblichen öffentlichen/geschützten/privaten/Freunden überprüft) und dann die Zugangskontrolle von Der zu A. Eine nicht virtuelle Vererbungsbeziehung kann in abgeleiteten Klassen nicht verfeinert oder außer Kraft gesetzt werden.

Nicht virtuelle Mitgliederfunktionen können direkt aufgerufen werden, und virtuelle Mitglieder müssen indirekt über das VTable bezeichnet werden (es sei denn, der reale Objekttyp wird vom Compiler bekannt), also die virtual Das Schlüsselwort fügt den Mitgliedern Funktionen zu Zugang zu. Genau wie für Funktionsmitglieder die virtual Schlüsselwort fügt eine Indirektion zum Basisobjektzugriff hinzu. Genau wie bei Funktionen fügen virtuelle Basisklassen einen Flexibilitätspunkt in der Vererbung hinzu.

Bei nicht virtuellen, wiederholten, multiplen Vererbung:

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

Es gibt nur zwei Top::i Unterobjekte in Bottom (Left::i und Right::i), wie bei Mitgliedern Objekten:

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

Niemand ist überrascht, dass es zwei gibt int Untermitglieder (l.t.i und r.t.i).

Mit virtuellen Funktionen:

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)

Es bedeutet, dass es zwei verschiedene (nicht verwandte) virtuelle Funktionen gibt foo, Mit unterschiedlichen VTABLE -Einträgen (beide, da sie die gleiche Signatur haben, können sie eine gemeinsame Überschreibung haben).

Die Semantik der nicht virtuellen Basisklassen folgt der Tatsache, dass grundlegende, nicht virtuelle Vererbung eine ausschließliche Beziehung ist: Die zwischen linke und oben und oben festgelegte Vererbungsbeziehung kann nicht durch eine weitere Ableitung geändert werden Right und Top kann diese Beziehung nicht beeinflussen. Insbesondere bedeutet es das Left::Top::foo() kann in überschrieben werden in Left und in Bottom, aber Right, was keine Vererbungsbeziehung mit Left::Top, kann diesen Anpassungspunkt nicht festlegen.

Virtuelle Basisklassen sind unterschiedlich: Eine virtuelle Vererbung ist eine gemeinsame Beziehung, die in abgeleiteten Klassen angepasst werden kann:

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

Hier ist dies nur ein Unterobjekt der Basisklasse Top, einziger int Mitglied.

Implementierung:

Raum für nicht virtuelle Basisklassen werden auf der Grundlage eines statischen Layouts mit festen Offsets in der abgeleiteten Klasse zugewiesen. Beachten Sie, dass das Layout einer abgeleiteten Klasse das im Layout der abgeleiteten Klasse enthalten ist ). OTOH, die Position von SubObjects in einer Klasse mit virtueller Vererbung wird durch den dynamischen Typ bestimmt (genau wie die Adresse der Implementierung einer virtuellen Funktion ist nur dann bekannt, wenn der dynamische Typ bekannt ist).

Der Standort des SubObject wird zur Laufzeit mit dem VPTR und dem VTABLE festgelegt (Wiederverwendung des vorhandenen VPTR impliziert weniger Raumaufwand) oder einen direkten internen Zeiger auf das SubObject (mehr Overhead, weniger erforderliche Indirektionen).

Da der Versatz einer virtuellen Basisklasse nur für ein vollständiges Objekt bestimmt wird und nicht für einen bestimmten deklarierten Typ bekannt sein kann, kann Eine virtuelle Basis kann nicht bei Offset Null zugewiesen werden und ist niemals eine primäre Basis. Eine abgeleitete Klasse wird den VPTR einer virtuellen Basis niemals als eigenes VPTR wiederverwenden.

In Begriff möglicher Übersetzung:

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);
}

Für ein Objekt des bekannten Typs ist der Zugriff auf die Basisklasse durch vLeft__complete:

struct a_vLeft {
    vLeft m;
};

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

wird übersetzt in:

struct a_vLeft {
    vLeft__complete m;
};

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

Hier die reale (dynamische) Art von r.m ist bekannt, ebenso wie die relative Position des Unterobjekts ist zur Kompilierungszeit bekannt. Aber hier:

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

der reale (dynamische) Typ von r ist nicht bekannt, daher erfolgt der Zugang über das VPTR:

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

Diese Funktion kann jede abgeleitete Klasse mit einem anderen Layout akzeptieren:

// 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;
}; 

Notiere dass der vLeft Die Basisklasse befindet sich an einem festen Ort in a vBottom__subobject, Also vBottom__subobject.__ptr wird als VPTR für das Ganze verwendet vBottom.

Semantik:

Die Vererbungsbeziehung wird von allen abgeleiteten Klassen geteilt; Dies bedeutet, dass das Recht auf Überschreibung so geteilt wird, also vRight kann überschreiben vLeft::foo. Dies schafft eine Teile der Verantwortlichkeiten: vLeft und vRight muss sich darauf einigen, wie sie sich anpassen 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

Hier sehen wir einen Konflikt: vLeft und vRight versuchen, das Verhalten der einzigen virtuellen Foo -Funktion zu definieren, und vBottom Die Definition ist fehlerhaft, weil ein gemeinsamer Überschreibungsmangel.

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

Implementierung:

Bei der Konstruktion der Klasse mit nicht virtuellen Basisklassen mit nicht virtuellen Basisklassen werden die Basisklassenkonstruktoren in derselben Reihenfolge wie für Mitgliedsvariablen aufgerufen, wobei der dynamische Typ bei jedem Eingeben eines CTOR geändert wird. Während der Konstruktion handeln die Unterobjekte der Basisklasse wirklich so, als wären sie vollständige Objekte (dies gilt sogar für unmögliche vollständige abstrakte Basisklassen -Unterobjekte: Sie sind Objekte mit undefinierten (reinen) virtuellen Funktionen). Virtuelle Funktionen und RTTI können während der Konstruktion aufgerufen werden (außer natürlich reinen virtuellen Funktionen).

Die Konstruktion einer Klasse mit nicht virtuellen Basisklassen mit virtuellen Basen ist komplizierter: Während der Konstruktion ist der dynamische Typ der Basisklassentyp, aber das Layout der virtuellen Basis ist immer noch das Layout des am meisten abgeleiteten Typs, der noch nicht konstruiert ist. Daher benötigen wir mehr Vtables, um diesen Zustand zu beschreiben:

// 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 
};

Die virtuellen Funktionen sind die von vLeft (Während des Baus hat die Lebensdauer des VBOTTOM -Objekts nicht begonnen), während die virtuellen Basisstandorte die von a sind vBottom (wie in der definiert vBottom__complete Übersetzte Einwände).

Semantik:

Während der Initialisierung ist es offensichtlich, dass wir darauf achten müssen, dass wir ein Objekt nicht verwenden, bevor es initialisiert wird. Da C ++ uns einen Namen gibt, bevor ein Objekt vollständig initialisiert ist, ist dies einfach:

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

oder mit diesem Zeiger im Konstruktor:

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)) { }
};

Es ist ziemlich offensichtlich, dass jede Verwendung von this In der CTOR-Init-Liste muss sorgfältig überprüft werden. Nach der Initialisierung aller Mitglieder, this kann an andere Funktionen übergeben und in einigen Sätzen registriert werden (bis die Zerstörung beginnt).

Was weniger offensichtlich ist, ist, dass beim Bau einer Klasse, die gemeinsame virtuelle Basen beinhaltet vBottom:

  • Zunächst werden die virtuellen Basen konstruiert: Wann Top ist konstruiert, es ist wie ein normales Thema konstruiert (Top weiß nicht einmal, dass es virtuelle Basis ist)

  • Dann sind die Basisklassen in links nach rechts auf Reihenfolge konstruiert: die vLeft Das SubObject wird konstruiert und wird als normal funktionsfähig vLeft (aber mit einem vBottom Layout), also die Top Basisklassen -Unterobjekt hat jetzt a vLeft dynamischer Typ;

  • das vRight Die SuboDject -Konstruktion beginnt und der dynamische Typ der Basisklasse ändert sich zu vright. aber vRight ist nicht abgeleitet von vLeft, weiß nichts darüber vLeft, so die vLeft Basis ist jetzt gebrochen;

  • Wenn der Körper des Bottom Der Konstruktor beginnt, die Arten aller Unterobjekte haben sich stabilisiert und vLeft ist wieder funktional.

Ich bin mir nicht sicher, wie diese Antwort als vollständige Antwort ohne die Erwähnung von Ausrichtungs- oder Polsterbits angesehen werden kann.

Lassen Sie mich ein bisschen Hintergrund der Ausrichtung geben:

"Eine Speicheradresse A soll n-byte ausgerichtet sein, wenn a ein Vielfaches von n Bytes ist (wobei N eine Leistung von 2 ist). In diesem Zusammenhang ist ein Byte die kleinste Einheit des Speicherzugriffs, dh jede Speicheradresse gibt an Ein anderes Byte. Eine N-Byte-ausgerichtete Adresse hätte log2 (n) am wenigsten signifikante Nullen, wenn sie in binär ausgedrückt werden.

Der alternative Wortlaut B-Bit Ausgestattet bezeichnet AB/8-Byte-ausgerichtete Adresse (Ex. 64-Bit-Ausrichtung ist 8 Bytes ausgerichtet).

Ein Speicherzugriff soll ausgerichtet sein, wenn auf das auf das Datum zugegriffene Daten zu lang ist, und die Datum-Adresse ist N-Byte ausgerichtet. Wenn ein Speicherzugriff nicht ausgerichtet ist, soll er falsch ausgerichtet werden. Beachten Sie, dass per Definition Byte Speicherzugriffe immer ausgerichtet sind.

Ein Speicherzeiger, der sich auf primitive Daten bezieht, die lange Bytes sind, soll ausgerichtet sein, wenn er nur Adressen enthalten darf, die N-Byte ausgerichtet sind, sonst soll er nicht ausgerichtet sein. Ein Speicherzeiger, der sich auf ein Datenaggregat (eine Datenstruktur oder ein Array) bezieht, wird ausgerichtet, wenn (und wenn) jedes primitive Datum im Aggregat ausgerichtet ist.

Beachten Sie, dass die obigen Definitionen davon ausgehen, dass jedes primitive Datum eine Leistung von zwei Bytes lang ist. Wenn dies nicht der Fall ist (wie bei 80-Bit-Gleitpunkt auf x86), beeinflusst der Kontext die Bedingungen, unter denen das Datum als ausgerichtet angesehen wird oder nicht.

Datenstrukturen können im Speicher auf dem Stapel mit einer statischen Größe gespeichert werden, die als begrenzt oder auf dem Haufen mit einer dynamischen Größe, die als unbegrenzt bekannt ist, bekannt ist. " - vom Wiki ...

Um die Ausrichtung aufrechtzuerhalten, fügt Compiler Polsterbits in den kompilierten Code eines Struktur-/Klassenobjekts ein. "Obwohl der Compiler (oder Dolmetscher) normalerweise einzelne Datenelemente an ausgerichteten Grenzen zuteilt, haben Datenstrukturen häufig Mitglieder mit unterschiedlichen Ausrichtungsanforderungen. Um ordnungsgemäß ausgerichtet zu werden, fügt der Übersetzer normalerweise zusätzliche nicht genannte Datenmitglieder ein, damit jedes Mitglied ordnungsgemäß ausgerichtet ist. Zusätzlich die Die Datenstruktur als Ganzes kann mit einem endgültigen ungenannten Element gepolstert werden. Dadurch kann jedes Mitglied einer Reihe von Strukturen ordnungsgemäß ausgerichtet sein. .... .... .... .... .... .... .... ....

Die Polsterung wird nur eingefügt, wenn ein Mitglied ein Mitglied mit einer größeren Ausrichtungsanforderung oder am Ende der Struktur folgt " - Wiki

Um weitere Informationen darüber zu erhalten, wie GCC es macht, schauen Sie sich bitte an

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

und suchen Sie nach dem Text "Basic-Align"

Jetzt kommen wir zu diesem Problem:

Mit der Beispielklasse habe ich dieses Programm für einen GCC -Compiler erstellt, der auf einem 64 -Bit -Ubuntu ausgeführt wird.

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;
}

Und das Ergebnis für dieses Programm lautet wie folgt:

4
8
4
16
16
32
4
8
8

Lassen Sie es mich jetzt erklären. Da sowohl A & B über virtuelle Funktionen verfügen, erstellen sie separate Vtables und VPTR wird zu Beginn ihrer Objekte hinzugefügt.

Daher hat das Objekt der Klasse A ein VPTR (auf die VTable von a) und ein int. Der Zeiger ist 8 Byte lang und der int wird 4 Byte lang sein. Daher beträgt die Größe 12 Bytes. Aber der Compiler fügt zusätzliche 4 Bytes am Ende von int a als Polsterbits hinzu. Daher beträgt die Größe der Objekte des A nach der Zusammenstellung 12+4 = 16.

Ähnlich für Objekte der Klasse B.

Jetzt hat das Objekt von C zwei VPTRs (eine für jede Klasse A & Klasse B) und 3 INTs (A, B, C). Die Größe hätte also 8 (VPTR a) + 4 (int a) + 4 (Polsterbytes) + 8 (VPTR B) + 4 (int b) + 4 (int c) = 32 Bytes gewesen sein. Die Gesamtgröße von C beträgt also 32 Bytes.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top