2009-02-18 5 views
10

Hat jedes Objekt der virtuellen Klasse einen Zeiger auf vtable?Hat jedes Objekt der virtuellen Klasse einen Zeiger auf vtable?

Oder nur das Objekt der Basisklasse mit virtueller Funktion hat es?

Wo wurde die VTable gespeichert? Codeabschnitt oder Datenabschnitt des Prozesses?

+0

Duplizieren? http://stackoverflow.com/questions/99297/at-as-deep-of-a-level-as-possible-how-are-virtual-functions-implementiert – Anonymous

+0

Es gibt keine so genannte "virtuelle Klasse" in C++. – curiousguy

Antwort

1

Alle virtuellen Klassen haben in der Regel eine vtable, die vom C++ - Standard nicht benötigt wird und die Speichermethode vom Compiler abhängig ist.

4

Vtable ist eine Klasseninstanz, d. H. Wenn ich 10 Objekte einer Klasse mit einer virtuellen Methode habe, gibt es nur eine Vtable, die von allen 10 Objekten gemeinsam genutzt wird.

Alle 10 Objekte verweisen in diesem Fall auf denselben vtable.

+0

Was ist mit Vptr, es wird 10 vptr mit jedem Objekt verbunden sein oder wie eine einzelne vtable wird es nur einen vptr geben? – Rndp13

0

Jedes Objekt des polymorphen Typs hat einen Zeiger auf Vtable.

Wo VTable gespeichert wird, ist abhängig vom Compiler.

15

Alle Klassen mit einer virtuellen Methode haben eine einzelne vtable, die von allen Objekten der Klasse gemeinsam genutzt wird.

Jede Objektinstanz wird einen Zeiger auf diese vtable haben (so wird die vtable gefunden), normalerweise vptr genannt. Der Compiler erzeugt implizit Code zum Initialisieren des vptr im Konstruktor.

Beachten Sie, dass nichts davon von der C++ - Sprache vorgeschrieben ist - eine Implementierung kann den virtuellen Versand auf andere Weise handhaben, wenn sie es möchte. Dies ist jedoch die Implementierung, die von jedem mir bekannten Compiler verwendet wird. Stan Lippmans Buch "Inside the C++ Object Model" beschreibt, wie das sehr gut funktioniert.

+2

+1 Und würden Sie bitte erklären, warum der virtuelle Zeiger pro Objekt und nicht pro Klasse ist? Vielen Dank. – Viet

+1

@Viet Sie können sich den vPtr als Bootstrap für die Laufzeitdefinition eines Objekts vorstellen. Erst nachdem das vPtr eingerichtet wurde, kann das Objekt wissen, was sein tatsächlicher Typ ist. In diesem Sinne macht das Erstellen eines vPtr pro Klasse (statisch) keinen Sinn. Wenn Sie darüber nachdenken, dass ein Objekt kein vPtr benötigt, muss es bereits während der Kompilierung über seine Laufzeitdefinition Bescheid wissen, was im Widerspruch zu einem dynamisch aufgelösten Objekt steht. –

0

Nicht unbedingt

ziemlich jedes Objekt, das eine virtuelle Funktion hat, wird eine V-Tabelle Zeiger haben. Für jede Klasse, die eine virtuelle Funktion hat, von der das Objekt abgeleitet ist, muss kein v-Tabellenzeiger vorhanden sein.

Neue Compiler, die den Code ausreichend analysieren, können in manchen Fällen v-Tabellen eliminieren. Beispielsweise in einem einfachen Fall: Wenn Sie nur eine konkrete Implementierung einer abstrakten Basisklasse haben, weiß der Compiler, dass er die virtuellen Aufrufe als normale Funktionsaufrufe ändern kann, denn immer wenn die virtuelle Funktion aufgerufen wird, wird sie immer funktionieren auf genau die gleiche Funktion auflösen.

Auch wenn es nur ein paar verschiedene konkrete Funktionen gibt, kann der Compiler die Call-Site effektiv ändern, so dass er ein 'if' verwendet, um die richtige konkrete Funktion auszuwählen, die aufgerufen werden soll.

Also, in Fällen wie diesem wird die V-Tabelle nicht benötigt und die Objekte könnten am Ende keine haben.

+0

Hmm. Ich habe gerade versucht, einen Compiler zu finden, der die Eliminierung von Zeigern in V-Tabellen durchführt. Sieht nicht so aus, als gäbe es momentan keine. Aber das Teilen von Informationen zwischen Compilern und Linkern wird hoch, so dass sie miteinander verschmelzen. Bei fortgesetzter Entwicklung kann dies passieren. –

+0

Dies könnte daran liegen, dass die tatsächliche Beseitigung des vptr eine schwerwiegende Verletzung des ABI bedeuten würde - und dies würde sicherstellen müssen, dass jedes Objekt der fraglichen Klasse nie außerhalb des Moduls gesehen wird - für nur 4 Bytes Speicher, was sogar passieren könnte nicht tatsächlich gespeichert werden – jpalecek

+0

OTOH, nur nicht die Methoden durch virtuelle Dispatch aufrufen bricht nur die Schnittstelle dieser bestimmten Methode, und der Compiler kann dies lösen, indem er eine andere Version des Codes mit vollständigen virtuellen Versand ausgibt. Es gibt auch einen größeren Vorteil, vor allem, wenn die Funktion dann inline sein kann – jpalecek

4

dieses zu Hause versuchen:

#include <iostream> 
struct non_virtual {}; 
struct has_virtual { virtual void nop() {} }; 
struct has_virtual_d : public has_virtual { virtual void nop() {} }; 

int main(int argc, char* argv[]) 
{ 
    std::cout << sizeof non_virtual << "\n" 
      << sizeof has_virtual << "\n" 
      << sizeof has_virtual_d << "\n"; 
} 
+1

Antworten für VS2005: 1, 4, 4 –

+1

Das Zeichnen der notwendigen Schlussfolgerung war 'links als eine Zuweisung' für das OP;) – dirkgently

+0

Diese Zahlen sind zwar typisch, aber nicht erforderlich. Es wird nicht angegeben, wie viele VTables vorhanden sind oder wofür diese 4 Bytes ausgegeben werden. – jalf

2

A VTable ist ein Detail Umsetzung nichts da in der Sprachdefinition ist, die sagt, es existiert. In der Tat habe ich über alternative Methoden zur Implementierung von virtuellen Funktionen gelesen.

ABER: Alle gängigen Compiler (dh diejenigen, die ich kenne) verwenden VTabels.
Dann ja. Jede Klasse, die über eine virtuelle Methode verfügt oder von einer Klasse (direkt oder indirekt) abgeleitet ist, die über eine virtuelle Methode verfügt, verfügt über Objekte mit einem Zeiger auf eine VTable.

Alle anderen Fragen, die Sie stellen, hängen vom Compiler/der Hardware ab. Es gibt keine echte Antwort auf diese Fragen.

11

Wie eine andere Person sagte, schreibt der C++ Standard keine virtuelle Methodentabelle vor, sondern erlaubt die Verwendung einer Tabelle. Ich habe meine Tests gemacht gcc verwenden und diesen Code und eine der einfachsten möglichen Szenario:

class Base { 
public: 
    virtual void bark() { } 
    int dont_do_ebo; 
}; 

class Derived1 : public Base { 
public: 
    virtual void bark() { } 
    int dont_do_ebo; 
}; 

class Derived2 : public Base { 
public: 
    virtual void smile() { } 
    int dont_do_ebo; 
}; 

void use(Base*); 

int main() { 
    Base * b = new Derived1; 
    use(b); 

    Base * b1 = new Derived2; 
    use(b1); 
} 

Added Daten-Mitglieder den Compiler zu verhindern, dass die Basis-Klasse eine größen zu geben, von Null (es ist bekannt als die Leer-Basis-Klassen-Optimierung). Dies ist das Layout, das GCC gewählt: (drucken -fdump-Klasse-Hierarchie)

Vtable for Base 
Base::_ZTV4Base: 3u entries 
0  (int (*)(...))0 
4  (int (*)(...))(& _ZTI4Base) 
8  Base::bark 

Class Base 
    size=8 align=4 
    base size=8 base align=4 
Base (0xb7b578e8) 0 
    vptr=((& Base::_ZTV4Base) + 8u) 

Vtable for Derived1 
Derived1::_ZTV8Derived1: 3u entries 
0  (int (*)(...))0 
4  (int (*)(...))(& _ZTI8Derived1) 
8  Derived1::bark 

Class Derived1 
    size=12 align=4 
    base size=12 base align=4 
Derived1 (0xb7ad6400) 0 
    vptr=((& Derived1::_ZTV8Derived1) + 8u) 
    Base (0xb7b57ac8) 0 
     primary-for Derived1 (0xb7ad6400) 

Vtable for Derived2 
Derived2::_ZTV8Derived2: 4u entries 
0  (int (*)(...))0 
4  (int (*)(...))(& _ZTI8Derived2) 
8  Base::bark 
12 Derived2::smile 

Class Derived2 
    size=12 align=4 
    base size=12 base align=4 
Derived2 (0xb7ad64c0) 0 
    vptr=((& Derived2::_ZTV8Derived2) + 8u) 
    Base (0xb7b57c30) 0 
     primary-for Derived2 (0xb7ad64c0) 

Wie Sie jede Klasse sehen eine VTable hat. Die ersten beiden Einträge sind speziell. Der zweite zeigt auf die RTTI-Daten der Klasse. Der erste - ich wusste es aber vergaß. Es hat etwas Verwendung in komplizierteren Fällen. Nun, wie das Layout zeigt, wenn Sie ein Objekt der Klasse Derived1 haben, dann zeigt der vptr (v-table-pointer) natürlich auf die v-Tabelle der Klasse Derived1, die genau einen Eintrag für ihre Funktion bark aufweist, die auf zeigt Derived1s Version. Der vptr von Derived2 verweist auf die vtable von Derived2, die zwei Einträge enthält. Der andere ist die neue Methode, die hinzugefügt wird, Lächeln. Es wiederholt den Eintrag für Base :: bark, der auf die Base-Version der Funktion natürlich zeigen wird, weil es die abgeleitete Version davon ist.

Ich habe auch den Baum, der von GCC generiert wird, nachdem einige Optimierungen durchgeführt wurden (Konstruktor inlined, ...), mit -fdump-tree-optimized. Der Ausgang wird mit GCC Mitte-End-Sprache GIMPL, die Front-End-unabhängigen, eingekerbten in eine C-ähnlichen Blockstruktur ist:

;; Function virtual void Base::bark() (_ZN4Base4barkEv) 
virtual void Base::bark() (this) 
{ 
<bb 2>: 
    return; 
} 

;; Function virtual void Derived1::bark() (_ZN8Derived14barkEv) 
virtual void Derived1::bark() (this) 
{ 
<bb 2>: 
    return; 
} 

;; Function virtual void Derived2::smile() (_ZN8Derived25smileEv) 
virtual void Derived2::smile() (this) 
{ 
<bb 2>: 
    return; 
} 

;; Function int main() (main) 
int main()() 
{ 
    void * D.1757; 
    struct Derived2 * D.1734; 
    void * D.1756; 
    struct Derived1 * D.1693; 

<bb 2>: 
    D.1756 = operator new (12); 
    D.1693 = (struct Derived1 *) D.1756; 
    D.1693->D.1671._vptr.Base = &_ZTV8Derived1[2]; 
    use (&D.1693->D.1671); 
    D.1757 = operator new (12); 
    D.1734 = (struct Derived2 *) D.1757; 
    D.1734->D.1682._vptr.Base = &_ZTV8Derived2[2]; 
    use (&D.1734->D.1682); 
    return 0;  
} 

Wie wir schön sehen, es ist nur einen Zeiger einstellen - die vptr - das wird Zeigen Sie auf die entsprechende vtable, die wir bereits beim Erstellen des Objekts gesehen haben. Ich habe auch den Assembler-Code für die Erstellung der Derived1 und Aufruf ($ 4 ist das erste Argument-Register, $ 2 ist Rückgabewert-Register, $ 0 ist immer-0-Register) nach dem Ablehnen der Namen in ihm durch die c++filt Werkzeug ausgegeben:

)
 # 1st arg: 12byte 
    add  $4, $0, 12 
     # allocate 12byte 
    jal  operator new(unsigned long)  
     # get ptr to first function in the vtable of Derived1 
    add  $3, $0, vtable for Derived1+8 
     # store that pointer at offset 0x0 of the object (vptr) 
    stw  $3, $2, 0 
     # 1st arg is the address of the object 
    add  $4, $0, $2 
    jal  use(Base*) 

Was passiert, wenn wir bark anrufen möchten, geschieht:

void doit(Base* b) { 
    b->bark(); 
} 

Gimpl Code:

;; Function void doit(Base*) (_Z4doitP4Base) 
void doit(Base*) (b) 
{ 
<bb 2>: 
    OBJ_TYPE_REF(*b->_vptr.Base;b->0) (b) [tail call]; 
    return; 
} 

OBJ_TYPE_REF ist ein GIMP L-Konstrukt, das in ziemlich gedruckt wird (es ist in gcc/tree.def im gcc SVN Source-Code dokumentiert ist)

OBJ_TYPE_REF(<first arg>; <second arg> -> <third arg>) 

Es bedeutet: Mit dem Ausdruck *b->_vptr.Base auf dem Objekt b, und speichern das Frontend (C++) spezifischen Wert 0 (es ist der Index in der vtable). Schließlich gibt es b als "dieses" Argument. Würden wir eine Funktion aufrufen, die am zweiten Index in der vtable erscheint (Anmerkung, wir wissen nicht, welche vtable welchen Typs!), Würde die Gimpl wie folgt aussehen:

OBJ_TYPE_REF(*(b->_vptr.Base + 4);b->1) (b) [tail call]; 

Natürlich hier die Assembler-Code wieder (Stack-Frame-Material abgeschnitten):

# load vptr into register $2 
    # (remember $4 is the address of the object, 
    # doit's first arg) 
ldw  $2, $4, 0 
    # load whatever is stored there into register $2 
ldw  $2, $2, 0 
    # jump to that address. note that "this" is passed by $4 
jalr $2 

Denken Sie daran, dass die vptr Punkte genau an der ersten Funktion . (Vor diesem Eintrag wurde der RTTI-Slot gespeichert). Also, was immer an diesem Slot erscheint, wird aufgerufen. Es markiert auch den Aufruf als Tail-Call, weil es als letzte Anweisung in unserer doit Funktion passiert.

1

Um die Frage zu beantworten, welche Objekte (Instanzen von nun an) vtables und wo haben, ist es hilfreich darüber nachzudenken, wann Sie einen vtable-Zeiger benötigen.

Für jede Vererbungshierarchie benötigen Sie eine vtable für jeden Satz virtueller Funktionen, die von einer bestimmten Klasse in dieser Hierarchie definiert sind. Mit anderen Worten, angesichts der folgenden:

class A { virtual void f(); int a; }; 
class B: public A { virtual void f(); virtual void g(); int b; }; 
class C: public B { virtual void f(); virtual void g(); virtual void h(); int c; }; 
class D: public A { virtual void f(); int d; }; 
class E: public B { virtual void f(); int e; }; 

Als Ergebnis müssen Sie fünf vtables: A, B, C, D und E alle ihre eigenen vtables benötigen.

Als nächstes müssen Sie wissen, welche vtable bei einem Zeiger oder Verweis auf eine bestimmte Klasse verwendet werden soll. Z. B. müssen Sie, wenn Sie einen Zeiger auf A haben, genug über das Layout von A wissen, so dass Sie eine vtable erhalten, die Ihnen sagt, wohin Sie A :: f() schicken sollen. Wenn Sie einen Zeiger auf B haben, müssen Sie genug über das Layout von B wissen, um B :: f() und B :: g() zu versenden. Und so weiter und so weiter.

Eine mögliche Implementierung könnte einen Vtable-Zeiger als erstes Mitglied einer Klasse setzen. Das würde bedeuten, das Layout einer Instanz von A wäre:

A's vtable; 
int a; 

Und eine Instanz von B wäre:

A's vtable; 
int a; 
B's vtable; 
int b; 

Und man konnte richtigen virtuellen Dispatching-Code aus diesem Layout erzeugen.

Sie können das Layout auch optimieren, indem Sie vtable-Zeiger von VTables mit dem gleichen Layout kombinieren oder wenn eine Teilmenge der anderen ist. So in dem obigen Beispiel könnten Sie auch Layout B als:

B's vtable; 
int a; 
int b; 

Weil B VTable eine Obermenge von A ist ist. B's vtable hat Einträge für A :: f und B :: g, und A's vtable hat Einträge für A :: f.

Für Vollständigkeit, das ist, wie Sie die vtables alle würden Layout wir bisher gesehen haben:

A's vtable: A::f 
B's vtable: A::f, B::g 
C's vtable: A::f, B::g, C::h 
D's vtable: A::f 
E's vtable: A::f, B::g 

Und die tatsächlichen Einträge wäre:

A's vtable: A::f 
B's vtable: B::f, B::g 
C's vtable: C::f, C::g, C::h 
D's vtable: D::f 
E's vtable: E::f, B::g 

Für Mehrfachvererbung, was Sie tun die gleiche Analyse:

class A { virtual void f(); int a; }; 
class B { virtual void g(); int b; }; 
class C: public A, public B { virtual void f(); virtual void g(); int c; }; 

und die daraus resultierenden Layouts wäre:

Sie benötigen einen Zeiger auf eine mit A kompatible Vtable und einen Zeiger auf eine mit B kompatible Vtable, da eine Referenz auf C in eine Referenz von A oder B konvertiert werden kann und Sie virtuelle Funktionen an C senden müssen.

Daraus können Sie sehen, dass die Anzahl der Vtable-Zeiger einer bestimmten Klasse mindestens die Anzahl der Stammklassen ist, von denen sie abgeleitet ist (entweder direkt oder aufgrund einer Superklasse). Eine Stammklasse ist eine Klasse mit einer vtable, die nicht von einer Klasse mit einer vtable erbt.

Die virtuelle Vererbung wirft ein weiteres Indiz in die Mischung, aber Sie können dieselbe Metrik verwenden, um die Anzahl der vtable-Zeiger zu bestimmen.

+0

Bitte zeigen Sie, was in der Antwort falsch ist, wenn Sie es abstimmen. Ansonsten können wir den Inhalt nicht verbessern! Vielen Dank. –

Verwandte Themen