本文討論的是下面 3 個問題:
- 以不同方式繼承之后,類的成員變量是如何分布的?
- 虛函數表及虛函數表指針,在可執行文件中的位置?
- 單一繼承、多繼承、虛擬繼承之后,類的虛函數表的內容是如何變化的?
在這里涉及的變量有:有無繼承、有無虛函數、是否多繼承、是否虛繼承。
准備工作
在開始探索類的內存布局之前,我們先了解虛函數表的概念,字節對齊的規則,以及如何打印一個類的內存布局。
查看類的內存布局
我們可以使用 clang++ 來查看類的內存布局:
# 查看對象布局, 要求 main 中有 sizeof(class_t)
clang++ -Xclang -fdump-record-layouts xxx.cpp
# 查看虛函數表布局, 要求 main 中實例化一個對象
clang++ -Xclang -fdump-record-layouts xxx.cpp
# 或者
clang -cc1 -fdump-vtable-layouts -emit-llvm xxx.cpp
虛函數表
- 每個類都有一個屬於自己虛函數表,虛函數表屬於類,而不是某一個實例化對象。
- 如果一個類聲明了虛函數,那么在該類的所有實例化對象中,在
[0, 7]這 8 個字節(假設是 64 位機器),會存放一個虛函數表的指針vtable。 - 虛函數表中的每一個元素都是一個函數地址,指向代碼段的某一虛函數。
- 虛函數表指針
vtable是在對象實例化的時候填入的(因此構造函數不能用virtual聲明為一個虛函數)。- 假設 B 繼承了 A ,假如我們在運行時有
A *a = new B(),那么a->vtable實際上填入的是類 B 的虛函數表地址。 - 如何獲得
vtable的值?通過讀取對象的起始 8 個字節的內容,即*(uint64_t *)&object。
- 假設 B 繼承了 A ,假如我們在運行時有
+---------+ +----------------+
| entity1 | | .text segment |
+---------+ +----------------+
| vtable |-------+ +------->| Entity::vfunc1 |
| member1 | | +-----------------+ | +---->| Entity::vfunc2 |
| member2 | | | Entity's vtable | | | | ... |
+---------+ | +-----------------+ | | +----------------+
+-------->| 0 : vfunc_ptr0 |------+ | | Entity::func1 |
+---------+ | | 1 : vfunc_ptr1 |---------+ | Entity::func2 |
| entity2 | | | ... | | ... |
+---------+ | +-----------------+ +----------------+
| vtable |-------+
| member1 |
| member2 |
+---------+
那么虛函數表(即上圖的 Entity's vtable )會存放在哪里呢?
一個直覺是與 static 成員變量一樣,存放在 .data segment ,因為二者都屬於是類共享的數據。
字節對齊
字節對齊的規則:按照編譯器「已經掃描」的最長的數據類型的字節數 (總是為 1, 2, 4, 8 ) 進行對齊,並且盡量填滿「空隙」。
編譯器是按照聲明順序(從前往后掃描)來解析一個 struct / class 的。
需要注意的是,不同的編譯器,其字節對齊的規則會略有差異,但總的來說是大同小異的。本文所使用的編譯器均為 clang/clang++ 。
例子一
struct Entity
{
char c1;
int val;
};
// sizeof(Entity) = 8
- 如果把
char c1換成short val0,那么還是 8 。 - 如果把
int val換成double d,那么是 16 。
例子二
struct Entity
{
char cval;
short ival;
double dval;
};
/*
*** Dumping AST Record Layout
0 | struct Entity
0 | char cval
2 | short ival
8 | double dval
| [sizeof=16, dsize=16, align=8,
| nvsize=16, nvalign=8]
*/
- 如果
short ival換成int ival,那么ival的起始位置是 4 (因為編譯器掃描到ival的時候,看到的最長字節數是sizeof(int) = 4)。
例子三
struct Entity
{
char cval;
double dval;
char cval2;
int ival;
};
/*
*** Dumping AST Record Layout
0 | struct Entity
0 | char cval
8 | double dval
16 | char cval2
20 | int ival
| [sizeof=24, dsize=24, align=8,
| nvsize=24, nvalign=8]
*/
此處的例子,就是為了說明上述的「盡可能填滿空隙」,注意到 cval2 和 ival 之間留出了 17, 18, 19 這 3 個字節的空白。
- 在
cval2, ival插入任意的一個字節的數據類型(最多插入 3 個),不會影響sizeof(Entity)的大小。 - 如果我們在
cval2, ival之間插入一個short sval,那么sval會位於 18 這一位置。
例子四
如果有虛函數,又會怎么樣呢?
class Entity
{
char cval;
virtual void vfunc() {}
};
/*
*** Dumping AST Record Layout
0 | class Entity
0 | (Entity vtable pointer)
8 | char cval
| [sizeof=16, dsize=9, align=8,
| nvsize=9, nvalign=8]
*/
在 64 位機器上,一個指針的大小是 8 字節,所以編譯器會按照 8 字節對齊。
單一的類
成員變量
考慮無虛函數的條件下,成員變量的內存布局。
class A
{
private:
short val1;
public:
int val2;
double d;
static char ch;
void funcA1() {}
};
int main()
{
__attribute__((unused)) int k = sizeof(A);
}
// clang++ -Xclang -fdump-record-layouts test.cpp
使用上述命令編譯之后,輸出為:
*** Dumping AST Record Layout
0 | class A
0 | short val1
4 | int val2
8 | double d
| [sizeof=16, dsize=16, align=8,
| nvsize=16, nvalign=8]
從上面的輸出可以看出:
static類型的成員並不占用實例化對象的內存(因為static類型的成員存放在靜態數據區.data)。- 成員函數不占用內存(因為存放在代碼段
.text)。 - 成員變量的權限級別
private, public不影響內存布局,內存布局只跟聲明順序有關(可能需要字節對齊)。
虛函數表
class A
{
private:
short val1;
public:
int val2;
double d;
static char ch;
void funcA1() {}
virtual void vfuncA1() {}
virtual void vfuncA2() {}
};
int main()
{
__attribute__((unused)) int k = sizeof(A);
// __attribute__((unused)) A a;
}
內存布局如下:
clang++ -Xclang -fdump-record-layouts test.cpp
*** Dumping AST Record Layout
0 | class A
0 | (A vtable pointer)
8 | short val1
12 | int val2
16 | double d
| [sizeof=24, dsize=24, align=8,
| nvsize=24, nvalign=8]
clang++ -Xclang -fdump-vtable-layouts test.cpp
Original map
Vtable for 'A' (4 entries).
0 | offset_to_top (0)
1 | A RTTI
-- (A, 0) vtable address --
2 | void A::vfuncA1()
3 | void A::vfuncA2()
VTable indices for 'A' (2 entries).
0 | void A::vfuncA1()
1 | void A::vfuncA2()
從這里可以看出,虛函數表的指針默認是存放在一個類的起始位置(一般占用 4 或者 8 字節,視乎機器的字長)。
offset_to_top(0): 表示當前這個虛函數表地址距離對象頂部地址的偏移量,因為對象的頭部就是虛函數表的指針,所以偏移量為0。如果是多繼承的情況,一個類可能存在多個vtable的指針。RTTI: 即 Run Time Type Info, 指向存儲運行時類型信息 (type_info) 的地址,用於運行時類型識別,用於typeid和dynamic_cast。
單一繼承
成員變量
class A
{
public:
char aval;
static int sival;
void funcA1();
};
class B : public A
{
public:
double bval;
void funcB1();
};
class C : public B
{
public:
int cval;
void funcC1() {}
};
內存布局:
clang++ -Xclang -fdump-record-layouts test.cpp
*** Dumping AST Record Layout
0 | class A
0 | char aval
| [sizeof=1, dsize=1, align=1,
| nvsize=1, nvalign=1]
*** Dumping AST Record Layout
0 | class B
0 | class A (base)
0 | char aval
8 | double bval
| [sizeof=16, dsize=16, align=8,
| nvsize=16, nvalign=8]
*** Dumping AST Record Layout
0 | class C
0 | class B (base)
0 | class A (base)
0 | char aval
8 | double bval
16 | int cval
| [sizeof=24, dsize=20, align=8,
| nvsize=20, nvalign=8]
可以看出,普通的單一繼承,成員變量是從上到下依次排列的,並且遵循前面提到的字節對齊規則。
虛函數表
- A 中有 2 個虛函數
vfuncA1, vfuncA2. - B 重寫 (Override) 了
vfuncA1,自定義虛函數vfuncB. - C 重寫了
vfunc1,自定義虛函數vfuncC.
class A
{
public:
char aval;
static int sival;
virtual void vfuncA1() {}
virtual void vfuncA2() {}
};
class B : public A
{
public:
double bval;
virtual void vfuncA1() {}
virtual void vfuncB() {}
};
class C : public B
{
public:
int cval;
virtual void vfuncA1() {}
virtual void vfuncC() {}
};
成員變量布局:
clang++ -Xclang -fdump-record-layouts test.cpp
*** Dumping AST Record Layout
0 | class A
0 | (A vtable pointer)
8 | char aval
| [sizeof=16, dsize=9, align=8,
| nvsize=9, nvalign=8]
*** Dumping AST Record Layout
0 | class B
0 | class A (primary base)
0 | (A vtable pointer)
8 | char aval
16 | double bval
| [sizeof=24, dsize=24, align=8,
| nvsize=24, nvalign=8]
*** Dumping AST Record Layout
0 | class C
0 | class B (primary base)
0 | class A (primary base)
0 | (A vtable pointer)
8 | char aval
16 | double bval
24 | int cval
| [sizeof=32, dsize=28, align=8,
| nvsize=28, nvalign=8]
3 個類的虛函數表如下:
clang++ -Xclang -fdump-vtable-layouts test.cpp
Original map
void C::vfuncA1() -> void B::vfuncA1()
void B::vfuncA1() -> void A::vfuncA1()
Vtable for 'C' (6 entries).
0 | offset_to_top (0)
1 | C RTTI
-- (A, 0) vtable address --
-- (B, 0) vtable address --
-- (C, 0) vtable address --
2 | void C::vfuncA1()
3 | void A::vfuncA2()
4 | void B::vfuncB()
5 | void C::vfuncC()
VTable indices for 'C' (2 entries).
0 | void C::vfuncA1()
3 | void C::vfuncC()
Original map
void C::vfuncA1() -> void B::vfuncA1()
void B::vfuncA1() -> void A::vfuncA1()
Vtable for 'B' (5 entries).
0 | offset_to_top (0)
1 | B RTTI
-- (A, 0) vtable address --
-- (B, 0) vtable address --
2 | void B::vfuncA1()
3 | void A::vfuncA2()
4 | void B::vfuncB()
VTable indices for 'B' (2 entries).
0 | void B::vfuncA1()
2 | void B::vfuncB()
Original map
void C::vfuncA1() -> void B::vfuncA1()
void B::vfuncA1() -> void A::vfuncA1()
Vtable for 'A' (4 entries).
0 | offset_to_top (0)
1 | A RTTI
-- (A, 0) vtable address --
2 | void A::vfuncA1()
3 | void A::vfuncA2()
VTable indices for 'A' (2 entries).
0 | void A::vfuncA1()
1 | void A::vfuncA2()
可以看出,在單一繼承中,子類的虛函數表通過以下步驟構造出來:
- 先拷貝上一層次父類的虛函數表。
- 如果子類有自定義虛函數(例如
B::vfuncB, C::vfuncC),那么直接在虛函數表后追加這些虛函數的地址。 - 如果子類覆蓋了父類的虛函數,使用新地址(例如
B::vfuncA1, C::vfuncA1)覆蓋原有地址(即A::vfunc1)。
多繼承
現直接組合成員變量和虛函數一起來看。
class A
{
char aval;
virtual void vfuncA1() {}
virtual void vfuncA2() {}
};
class B
{
double bval;
virtual void vfuncB1() {}
virtual void vfuncB2() {}
};
class C : public A, public B
{
char cval;
virtual void vfuncC() {}
virtual void vfuncA1() {}
virtual void vfuncB1() {}
};
內存布局如下(注意類 C 的布局):
clang++ -Xclang -fdump-record-layouts test.cpp
*** Dumping AST Record Layout
0 | class A
0 | (A vtable pointer)
8 | char aval
| [sizeof=16, dsize=9, align=8,
| nvsize=9, nvalign=8]
*** Dumping AST Record Layout
0 | class B
0 | (B vtable pointer)
8 | double bval
| [sizeof=16, dsize=16, align=8,
| nvsize=16, nvalign=8]
*** Dumping AST Record Layout
0 | class C
0 | class A (primary base)
0 | (A vtable pointer)
8 | char aval
16 | class B (base)
16 | (B vtable pointer)
24 | double bval
32 | char cval
| [sizeof=40, dsize=33, align=8,
| nvsize=33, nvalign=8]
注意到類 C 的內存布局:
- 一共 40 字節,有 2 個
vtable指針。 - 繼承有
primary base父類和普通base父類之分。
實際上就是:
+--------+--------+---------------+
| offset | size | content |
+--------+--------+---------------+
| 0 | 8 | vtable1 |
| 8 | 1 | aval |
| 9 | 7 | aligned bytes |
| 16 | 8 | vtable2 |
| 24 | 8 | bval |
| 32 | 1 | cval |
| 33 | 7 | aligned bytes |
+--------+--------+---------------+
總的來說,在最底層子類的內存布局中,多繼承的成員變量,以及 vtable 指針的排列規則是:
- 第一個聲明的繼承是
primary base父類。 - 按照繼承的聲明順序依次排列,並需要遵循編譯器的字節對齊規則。
- 最后排列最底層子類的成員變量。
虛函數表如下(省略了 A 和 B 的內容):
clang++ -Xclang -fdump-vtable-layouts test.cpp
Original map
void C::vfuncA1() -> void A::vfuncA1()
Vtable for 'C' (10 entries).
0 | offset_to_top (0)
1 | C RTTI
-- (A, 0) vtable address --
-- (C, 0) vtable address --
2 | void C::vfuncA1()
3 | void A::vfuncA2()
4 | void C::vfuncC()
5 | void C::vfuncB1()
6 | offset_to_top (-16)
7 | C RTTI
-- (B, 16) vtable address --
8 | void C::vfuncB1()
[this adjustment: -16 non-virtual] method: void B::vfuncB1()
9 | void B::vfuncB2()
Thunks for 'void C::vfuncB1()' (1 entry).
0 | this adjustment: -16 non-virtual
VTable indices for 'C' (3 entries).
0 | void C::vfuncA1()
2 | void C::vfuncC()
3 | void C::vfuncB1()
從上面可以看出,C 的虛函數表是由 2 部分組成的:
- 首先是 「C 繼承 A」,按照上述單一繼承的虛函數表生成原則,生成了第一個虛函數表。此時
C::vfuncB1()對於 A 來說是一個自定義的虛函數,因此虛函數表的第一部分有 4 個函數地址。 - 其次是「C 繼承 B」,同樣按照單一繼承的規則生成,但不用追加
C::vfuncC(),因為C::vfuncC()已經在第一部分填入。
可以發現的是:
- C 的虛函數表存在一個重復的函數地址
C::vfuncB1。 - 雖然 C 有 2 個
vtable指針,但仍然只有一個虛函數表( 😅 其實也可以理解為 2 個表,不過這 2 個表是緊挨着的),而 2 個vtable指針指向了虛函數表的不同位置(也許跟編譯器的處理有關,至少 clang 下的情況是這樣的)。
加入虛函數表后,C 的內存布局如下:
+-----------------------+
|-2: offset_to_top(0) |
|-1: C RTTI |
+--------+--------+---------------+ +-----------------------+
| offset | size | content | | class C's vtable |
+--------+--------+---------------+ +-----------------------+
| 0 | 8 | vtable1 |--------------------->| 0: C::vfuncA1_ptr |
| 8 | 1 | aval | | 1: A::vfuncA2_ptr |
| 9 | 7 | aligned bytes | | 2: C::vfuncC_ptr |
| 16 | 8 | vtable2 |------------+ | 3: C::vfuncB1_ptr |
| 24 | 8 | bval | | | 4: offset_to_top(-16) |
| 32 | 1 | cval | | | 5: C RTTI |
| 33 | 7 | aligned bytes | +-------->| 6: C::vfuncB1_ptr |
+--------+--------+---------------+ | 7: B::vfuncB2_ptr |
+-----------------------+
如何驗證這個想法呢?
class A
{
public:
char aval;
virtual void vfuncA1() { cout << "A::vfuncA1()" << endl; }
virtual void vfuncA2() { cout << "A::vfuncA2()" << endl; }
};
class B
{
public:
double bval;
virtual void vfuncB1() { cout << "B::vfuncB1()" << endl; }
virtual void vfuncB2() { cout << "B::vfuncB2()" << endl; }
};
class C : public A, public B
{
public:
char cval;
virtual void vfuncC() { cout << "C::vfuncC()" << endl; }
virtual void vfuncA1() { cout << "C::vfuncA1()" << endl; }
virtual void vfuncB1() { cout << "C::vfuncB1()" << endl; }
};
int main()
{
__attribute__((unused)) int k = sizeof(C);
C c;
uint64_t *cvtable = (uint64_t *)*(uint64_t *)(&c);
uint64_t *cvtable2 = (uint64_t *)*(uint64_t *)((uint8_t *)(&c) + 16);
typedef void (*func_t)(void);
cout << "---- vtable1 ----" << endl;
((func_t)(*(cvtable + 0)))(); // C::vfuncA1()
((func_t)(*(cvtable + 1)))(); // A::vfuncA2()
((func_t)(*(cvtable + 2)))(); // C::vfuncC()
((func_t)(*(cvtable + 3)))(); // C::vfuncB1()
printf("offset_to_top = %d\n", *(cvtable2 - 2)); // -16
cout << "---- vtable2 ----" << endl;
((func_t)(*(cvtable2 + 0)))(); // C::vfuncB1(), same as cvtable + 6
((func_t)(*(cvtable2 + 1)))(); // B::vfuncB2(), same as cvtable + 7
}
棱形繼承和虛擬繼承
如果我們需要用到類似「棱形」的繼承鏈,那么就要通過「虛擬繼承」的方式實現。
假設此處的繼承鏈為:
Base
/ \
A B
\ /
Child
如果不使用 virtual 修飾繼承方式:
class Base { public: int value; };
class A : public Base { };
class B : public Base { };
class Child : public A, public B { };
int main()
{
Child child;
child.value;
}
那么成員變量 child.value 會出現編譯時錯誤 (clang++) ,類似於「命名沖突」。
單一虛擬繼承
class Base
{
char baseval;
virtual void vfuncBase1() {}
virtual void vfuncBase2() {}
};
class A : virtual public Base
{
double aval;
virtual void vfuncBase1() {}
virtual void vfuncA() {}
};
class B : virtual public Base
{
double bval;
virtual void vfuncBase2() {}
virtual void vfuncB() {}
};
以 A 為例子進行說明。成員變量布局:
clang++ -Xclang -fdump-record-layouts diamond2.cpp
*** Dumping AST Record Layout
0 | class A
0 | (A vtable pointer)
8 | double aval
16 | class Base (virtual base)
16 | (Base vtable pointer)
24 | char baseval
| [sizeof=32, dsize=25, align=8,
| nvsize=16, nvalign=8]
與上述的「單一繼承」不同,此處虛擬繼承是會有 2 個 vtable 指針的,並且被虛擬繼承的目標(即 Base 會排列在最后面)。
虛函數表的內容如下:
clang++ -Xclang -fdump-vtable-layouts diamond2.cpp
Original map
Vtable for 'A' (11 entries).
0 | vbase_offset (16)
1 | offset_to_top (0)
2 | A RTTI
-- (A, 0) vtable address --
3 | void A::vfuncBase1()
4 | void A::vfuncA()
5 | vcall_offset (0)
6 | vcall_offset (-16)
7 | offset_to_top (-16)
8 | A RTTI
-- (Base, 16) vtable address --
9 | void A::vfuncBase1()
[this adjustment: 0 non-virtual, -24 vcall offset offset] method: void Base::vfuncBase1()
10 | void Base::vfuncBase2()
Virtual base offset offsets for 'A' (1 entry).
Base | -24
Thunks for 'void A::vfuncBase1()' (1 entry).
0 | this adjustment: 0 non-virtual, -24 vcall offset offset
VTable indices for 'A' (2 entries).
0 | void A::vfuncBase1()
1 | void A::vfuncA()
化簡一下:
A vtable: B vtable:
- A::vfuncBase1() - B::vfuncBase2()
- A::vfuncA() - B::vfuncB()
- A::vfuncBase1() - Base::vfuncBase1()
- Base::vfuncBase2() - B::vfuncBase2()
從上面可以看出:
- 虛函數表的第一部分
3-4,按照A是一個「單一的類」時的規則構造。 - 虛函數表的第二部分
9-10,按照A單一繼承Base的規則構造。
棱形繼承的成員變量
class Child : public A, public B
{
char childval;
virtual void vfuncC() {}
virtual void vfuncB() {}
virtual void vfuncA() {}
};
Child 成員變量內存布局如下:
clang++ -Xclang -fdump-record-layouts diamond.cpp
*** Dumping AST Record Layout
0 | class A
0 | (A vtable pointer)
8 | double aval
16 | class Base (virtual base)
16 | char baseval
| [sizeof=24, dsize=17, align=8,
| nvsize=16, nvalign=8]
*** Dumping AST Record Layout
0 | class B
0 | (B vtable pointer)
8 | double bval
16 | class Base (virtual base)
16 | char baseval
| [sizeof=24, dsize=17, align=8,
| nvsize=16, nvalign=8]
*** Dumping AST Record Layout
0 | class Child
0 | class A (primary base)
0 | (A vtable pointer)
8 | double aval
16 | class B (base)
16 | (B vtable pointer)
24 | double bval
32 | char childval
33 | class Base (virtual base)
33 | char baseval
| [sizeof=40, dsize=34, align=8,
| nvsize=33, nvalign=8]
在 Child 中:
- 成員變量和虛函數指針與「多繼承」的情況相同。
Child把Base(被虛擬繼承的父類)的內容排在最后(比Child的自定義成員還要后),並且只保留了一份Base的數據,這就是虛擬繼承的作用。
棱形繼承的虛函數表
A, B 的虛函數表,如「單一虛擬繼承」一節所述。 Child 的虛函數表如下:
clang++ -Xclang -fdump-vtable-layouts diamond.cpp
Original map
void Child::vfuncA() -> void A::vfuncA()
Vtable for 'Child' (18 entries).
0 | vbase_offset (40)
1 | offset_to_top (0)
2 | Child RTTI
-- (A, 0) vtable address --
-- (Child, 0) vtable address --
3 | void A::vfuncBase1()
4 | void Child::vfuncA()
5 | void Child::vfuncC()
6 | void Child::vfuncB()
7 | vbase_offset (24)
8 | offset_to_top (-16)
9 | Child RTTI
-- (B, 16) vtable address --
10 | void B::vfuncBase2()
11 | void Child::vfuncB()
[this adjustment: -16 non-virtual] method: void B::vfuncB()
12 | vcall_offset (-24)
13 | vcall_offset (-40)
14 | offset_to_top (-40)
15 | Child RTTI
-- (Base, 40) vtable address --
16 | void A::vfuncBase1()
[this adjustment: 0 non-virtual, -24 vcall offset offset] method: void Base::vfuncBase1()
17 | void B::vfuncBase2()
[this adjustment: 0 non-virtual, -32 vcall offset offset] method: void Base::vfuncBase2()
Virtual base offset offsets for 'Child' (1 entry).
Base | -24
Thunks for 'void Child::vfuncB()' (1 entry).
0 | this adjustment: -16 non-virtual
VTable indices for 'Child' (3 entries).
1 | void Child::vfuncA()
2 | void Child::vfuncC()
3 | void Child::vfuncB()
回顧一下 A 和 B 的虛函數表:
A vtable: B vtable:
- A::vfuncBase1() - B::vfuncBase2()
- A::vfuncA() - B::vfuncB()
- A::vfuncBase1() - Base::vfuncBase1()
- Base::vfuncBase2() - B::vfuncBase2()
可以看出,Child 的虛函數表有 2 部分:
- 第一部分
3-6, 10-11,與Child多繼承A, B的構造規則類似,即合並Avtable[0 - 1]和Bvtable[0 - 1]。 - 第二部分
16-17,合並Avtable[2 - 3]和Bvtable[2 - 3]。
總結
| 場景 | 成員變量 | 虛函數表 |
|---|---|---|
| 單一的類 | 按照聲明順序依次排列,並需要遵循字節對齊的規則 | 在對象的起始 8 個字節的內存中,存放 vtable 指針 |
| 單一繼承 | 1. 按照繼承的層次順序,依次排列,並需要遵循字節對齊的規則 2. 只有一個 vtable 指針 |
1. 拷貝上一層次父類的虛函數表 2. 如果有自定義的虛函數,在虛函數表后追加對應的地址 3. 如果 Override 了父類虛函數,那么使用新地址覆蓋原有地址。 |
| 多繼承 | 1. 多個 vtable 指針2. 按照繼承的順序,依次排列父類的 <vtable, members> |
參考「多繼承」一節。 |
| 單一虛擬繼承 | 與普通的單一繼承不同,會有多個 vtable 指針 |
2 部分:第一部分按照「單一的類」規則和第二部分按照「單一繼承」規則。 |
| 棱形繼承 | 1. 與多繼承類似 2. 在最后添加被虛擬繼承目標的數據 |
參考「棱形繼承的虛函數表」一節。 |
