本文討論的是下面 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. 在最後新增被虛擬繼承目標的資料 |
參考「稜形繼承的虛擬函式表」一節。 |