Virtuelle Tabellen und Speicher-layout in mehrere virtuelle Vererbung
Betrachten folgende Hierarchie:
struct A {
int a;
A() { f(0); }
A(int i) { f(i); }
virtual void f(int i) { cout << i; }
};
struct B1 : virtual A {
int b1;
B1(int i) : A(i) { f(i); }
virtual void f(int i) { cout << i+10; }
};
struct B2 : virtual A {
int b2;
B2(int i) : A(i) { f(i); }
virtual void f(int i) { cout << i+20; }
};
struct C : B1, virtual B2 {
int c;
C() : B1(6),B2(3),A(1){}
virtual void f(int i) { cout << i+30; }
};
-
Was ist der genaue Speicher-layout von
C
Instanz? Wie viele vptrs es enthält, wo genau jeder von Ihnen platziert wird? Die virtuellen Tabellen werden gemeinsam mit der virtuellen Tabelle der C? Was genau die einzelnen virtuellen Tabelle enthält?Hier, wie ich verstehe, layout:
---------------------------------------------------------------- |vptr1 | AptrOfB1 | b1 | B2ptr | c | vptr2 | AptrOfB2 | b2 | a | ----------------------------------------------------------------
wo
AptrOfBx
ist der Zeiger aufA
Instanz, dieBx
enthält (da die Vererbung virtuell ist).
Ist das richtig? Welche Funktionenvptr1
Punkte zu? Welche Funktionenvptr2
Punkte zu? -
Angesichts des folgenden code
C* c = new C(); dynamic_cast<B1*>(c)->f(3); static_cast<B2*>(c)->f(3); reinterpret_cast<B2*>(c)->f(3);
Warum die alle Aufrufe
f
drucken33
?
- Ist diese Hausaufgaben, oder Neugier?
- Eigentlich ist das Examen. Aber ich bin mir sicher, wenn ich endlich verstehen, wie die Dinge funktioniert in diesem Beispiel, kann ich alles verstehen, die Bezug auf Mehrfachvererbung und virtuelle Vererbung.
- Sie können leicht herausfinden, die zu Beginn jedes Elternteil Unterobjekt wie diese:
C foo; intptr_t offsetB1 = (intptr_t)(B1*)&foo - (intptr_t)&foo;
, die beginnt von den anderen Basen abgeleitet werden können, entsprechend. Auch zur Berechnung dersizeof
aller Klassen sollte Ihnen ein weiterer guter Anhaltspunkt.
Du musst angemeldet sein, um einen Kommentar abzugeben.
Virtuellen Basen sind sehr Verschieden von den gewöhnlichen Basen. Denken Sie daran, dass "virtuelle" bedeutet "zur Laufzeit bestimmt" - so die gesamte Basis Unterobjekt muss zur Laufzeit ermittelt.
Vorstellen, dass Sie eine
B & x
Referenz, und Sie sind beauftragt, Sie zu finden, dieA::a
Mitglied. Wenn die erbschaft real waren, dannB
hat eine SuperklasseA
, und damit dieB
-Objekt, das Sie anzeigen durchx
hat eineA
-Unterobjekt, in dem Sie suchen können Sie Ihre MitgliedA::a
. Wenn die abgeleitete Objekt derx
hat mehrere Basen des TypsA
, dann können Sie nur sehen, dass insbesondere die Kopie, die das teilobjekt derB
.Aber wenn die Vererbung ist virtuell, keiner macht das Sinn. Wir wissen nicht die
A
-Unterobjekt wir brauchen -- diese Informationen einfach nicht existieren zur compile-Zeit. Wir konnten den Umgang mit einem tatsächlichenB
-Objekt, wie imB y; B & x = y;
oder mit einemC
-Objekt wieC z; B & x = z;
oder etwas ganz anderes, das sich praktisch vonA
viele weitere Male. Der einzige Weg, um wissen zu finden, ist die eigentliche BasisA
zur Laufzeit.Diese umgesetzt werden können, mit einer weiteren Ebene von runtime-Dereferenzierung. (Beachten Sie, wie das ist völlig parallel, wie virtuelle Funktionen umgesetzt werden, mit der eine zusätzliche Ebene der runtime-Dereferenzierung im Vergleich zu nicht-virtuellen Funktionen.) Anstelle eines pointer auf eine vtable-oder base-Unterobjekt, eine Lösung ist zum speichern eines Zeigers auf einen Zeiger, um die tatsächliche base-Unterobjekt. Dies wird manchmal als ein "thunk" oder "Trampolin".
Also das eigentliche Objekt
C z;
kann wie folgt Aussehen. Die eigentliche Bestellung im Speicher ist, bis der compiler und unwichtig, und ich habe unterdrückt vtables.So, egal ob Sie einen
B1&
oder eineB2&
Sie zuerst suchen Sie die thunk, und die wiederum sagt dir, wo finden Sie die aktuelle Basis-Unterobjekt. Dies erklärt auch, warum Sie können nicht führen Sie eine statische Besetzung von einerA&
zu einem von der abgeleitete Typen: diese Informationen einfach nicht vorhanden ist zur compile-Zeit.Für eine detailliertere Erklärung, werfen Sie einen Blick auf dieser feine Artikel. (In dieser Beschreibung, wird der thunk Teil der vtable von
C
und virtuelle Vererbung bedingt immer die Wartung von vtables, auch wenn es keine virtuellen Funktionen überall.)Habe ich gepimpt deinen code ein wenig wie folgt:
Wie Sie sehen, dadurch wird einiges an zusätzlichen Informationen, die es uns erlaubt, Rückschlüsse auf die Speicher-layout. Die Ausgabe auf meinem Rechner (64-bit-linux, little endian byte order) ist diese:
So, wir können beschreiben das layout wie folgt:
Hier, xx bezeichnet Polsterung. Beachten Sie, wie der compiler hat die variable
c
in die Polsterung seiner nicht-virtueller Basis. Beachten Sie auch, dass alle drei v-Zeiger Verschieden sind, erlaubt das Programm, um Rückschlüsse auf die korrekte Position aller virtuellen Basen.