基礎:單繼承
正如我們在類討論的,單繼承導致與佈局導出之前類數據的基礎類數據的對象的佈局。因此,如果類A
和B
被定義正是如此:
class A {
public:
int a;
};
class B : public A {
public:
int b;
};
然後B
類型的對象被佈局是這樣的(其中 「B」 是指向這樣的對象):
b --> +-----------+
| a |
+-----------+
| b |
+-----------+
如果有虛擬方法:
class A {
public:
int a;
virtual void v();
};
class B : public A {
public:
int b;
};
那麼你也會有一個vtable指針:
+-----------------------+
| 0 (top_offset) |
+-----------------------+
b --> +----------+ | ptr to typeinfo for B |
| vtable |-------> +-----------------------+
+----------+ | A::v() |
| a | +-----------------------+
+----------+
| b |
+----------+
即top_offset
,並且typeinfo指針位於vtable指針指向的位置的上方。
簡單的多重繼承
現在考慮多重繼承:
class A {
public:
int a;
virtual void v();
};
class B {
public:
int b;
virtual void w();
};
class C : public A, public B {
public:
int c;
};
在這種情況下,C類的對象佈局是這樣的:
+-----------------------+
| 0 (top_offset) |
+-----------------------+
c --> +----------+ | ptr to typeinfo for C |
| vtable |-------> +-----------------------+
+----------+ | A::v() |
| a | +-----------------------+
+----------+ | -8 (top_offset) |
| vtable |---+ +-----------------------+
+----------+ | | ptr to typeinfo for C |
| b | +---> +-----------------------+
+----------+ | B::w() |
| c | +-----------------------+
+----------+
...但爲什麼?爲什麼兩個vtable在一個?那麼,考慮類型替換。如果我有一個指向C的指針,我可以將它傳遞給一個函數,該函數需要一個指向A的指針或一個期望指向B的函數。如果一個函數需要一個指向A的指針,並且我想將它傳遞給我的變量c(指向C的指針)的值,那麼我已經設置好了。可以通過(第一個)vtable調用A::v()
,並且被調用的函數可以通過我通過的指針訪問成員a,方式與通過任何指針指向的方式相同。但是,如果我將指針變量c
的值傳遞給需要指向B的函數,我們還需要C中的一個子對象B來引用它。這就是爲什麼我們有第二個vtable指針。我們可以將指針值(c + 8字節)傳遞給需要指針指向B的函數,並且它已全部設置:它可以通過(第二個)vtable指針調用B::w()
,並通過我們以同樣的方式傳遞指針,它可以通過指向B的指針傳遞。
請注意,這個「指針校正」也需要針對被調用的方法進行。在這種情況下,類C
繼承B::w()
。當w()
通過C指針被調用時,指針(它變成w()
裏面的這個指針需要調整,這通常叫做這個指針調整)
在某些情況下,編譯器會生成修復地址。考慮上述相同的代碼,但這個時候C
覆蓋B
的成員函數w()
:
class A {
public:
int a;
virtual void v();
};
class B {
public:
int b;
virtual void w();
};
class C : public A, public B {
public:
int c;
void w();
};
C
的對象佈局現在虛函數表是這樣的:
+-----------------------+
| 0 (top_offset) |
+-----------------------+
c --> +----------+ | ptr to typeinfo for C |
| vtable |-------> +-----------------------+
+----------+ | A::v() |
| a | +-----------------------+
+----------+ | C::w() |
| vtable |---+ +-----------------------+
+----------+ | | -8 (top_offset) |
| b | | +-----------------------+
+----------+ | | ptr to typeinfo for C |
| c | +---> +-----------------------+
+----------+ | thunk to C::w() |
+-----------------------+
現在,當w()
被稱爲通過指針B在C
的實例上調用thunk。 thunk是做什麼的?讓我們來拆解(這裏,與gdb
):
0x0804860c <_ZThn8_N1C1wEv+0>: addl $0xfffffff8,0x4(%esp)
0x08048611 <_ZThn8_N1C1wEv+5>: jmp 0x804853c <_ZN1C1wEv>
所以它只是調整this
指針,並跳轉到C::w()
。一切都很好。
但是,以上是否意味着B
的vtable始終指向此C::w()
thunk?我的意思是,如果我們有一個指向B的合法B
(而不是C
),我們不想調用thunk,對吧?
對。以上嵌入的用於B
的的vtable對於B-in-C情況是特殊的。 B的常規虛擬表是正常的,並直接指向B::w()
。
鑽石:基類的多個副本(非虛擬繼承)
好。現在要解決真正困難的事情。形成一個繼承的鑽石時,回想一下基類的多個副本的通常問題:
class A {
public:
int a;
virtual void v();
};
class B : public A {
public:
int b;
virtual void w();
};
class C : public A {
public:
int c;
virtual void x();
};
class D : public B, public C {
public:
int d;
virtual void y();
};
注意D
來自B
和C
和B
和C
無論從A
繼承繼承。這意味着D
在其中有兩個A
的副本。對象佈局和虛函數表嵌入是什麼我們會從前面的章節中預計:
+-----------------------+
| 0 (top_offset) |
+-----------------------+
d --> +----------+ | ptr to typeinfo for D |
| vtable |-------> +-----------------------+
+----------+ | A::v() |
| a | +-----------------------+
+----------+ | B::w() |
| b | +-----------------------+
+----------+ | D::y() |
| vtable |---+ +-----------------------+
+----------+ | | -12 (top_offset) |
| a | | +-----------------------+
+----------+ | | ptr to typeinfo for D |
| c | +---> +-----------------------+
+----------+ | A::v() |
| d | +-----------------------+
+----------+ | C::x() |
+-----------------------+
當然,我們希望A
對象s佈局的數據(成員a
)在D
兩次存在'(這是),並且我們期望A
的虛擬成員函數在vtable中被表示兩次(並且A::v()
確實存在)。好的,這裏沒有新東西。
鑽石:如果我們將虛擬繼承虛基
但什麼的單份? C++虛擬繼承允許我們指定菱形層次結構,但只能保證虛擬繼承基礎的一個副本。所以讓我們這樣寫我們的代碼:
class A {
public:
int a;
virtual void v();
};
class B : public virtual A {
public:
int b;
virtual void w();
};
class C : public virtual A {
public:
int c;
virtual void x();
};
class D : public B, public C {
public:
int d;
virtual void y();
};
所有突然的事情變得更加複雜。如果我們只能有A
一個副本,在我們的D
表示,那麼我們就可以不再逃脫我們的D
嵌入C
(和包埋的D
在D
的虛函數表的C
部分虛函數表的「絕招」 )。但是如果我們不能這樣做,我們如何處理通常的類型替換?
讓我們試着用圖的佈局:
+-----------------------+
| 20 (vbase_offset) |
+-----------------------+
| 0 (top_offset) |
+-----------------------+
| ptr to typeinfo for D |
+----------> +-----------------------+
d --> +----------+ | | B::w() |
| vtable |----+ +-----------------------+
+----------+ | D::y() |
| b | +-----------------------+
+----------+ | 12 (vbase_offset) |
| vtable |---------+ +-----------------------+
+----------+ | | -8 (top_offset) |
| c | | +-----------------------+
+----------+ | | ptr to typeinfo for D |
| d | +-----> +-----------------------+
+----------+ | C::x() |
| vtable |----+ +-----------------------+
+----------+ | | 0 (vbase_offset) |
| a | | +-----------------------+
+----------+ | | -20 (top_offset) |
| +-----------------------+
| | ptr to typeinfo for D |
+----------> +-----------------------+
| A::v() |
+-----------------------+
好。因此,您看到A
現在嵌入在D
中,與其他基本相同。但它嵌入在D中而不是直接派生類中。在多重繼承的存在
@AlokSave,不是該問題的重複,問題是關於縮寫,可能對自己有用。 – unkulunkulu 2013-04-19 08:20:57