C++:虚函数内存布局解析(以 clang 编译器为例)
找遍中文全网,竟然没找到一篇把虚函数、虚继承讲透的。因此花了十多个小时写成此文。基于 Itanium C++ ABI 讲解。对于 clang 和 gcc 基本一致。
基础概念
基础问题
为什么需要虚函数?
主要在于功能解耦合以及规范化的需要。我们很多时候需要做到:一个模块对达成某个功能(机制)有一组可替换的实现(策略)。也就是说,需要策略和机制分离。那么我们的模块就应该只依赖这个机制的接口,而非这个机制的具体实现。则:
-
可以使用纯虚函数要求子类必须实现某个方法。
-
或者使用普通虚函数,允许子类复用这个方法,也可以自己实现这个方法。
实际上,虚函数也并非必须的,C 语言可以通过一些 Tricky 的操作实现多态和继承。但相比于使用裸函数指针,利用 C++ 的语言特性能让我们减轻开发负担,例如编译器会要求覆盖基类虚函数时,函数类型完全一致,如果我们粗心了,会被发现。
更具体地说,虚函数还解决了动态分发的问题。比如下面的代码(警告:实际开发中永远不要这么写)
1#include <cstdio>
2class Base {
3 public:
4 void nonvirtual() { printf("base\n"); }
5};
6
7class Child : public Base {
8 public:
9 void nonvirtual() { printf("child\n"); }
10};
11
12int main() {
13 Child c;
14 Base* b = &c;
15 b->nonvirtual();
16 return 0;
17}
运行后的输出是 base
,那么我们为了使用 child 的功能,就不得不强耦合 child。而且更大的问题是 Base 自动生成了非虚的析构函数,那么我们如果 delete
c 的指针,可能会执行错误的解构函数。
而虚函数可以解决这个问题:
1#include <cstdio>
2class Base {
3 public:
4 virtual void virt() { printf("base\n"); }
5 virtual ~Base() = default;
6};
7
8class Child : public Base {
9 public:
10 virtual void virt() override { printf("child\n"); }
11};
12
13int main() {
14 Child c;
15 Base* b = &c;
16 b->virt();
17 return 0;
18}
输出为 child
,这是怎么实现的呢?正是本文的内容。
虚函数的注意事项
-
基类(无论虚不虚)都必须实现虚析构函数。从而确保执行正确的析构版本,并避免生成移动构造和移动赋值函数。
-
建议所有覆盖都显示地注明
override
。这样编译器才能帮到你。 -
一个派生类的函数如果覆盖了继承而来的虚函数,那么形参类型、返回值类型必须与被覆盖函数完全一致
-
子类覆盖的函数,哪怕没有标注
virtual
,其实质上也是 virtual 的。 -
如果你希望函数不要再被子类覆盖,那么可以使用
final
关键字。1class Child : public Base { 2 public: 3 // v-- here 4 virtual void virt() override final{ printf("child\n"); } 5};
-
如果虚函数有默认实参,那么继承时也应当使用相同的默认值。
-
如果你偏要使用基类的方法,那么可以通过作用域运算符
::
做到。1b->Base::virt();
访问控制
private
和 public
就不说了。protect
能够让成员变成:用户访问不到,子类友元访问得到;且子类和友元只能通过派生类对象访问基类成员,不能直接用基类对象访问基类成员。
最派生类和最派生对象
最派生类:派生关系树的叶子节点。
例如:
下面代码中,mostderived 类是最派生类。md 是最派生对象。
1class base {}; 2class derived : base {}; 3class base2 {}; 4class mostderived : derived, base2 {}; 5 6mostderived md;
参考:c++ - What does the “most derived object” mean? - Stack Overflow
同样的,我们把没有父类的 base 称为最超类。
虚调用偏移
虚调用偏移(vcall offset)是一个偏移量。有一些虚函数,声明于虚基类,重写于派生类。那么派生类就有必要对这些函数的地址进行修正,以便:即便使用虚基类指针,也能访问到派生类重写的函数地址。vcall_offset
只存在于虚继承的场景。
我们举个具体一点的例子。
1#include <cstdio>
2struct ABParent {
3 __int64_t ab;
4 virtual void parent_virtual1() { printf("front ABParent\n"); }
5 virtual ~ABParent() {}
6};
7struct A : virtual ABParent {
8 __int64_t a;
9 virtual void parent_virtual1() override { printf("front A\n"); }
10};
11int main() {
12 auto a = new A();
13 ABParent* p = a;
14 printf("p - a = %ld\n", (__int64_t)p - (__int64_t)a);
15 p->parent_virtual1();
16 delete a;
17 return 0;
18}
为什么要写虚析构函数?
考虑下面的代码,如果用基类指针指向子类对象,那么调用
delete
会导致删除的是基类对象。子类对象没有解构,导致内存泄漏。约定:如果你的类含有virtual
成员或者可能被多态地使用,必须将解构函数标记为virtual
1Base *b = new Derived(); 2delete b;
上述程序会输出
1p - a = 16
2front A
这对于习惯 C 语言的人来说会感到违背直觉。为什么 p 和 a 都能调用到同一个函数,但是 p 和 a 却在不同的地址?
实际上,当执行 p->parent_virtual1();
时,this
指针会进行修正,从而指向真正的对象 A。
在下面的例子中,我们会看到 vcall_offset (-16)
这表示需要将 p
指针加上 -16
作为真正对象 A 的 this
指针。
虚基偏移
虚基偏移(vbase offset)的作用则相反,是为了从真正对象 A 访问到虚对象 ABParent。只存在于虚继承的场景。
如果 vbase_offset
存在,则会位于 A 的 vtable 的第一个 slot. 例如上面的代码生成了这样的 vtable:
1Vtable for 'A' (13 entries).
2 0 | vbase_offset (16)
3 1 | offset_to_top (0)
4 2 | A RTTI
5 -- (A, 0) vtable address --
6 3 | void A::parent_virtual1()
7 4 | A::~A() [complete]
8 5 | A::~A() [deleting]
9 6 | vcall_offset (-16)
10 7 | vcall_offset (-16)
11 8 | offset_to_top (-16)
12 9 | A RTTI
13 -- (ABParent, 16) vtable address --
14 10 | void A::parent_virtual1()
15 [this adjustment: 0 non-virtual, -24 vcall offset offset]
16 11 | A::~A() [complete]
17 [this adjustment: 0 non-virtual, -32 vcall offset offset]
18 12 | A::~A() [deleting]
19 [this adjustment: 0 non-virtual, -32 vcall offset offset]
20
21Virtual base offset offsets for 'A' (1 entry).
22 ABParent | -24
23
24Thunks for 'A::~A()' (1 entry).
25 0 | this adjustment: 0 non-virtual, -32 vcall offset offset
26
27Thunks for 'void A::parent_virtual1()' (1 entry).
28 0 | this adjustment: 0 non-virtual, -24 vcall offset offset
29
30VTable indices for 'A' (3 entries).
31 0 | void A::parent_virtual1()
32 1 | A::~A() [complete]
33 2 | A::~A() [deleting]
对应于这样的 Layout
1*** Dumping AST Record Layout
2 0 | struct ABParent
3 0 | (ABParent vtable pointer)
4 8 | __int64_t ab
5 | [sizeof=16, dsize=16, align=8,
6 | nvsize=16, nvalign=8]
7
8*** Dumping AST Record Layout
9 0 | struct A
10 0 | (A vtable pointer)
11 8 | __int64_t a
12 16 | struct ABParent (virtual base)
13 16 | (ABParent vtable pointer)
14 24 | __int64_t ab
15 | [sizeof=32, dsize=32, align=8,
16 | nvsize=16, nvalign=8]
Layout 的 offset=16 的位置(第 12 行)恰好对应 vtable 中 vbase_offset=16
。再次证明 vbase_offset 就是从子类对象寻找虚基类对象的距离。
虚函数表
虚函数表,简称虚表。是一个信息表,用于分派虚拟函数、访问虚拟基类子对象以及访问运行时类型标识 (RTTI) 的信息。每个具有_虚拟成员函数_或_虚拟基类_的类都有一组关联的虚拟表。如果将特定类用作其他类的基类,则该类可能有多个虚拟表。但是,某些最派生类的所有对象(实例)中的虚拟表指针指向同一组虚拟表。
vptr 不一定是虚表开头
通常,我们认为虚拟表的地址(即 vptr 的地址)可能不是虚拟表的开头。我们称之为虚拟表的地址点。因此,虚拟表可能包含与其地址点的正偏移或负偏移的分量。
虚表在内存中的位置
虚函数表是 C++ 实现多态的方式。运行时,对象存在于堆或者栈上,其上有 vptr,指向虚函数表。虚函数表则位于只读数据区。虚函数表里的函数指针则指向代码段的虚函数代码。
编译选项
本文采用的编译选项是
1clang++ -cc1 -emit-llvm-only -fdump-vtable-layouts main.cpp
gcc 的结果和 clang 是大差不差的。
gcc 8 以前,你可以使用
-fdump-class-hierarchy
参数。在 这里 可以看到生成的虚表。
各种情况下的虚表
无虚函数对象
输入:
1struct Z {
2 void z_do(){}
3};
输出:
1(空)
结论:
无虚函数对象不生成虚函数表。
简单含虚函数对象
输入
1struct Z {
2 virtual void z_virtual(){}
3 virtual void z_pure() = 0;
4};
5
6void Z::z_pure(){}
7
8struct Z {
9 virtual void zf();
10 virtual int zg();
11 void zz();
12};
13
14int Z::zg() {
15 return 0;
16}
输出
1Vtable for 'Z' (4 entries).
2 0 | offset_to_top (0)
3 1 | Z RTTI
4 -- (Z, 0) vtable address --
5 2 | void Z::z_virtual()
6 3 | void Z::z_pure() [pure]
7
8VTable indices for 'Z' (2 entries).
9 0 | void Z::z_virtual()
10 1 | void Z::z_pure()
结论:
简单含虚函数对象产生的虚函数表,内容依次为:
-
offset_to_top
距离对象起始地址的偏移量。当含有多个虚函数表时,这个字段就有意义了
-
RTTI
的type_info
对象地址。 -
各个虚函数的指针。
非虚函数不会出现在这个表中。纯虚函数会出现在这个表中。
由于不存在虚继承,所以 vtable 里不会有 vbase_offset
或 vcall_offset
链式继承的对象
输入:
Z <-- Zson <-- ZgrandSon
1struct Z {
2 virtual void z_virtual() {}
3 virtual void z_pure() = 0;
4 void z_do(){};
5};
6
7struct Zson : Z {
8 int x;
9 virtual void zson_virtual() {}
10 virtual void zson_pure() = 0;
11};
12
13struct ZgrandSon : Zson {
14 int x;
15
16 virtual void zgson_virtual() {}
17 void zson_pure() override {}
18 void z_pure() override {}
19};
20
21int main() {
22 ZgrandSon z;
23 return 0;
24}
输出:
1Vtable for 'ZgrandSon' (7 entries).
2 0 | offset_to_top (0)
3 1 | ZgrandSon RTTI
4 -- (Z, 0) vtable address --
5 -- (ZgrandSon, 0) vtable address --
6 -- (Zson, 0) vtable address --
7 2 | void Z::z_virtual()
8 3 | void ZgrandSon::z_pure()
9 4 | void Zson::zson_virtual()
10 5 | void ZgrandSon::zson_pure()
11 6 | void ZgrandSon::zgson_virtual()
12
13Vtable for 'Zson' (6 entries).
14 0 | offset_to_top (0)
15 1 | Zson RTTI
16 -- (Z, 0) vtable address --
17 -- (Zson, 0) vtable address --
18 2 | void Z::z_virtual()
19 3 | void Z::z_pure() [pure]
20 4 | void Zson::zson_virtual()
21 5 | void Zson::zson_pure() [pure]
22
23Vtable for 'Z' (4 entries).
24 0 | offset_to_top (0)
25 1 | Z RTTI
26 -- (Z, 0) vtable address --
27 2 | void Z::z_virtual()
28 3 | void Z::z_pure() [pure]
可以发现,ZgrandSon
的 vtable RTTI 中线性排列了由远及近的 Z、ZgrandSon、Zson 三个类虚表地址。
而 vtable 中,继承了上一级别父类的 vtable. 而上一级别父类的 vtable 同样继承了更上一层的 vtable. 因此最终 ZgrandSon 的 vtable 既有自己的,也有父类的,也有父类的父类的。并且也是由远及近排列。
至于 ZgrandSon 覆盖父类的函数,也直接替换了 vtable 对应的函数地址。
(偷偷放个防盗标记,如果你在 Less Bug 之外的地方看到了此文,说明它被拷贝构造了)
树形继承的对象
输入:
Z
/ \
Zleft Zright
1struct Z {
2 virtual void z_virtual() {}
3 virtual void z_pure() = 0;
4 void z_do(){};
5};
6
7struct Zleft : Z {
8 int x;
9 virtual void zleft_virtual() {}
10 void z_pure() override {}
11};
12
13struct Zright : Z {
14 int x;
15 virtual void zright_virtual() {}
16 void z_pure() override {}
17};
18
19
20int main() {
21 Zleft zl;
22 Zright zr;
23 return 0;
24}
输出:
1Vtable for 'Zleft' (5 entries).
2 0 | offset_to_top (0)
3 1 | Zleft RTTI
4 -- (Z, 0) vtable address --
5 -- (Zleft, 0) vtable address --
6 2 | void Z::z_virtual()
7 3 | void Zleft::z_pure()
8 4 | void Zleft::zleft_virtual()
9
10Vtable for 'Z' (4 entries).
11 0 | offset_to_top (0)
12 1 | Z RTTI
13 -- (Z, 0) vtable address --
14 2 | void Z::z_virtual()
15 3 | void Z::z_pure() [pure]
16
17Vtable for 'Zright' (5 entries).
18 0 | offset_to_top (0)
19 1 | Zright RTTI
20 -- (Z, 0) vtable address --
21 -- (Zright, 0) vtable address --
22 2 | void Z::z_virtual()
23 3 | void Zright::z_pure()
24 4 | void Zright::zright_virtual()
可以发现,实际就是链式继承的分解情况,没有什么特别的。
树形虚继承的对象
不同之处在于子类通过 virtual 方式继承。
输入:
Z
v/ \v
Zleft Zright
1struct Z {
2 virtual void z_virtual() {}
3 virtual void z_pure() = 0;
4 void z_do(){};
5};
6
7struct Zleft : virtual Z {
8 int x;
9 virtual void zleft_virtual() {}
10 void z_pure() override {}
11};
12
13struct Zright : virtual Z {
14 int x;
15 virtual void zright_virtual() {}
16 void z_pure() override {}
17};
18
19int main() {
20 Zleft zl;
21 Zright zr;
22 return 0;
23}
24
25Vtable for 'Zleft' (8 entries).
26 0 | vbase_offset (0)
27 1 | vcall_offset (0)
28 2 | vcall_offset (0)
29 3 | offset_to_top (0)
30 4 | Zleft RTTI
31 -- (Z, 0) vtable address --
32 -- (Zleft, 0) vtable address --
33 5 | void Z::z_virtual()
34 6 | void Zleft::z_pure()
35 7 | void Zleft::zleft_virtual()
36
37Virtual base offset offsets for 'Zleft' (1 entry).
38 Z | -40
39
40Thunks for 'void Zleft::z_pure()' (1 entry).
41 0 | this adjustment: 0 non-virtual, -32 vcall offset offset
42
43VTable indices for 'Zleft' (2 entries).
44 1 | void Zleft::z_pure()
45 2 | void Zleft::zleft_virtual()
46
47Vtable for 'Z' (4 entries).
48 0 | offset_to_top (0)
49 1 | Z RTTI
50 -- (Z, 0) vtable address --
51 2 | void Z::z_virtual()
52 3 | void Z::z_pure() [pure]
53
54VTable indices for 'Z' (2 entries).
55 0 | void Z::z_virtual()
56 1 | void Z::z_pure()
57
58Vtable for 'Zright' (8 entries).
59 0 | vbase_offset (0)
60 1 | vcall_offset (0)
61 2 | vcall_offset (0)
62 3 | offset_to_top (0)
63 4 | Zright RTTI
64 -- (Z, 0) vtable address --
65 -- (Zright, 0) vtable address --
66 5 | void Z::z_virtual()
67 6 | void Zright::z_pure()
68 7 | void Zright::zright_virtual()
69
70Virtual base offset offsets for 'Zright' (1 entry).
71 Z | -40
72
73Thunks for 'void Zright::z_pure()' (1 entry).
74 0 | this adjustment: 0 non-virtual, -32 vcall offset offset
75
76VTable indices for 'Zright' (2 entries).
77 1 | void Zright::z_pure()
78 2 | void Zright::zright_virtual()
观察:
-
vbase_offset
:表示虚基类 Z 相对于当前对象的位置。 -
vcall_offset
:表示虚基类对象指针修正为实际对象指针,this 要加上的偏移量。
另外需要注意:vcall_offset
的数量取决于虚基类中虚函数的数量。
倒树形继承(多继承)
输入:
A B
\ /
ABChild
1struct A {
2 virtual void a_virtual() {}
3 virtual void a_pure() = 0;
4 void a_do(){};
5};
6struct B {
7 virtual void b_virtual() {}
8 virtual void b_pure() = 0;
9 void b_do(){};
10};
11
12struct ABChild : A, B {
13 void a_pure() override{}
14 void b_pure() override{}
15};
输出:
1Vtable for 'ABChild' (9 entries).
2 0 | offset_to_top (0)
3 1 | ABChild RTTI
4 -- (A, 0) vtable address --
5 -- (ABChild, 0) vtable address --
6 2 | void A::a_virtual()
7 3 | void ABChild::a_pure()
8 4 | void ABChild::b_pure()
9 5 | offset_to_top (-8)
10 6 | ABChild RTTI
11 -- (B, 8) vtable address --
12 7 | void B::b_virtual()
13 8 | void ABChild::b_pure()
14 [this adjustment: -8 non-virtual]
15
16Thunks for 'void ABChild::b_pure()' (1 entry).
17 0 | this adjustment: -8 non-virtual
18
19Vtable for 'A' (4 entries).
20 0 | offset_to_top (0)
21 1 | A RTTI
22 -- (A, 0) vtable address --
23 2 | void A::a_virtual()
24 3 | void A::a_pure() [pure]
25
26Vtable for 'B' (4 entries).
27 0 | offset_to_top (0)
28 1 | B RTTI
29 -- (B, 0) vtable address --
30 2 | void B::b_virtual()
31 3 | void B::b_pure() [pure]
出现了一个名为 Thunk 的奇怪东西。根据 c++ - What is a ’thunk’? - Stack Overflow 的说法,这是一小段代码,用于在调用 ABChild::b_pure()
时对指针进行修正。
还发现:
-
offset_to_top
出现了变化。现在有两个offset_to_top
,一个位于 0, 一个位于 -8.-
它是向负地址增长的。我猜这是为了兼容 c 的 struct 布局。
-
它的数量和多继承数量相同。也就是说,
ABChild
的虚函数表是父母的缝合。你可以理解为 ABChild 有两个并排虚函数表,但空间上放在一块。- 两个虚函数表的 RTTI 分别有各自的父类的 vtable 指针。
-
通过
thunk
对调用不同父类的函数的地址进行修正。
-
需要注意的是,ABChild 的虚函数表虽然有两部分构成,但两部分之间有字节对齐,并不是严格挤在一起。
钻石继承的对象
输入:
1ABParent
2 / \
3A B
4 \ /
5ABChild
1struct ABParent {
2 int k;
3 virtual void parent_virtual1() {}
4 virtual void parent_virtual2() {}
5};
6struct A : virtual ABParent {
7 int a;
8 virtual void a_virtual1() {}
9 virtual void a_virtual2() {}
10 virtual void parent_virtual1() {}
11 virtual void a_pure() = 0;
12 void a_do(){};
13};
14struct B : virtual ABParent {
15 int b;
16 virtual void b_virtual1() {}
17 virtual void b_virtual2() {}
18 virtual void parent_virtual2() {}
19 virtual void b_pure() = 0;
20 void b_do(){};
21};
22
23struct ABChild : A, B {
24 void a_pure() override {}
25 void b_pure() override {}
26 virtual void child_virtual1() {}
27 virtual void child_virtual2() {}
28};
29
30int main() {
31 ABChild abc;
32 return 0;
33}
我们采用了虚继承,从而避免生成两份 ABParent
注意:不应当采用
struct ABChild : virtual A, virtual B
的方式,这样依旧会生成歧义的符号。
输出:
1Vtable for 'ABChild' (23 entries).
2 0 | vbase_offset (32)
3 1 | offset_to_top (0)
4 2 | ABChild RTTI
5 -- (A, 0) vtable address --
6 -- (ABChild, 0) vtable address --
7 3 | void A::a_virtual1()
8 4 | void A::a_virtual2()
9 5 | void A::parent_virtual1()
10 6 | void ABChild::a_pure()
11 7 | void ABChild::b_pure()
12 8 | void ABChild::child_virtual1()
13 9 | void ABChild::child_virtual2()
14 10 | vbase_offset (16)
15 11 | offset_to_top (-16)
16 12 | ABChild RTTI
17 -- (B, 16) vtable address --
18 13 | void B::b_virtual1()
19 14 | void B::b_virtual2()
20 15 | void B::parent_virtual2()
21 16 | void ABChild::b_pure()
22 [this adjustment: -16 non-virtual]
23 17 | vcall_offset (-16)
24 18 | vcall_offset (-32)
25 19 | offset_to_top (-32)
26 20 | ABChild RTTI
27 -- (ABParent, 32) vtable address --
28 21 | void A::parent_virtual1()
29 [this adjustment: 0 non-virtual, -24 vcall offset offset]
30 22 | void B::parent_virtual2()
31 [this adjustment: 0 non-virtual, -32 vcall offset offset]
32
33Virtual base offset offsets for 'ABChild' (1 entry).
34 ABParent | -24
35
36Thunks for 'void ABChild::b_pure()' (1 entry).
37 0 | this adjustment: -16 non-virtual
38
39VTable indices for 'ABChild' (4 entries).
40 3 | void ABChild::a_pure()
41 4 | void ABChild::b_pure()
42 5 | void ABChild::child_virtual1()
43 6 | void ABChild::child_virtual2()
44
45Construction vtable for ('A', 0) in 'ABChild' (13 entries).
46 0 | vbase_offset (32)
47 1 | offset_to_top (0)
48 2 | A RTTI
49 -- (A, 0) vtable address --
50 3 | void A::a_virtual1()
51 4 | void A::a_virtual2()
52 5 | void A::parent_virtual1()
53 6 | void A::a_pure() [pure]
54 7 | vcall_offset (0)
55 8 | vcall_offset (-32)
56 9 | offset_to_top (-32)
57 10 | A RTTI
58 -- (ABParent, 32) vtable address --
59 11 | void A::parent_virtual1()
60 [this adjustment: 0 non-virtual, -24 vcall offset offset]
61 12 | void ABParent::parent_virtual2()
62
63Construction vtable for ('B', 16) in 'ABChild' (13 entries).
64 0 | vbase_offset (16)
65 1 | offset_to_top (0)
66 2 | B RTTI
67 -- (B, 16) vtable address --
68 3 | void B::b_virtual1()
69 4 | void B::b_virtual2()
70 5 | void B::parent_virtual2()
71 6 | void B::b_pure() [pure]
72 7 | vcall_offset (-16)
73 8 | vcall_offset (0)
74 9 | offset_to_top (-16)
75 10 | B RTTI
76 -- (ABParent, 32) vtable address --
77 11 | void ABParent::parent_virtual1()
78 12 | void B::parent_virtual2()
79 [this adjustment: 0 non-virtual, -32 vcall offset offset]
80
81Vtable for 'ABParent' (4 entries).
82 0 | offset_to_top (0)
83 1 | ABParent RTTI
84 -- (ABParent, 0) vtable address --
85 2 | void ABParent::parent_virtual1()
86 3 | void ABParent::parent_virtual2()
87
88VTable indices for 'ABParent' (2 entries).
89 0 | void ABParent::parent_virtual1()
90 1 | void ABParent::parent_virtual2()
91
92Vtable for 'A' (13 entries).
93 0 | vbase_offset (16)
94 1 | offset_to_top (0)
95 2 | A RTTI
96 -- (A, 0) vtable address --
97 3 | void A::a_virtual1()
98 4 | void A::a_virtual2()
99 5 | void A::parent_virtual1()
100 6 | void A::a_pure() [pure]
101 7 | vcall_offset (0)
102 8 | vcall_offset (-16)
103 9 | offset_to_top (-16)
104 10 | A RTTI
105 -- (ABParent, 16) vtable address --
106 11 | void A::parent_virtual1()
107 [this adjustment: 0 non-virtual, -24 vcall offset offset]
108 12 | void ABParent::parent_virtual2()
109
110Virtual base offset offsets for 'A' (1 entry).
111 ABParent | -24
112
113Thunks for 'void A::parent_virtual1()' (1 entry).
114 0 | this adjustment: 0 non-virtual, -24 vcall offset offset
115
116VTable indices for 'A' (4 entries).
117 0 | void A::a_virtual1()
118 1 | void A::a_virtual2()
119 2 | void A::parent_virtual1()
120 3 | void A::a_pure()
121
122Vtable for 'B' (13 entries).
123 0 | vbase_offset (16)
124 1 | offset_to_top (0)
125 2 | B RTTI
126 -- (B, 0) vtable address --
127 3 | void B::b_virtual1()
128 4 | void B::b_virtual2()
129 5 | void B::parent_virtual2()
130 6 | void B::b_pure() [pure]
131 7 | vcall_offset (-16)
132 8 | vcall_offset (0)
133 9 | offset_to_top (-16)
134 10 | B RTTI
135 -- (ABParent, 16) vtable address --
136 11 | void ABParent::parent_virtual1()
137 12 | void B::parent_virtual2()
138 [this adjustment: 0 non-virtual, -32 vcall offset offset]
139
140Virtual base offset offsets for 'B' (1 entry).
141 ABParent | -24
142
143Thunks for 'void B::parent_virtual2()' (1 entry).
144 0 | this adjustment: 0 non-virtual, -32 vcall offset offset
145
146VTable indices for 'B' (4 entries).
147 0 | void B::b_virtual1()
148 1 | void B::b_virtual2()
149 2 | void B::parent_virtual2()
150 3 | void B::b_pure()
钻石继承我们着重分析。先看 A 和 ABParent 的关系。
1struct ABParent {
2 int k;
3 virtual void parent_virtual1() {}
4 virtual void parent_virtual2() {}
5};
6struct A : virtual ABParent {
7 int a;
8 virtual void a_virtual1() {}
9 virtual void a_virtual2() {}
10 virtual void parent_virtual1() {}
11 virtual void a_pure() = 0;
12 void a_do(){};
13};
超类的虚表
ABParent 的虚表没有什么特别的。因为它也没有继承别人。虚表里除了常规的 offset_to_top 和 RTTI 外,只放了两个函数指针
1Vtable for 'ABParent' (4 entries).
2 0 | offset_to_top (0)
3 1 | ABParent RTTI
4 -- (ABParent, 0) vtable address --
5 2 | void ABParent::parent_virtual1()
6 3 | void ABParent::parent_virtual2()
7
8VTable indices for 'ABParent' (2 entries).
9 0 | void ABParent::parent_virtual1()
10 1 | void ABParent::parent_virtual2()
左右派生类的虚表
A 的 vtable 可以分为两个部分,也即两个表:
第一部分:自己的函数信息
1Part I
2Vtable for 'A' (13 entries).
3 0 | vbase_offset (16)
4 1 | offset_to_top (0)
5 2 | A RTTI
6 -- (A, 0) vtable address --
7 3 | void A::a_virtual1()
8 4 | void A::a_virtual2()
9 5 | void A::parent_virtual1()
10 6 | void A::a_pure() [pure]
11 // 未完
vbase_offset = 16
表示虚基类 ABParent 的虚对象,距离该 A 类对象的偏移。
这里表明:虚对象 ABParent 位于 A + 16 的位置。对应的 A 对象布局是这样的:
1*** Dumping AST Record Layout
2 0 | struct A
3 0 | (A vtable pointer)
4 8 | int a
5 16 | struct ABParent (virtual base) // <-------- 16
6 16 | (ABParent vtable pointer)
7 24 | int k
第二部分:父类的函数信息
1Part II
2Vtable for 'A' (13 entries).
3 7 | vcall_offset (0)
4 8 | vcall_offset (-16)
5 9 | offset_to_top (-16)
6 10 | A RTTI
7 -- (ABParent, 16) vtable address --
8 11 | void A::parent_virtual1()
9 [this adjustment: 0 non-virtual, -24 vcall offset offset]
10 12 | void ABParent::parent_virtual2()
注意到 offset_to_top
变成了 -16
,前面提到,这个值的出现表明我们进入了“第二个”虚表。
这里我们发现两个 vcall_offset 不同。为什么呢?
🕷️ 探究过程:注释掉 struct A 中的 parent_virtual1
override.
1Part I
2
3Vtable for 'A' (12 entries).
4 0 | vbase_offset (16)
5 1 | offset_to_top (0)
6 2 | A RTTI
7 -- (A, 0) vtable address --
8 3 | void A::a_virtual1()
9 4 | void A::a_virtual2()
10 5 | void A::a_pure() [pure]
11
12Part II
13 6 | vcall_offset (0)
14 7 | vcall_offset (0) // 注意:这里归零了
15 8 | offset_to_top (-16)
16 9 | A RTTI
17 -- (ABParent, 16) vtable address --
18 10 | void ABParent::parent_virtual1()
19 11 | void ABParent::parent_virtual2()
发现有两处变化:
-
Part I 中的
parent_virtual1
函数没了。 -
第二个
vcall_offset
归零了。
看来:
-
派生类 A 的
vcall_offset
个数取决于虚基类有多少个虚函数。 -
具体的偏移值取决于是否覆盖了虚基类的同名函数,如果覆盖了,那么就会有一个偏移,使得调用虚基类的函数时,加上这个偏移,从而调用到 A 实现的函数。
下面这个 24 怎么算出来的,我没有看懂。希望读者可以帮忙想一想。
1Virtual base offset offsets for 'A' (1 entry).
2 ABParent | -24
它的意思是,如果从 ABParent 的指针动态转化为 A 的指针,需要减少 24。
钻石底(子类)的虚表
ABChild 的对象布局:
1*** Dumping AST Record Layout
2 0 | struct ABChild
3 0 | struct A (primary base)
4 0 | (A vtable pointer)
5 8 | int a
6 16 | struct B (base)
7 16 | (B vtable pointer)
8 24 | int b
9 32 | struct ABParent (virtual base)
10 32 | (ABParent vtable pointer)
11 40 | int k
12 | [sizeof=48, dsize=44, align=8,
13 | nvsize=28, nvalign=8]
虚表:
1Vtable for 'ABChild' (23 entries).
2 0 | vbase_offset (32)
3 1 | offset_to_top (0)
4 2 | ABChild RTTI
5 -- (A, 0) vtable address --
6 -- (ABChild, 0) vtable address --
7 3 | void A::parent_virtual1()
8 4 | void A::a_virtual1()
9 5 | void A::a_virtual2()
10 6 | void ABChild::a_pure()
11 7 | void ABChild::b_pure()
12 8 | void ABChild::child_virtual1()
13 9 | void ABChild::child_virtual2()
14 10 | vbase_offset (16)
15 11 | offset_to_top (-16)
16 12 | ABChild RTTI
17 -- (B, 16) vtable address --
18 13 | void B::b_virtual1()
19 14 | void B::b_virtual2()
20 15 | void B::parent_virtual2()
21 16 | void ABChild::b_pure()
22 [this adjustment: -16 non-virtual]
23 17 | vcall_offset (-16)
24 18 | vcall_offset (-32)
25 19 | offset_to_top (-32)
26 20 | ABChild RTTI
27 -- (ABParent, 32) vtable address --
28 21 | void A::parent_virtual1()
29 [this adjustment: 0 non-virtual, -24 vcall offset offset]
30 22 | void B::parent_virtual2()
31 [this adjustment: 0 non-virtual, -32 vcall offset offset]
32
33Virtual base offset offsets for 'ABChild' (1 entry).
34 ABParent | -24
35
36Thunks for 'void ABChild::b_pure()' (1 entry).
37 0 | this adjustment: -16 non-virtual
38
39VTable indices for 'ABChild' (4 entries).
40 3 | void ABChild::a_pure()
41 4 | void ABChild::b_pure()
42 5 | void ABChild::child_virtual1()
43 6 | void ABChild::child_virtual2()
44
45Construction vtable for ('A', 0) in 'ABChild' (13 entries).
46 0 | vbase_offset (32)
47 1 | offset_to_top (0)
48 2 | A RTTI
49 -- (A, 0) vtable address --
50 3 | void A::parent_virtual1()
51 4 | void A::a_virtual1()
52 5 | void A::a_virtual2()
53 6 | void A::a_pure() [pure]
54 7 | vcall_offset (0)
55 8 | vcall_offset (-32)
56 9 | offset_to_top (-32)
57 10 | A RTTI
58 -- (ABParent, 32) vtable address --
59 11 | void A::parent_virtual1()
60 [this adjustment: 0 non-virtual, -24 vcall offset offset]
61 12 | void ABParent::parent_virtual2()
62
63Construction vtable for ('B', 16) in 'ABChild' (13 entries).
64 0 | vbase_offset (16)
65 1 | offset_to_top (0)
66 2 | B RTTI
67 -- (B, 16) vtable address --
68 3 | void B::b_virtual1()
69 4 | void B::b_virtual2()
70 5 | void B::parent_virtual2()
71 6 | void B::b_pure() [pure]
72 7 | vcall_offset (-16)
73 8 | vcall_offset (0)
74 9 | offset_to_top (-16)
75 10 | B RTTI
76 -- (ABParent, 32) vtable address --
77 11 | void ABParent::parent_virtual1()
78 12 | void B::parent_virtual2()
79 [this adjustment: 0 non-virtual, -32 vcall offset offset]
可以分成三个部分来看。
ABChild 的虚表
ABChild 的虚表又由三个部分组成。
-
自己的,以及 Primary Base(继承的第一个父类)的各虚函数。
- 其中
vbase_offset (32)
表明从 ABChild 对象访问到虚基类对象需要往前偏移 32。
查询对象布局发现,这个位置是 ABParent,也即最超类。
32 | struct ABParent (virtual base)
-
offset_to_top (0)
说明这是虚表组的第一个虚表。 -
再往后是 RTTI 信息和各个函数。包括所有继承而来的函数。
1Part I 2Vtable for 'ABChild' (23 entries). 3 0 | vbase_offset (32) 4 1 | offset_to_top (0) 5 2 | ABChild RTTI 6 -- (A, 0) vtable address -- 7 -- (ABChild, 0) vtable address -- 8 3 | void A::parent_virtual1() 9 4 | void A::a_virtual1() 10 5 | void A::a_virtual2() 11 6 | void ABChild::a_pure() 12 7 | void ABChild::b_pure() 13 8 | void ABChild::child_virtual1() 14 9 | void ABChild::child_virtual2()
- 其中
-
父类 B 的各虚函数。并且对于被 ABChild 给覆盖的函数
b_pure
,指针换成了 ABChild 的。-
其中
vbase_offset (16)
表明,从B*
访问虚基类ABParent*
的对象需要往前偏移 16 -
offset_to_top (-16)
表明,这是虚表组中的第二个虚表。
1Part II 2Vtable for 'ABChild' (23 entries). 3 10 | vbase_offset (16) 4 11 | offset_to_top (-16) 5 12 | ABChild RTTI 6 -- (B, 16) vtable address -- 7 13 | void B::b_virtual1() 8 14 | void B::b_virtual2() 9 15 | void B::parent_virtual2() 10 16 | void ABChild::b_pure() 11 [this adjustment: -16 non-virtual]
-
-
父类 ABParent 的各虚函数。并且对于被 ABChild 给覆盖的函数
b_pure
,指针换成了 ABChild 的。-
vbase_offset
无了,这说明这是一个最超类的虚表。 -
vcall_offset (-16)
表明虚函数parent_virtual1
的实际位置应该到this
偏移-16
的地方去找。这自然会找到类 B 的各虚函数。然后定位到parent_virtual2
函数。 -
vcall_offset (-32)
表明虚函数parent_virtual2
的实际位置应该到this
偏移-32
的地方去找。这自然会找到类 A 的各虚函数。然后定位到parent_virtual1
函数。
1Part III 2Vtable for 'ABChild' (23 entries). 3 17 | vcall_offset (-16) 4 18 | vcall_offset (-32) 5 19 | offset_to_top (-32) 6 20 | ABChild RTTI 7 -- (ABParent, 32) vtable address -- 8 21 | void A::parent_virtual1() 9 [this adjustment: 0 non-virtual, -24 vcall offset offset] 10 22 | void B::parent_virtual2() 11 [this adjustment: 0 non-virtual, -32 vcall offset offset]
-
A 的虚表
A 的虚表同样可以看成两部分。
要注意的是这里是 Construction 过程。最终生成的 A 虚表实际上和 ABChild 虚表合并,放在 ABChild 虚表的第一部分。
-
Part I 是 A 自身的虚函数
-
vbase_offset (32) 表明如果要寻找虚基类 ABParent,需要往前 32 个偏移。但是如果只看 A 的结构体对象布局,你会发现明明 ABParent 在 +16 的地方。为什么会这样呢?实际上这里 A 的虚表是作为 ABChild 的超类的虚表,而非 A 自身的虚表。
-
因此 32 是查询 ABChild 的对象内存布局得到,而非从 A 的对象布局得到。
1Part I 2Construction vtable for ('A', 0) in 'ABChild' (13 entries). 3 0 | vbase_offset (32) 4 1 | offset_to_top (0) 5 2 | A RTTI 6 -- (A, 0) vtable address -- 7 3 | void A::parent_virtual1() 8 4 | void A::a_virtual1() 9 5 | void A::a_virtual2() 10 6 | void A::a_pure() [pure]
-
-
Part II 是 ABParent 的虚函数。参数就不废话了。
1Part II 2Construction vtable for ('A', 0) in 'ABChild' (13 entries). 3 7 | vcall_offset (0) 4 8 | vcall_offset (-32) 5 9 | offset_to_top (-32) 6 10 | A RTTI 7 -- (ABParent, 32) vtable address -- 8 11 | void A::parent_virtual1() 9 [this adjustment: 0 non-virtual, -24 vcall offset offset] 10 12 | void ABParent::parent_virtual2()
B 的虚表
由于 A、B 是对称的,所以内容并没有很大的差异。但由于 A、B 的排布是线性的,所以 vbase_offset
即距离 ABParent 的距离就有所不同了。
1Construction vtable for ('B', 16) in 'ABChild' (13 entries).
2 0 | vbase_offset (16)
3 1 | offset_to_top (0)
4 2 | B RTTI
5 -- (B, 16) vtable address --
6 3 | void B::b_virtual1()
7 4 | void B::b_virtual2()
8 5 | void B::parent_virtual2()
9 6 | void B::b_pure() [pure]
10 7 | vcall_offset (-16)
11 8 | vcall_offset (0)
12 9 | offset_to_top (-16)
13 10 | B RTTI
14 -- (ABParent, 32) vtable address --
15 11 | void ABParent::parent_virtual1()
16 12 | void B::parent_virtual2()
17 [this adjustment: 0 non-virtual, -32 vcall offset offset]
钻石继承总结
菱形(💎)继承时,会为 ABChild 的各父类新建虚表(而非沿用它们已有的)这是为了确保生成正确的 vbase_offset
和 vcall_offset
。
-
vbase_offset
用于找虚基类对象。(虚基类对象并不是一个真实的独立对象,只是在此对象内 this 指针偏移后形成的一个对象,从而可以调用虚基类对象上的方法) -
vcall_offset
表示继承的对应位置的虚函数如果想要被调用,this 指针应该偏移的位置。 -
每个虚表可以从顶部开始、往后由多个部分组成,各部分之间可以通过
offset_to_top
划分。offset_to_top
自身也代表了距离顶部的距离。
而在非多重继承中,共享虚表才是可能的。
我们主要关注 ABChild(钻石底)的虚表。
-
ABChild 虽然总共有三个父类,按理说虚表应该有四部分。但第一个父类(Primary Base) A 和 ABChild 自身一起划入了第一个部分虚表。剩下 B 和 ABParent 各占一部分虚表。
-
每个部分虚表由一系列 slot 构成,从上到下依次是:
-
vcall_offset(如果有)
-
vbase_offset(如果有)
-
offset_to_top
-
RTTI(存放了类型信息 type_info 静态对象)
-
成员函数
-
-
除此之外,编译器还会生成一些 Thunk 代码,用于临时调整指针的位置。
还有一些小细节,比如:Base* p = child;
实际上暗含一个指针调整,导致 p
指向的是虚基类对象,child
指向的是真·对象。因此,对于向下转换(基类指针转派生类)我们不能用 static_cast
或者 C 风格括号转换表达式进行转换,而应该用 dynamic_cast
。转换之后用 if 判断转换是否成功。
而在内存布局中,ABChild 中依次排列 A、B、ABParent 三个虚对象,每个虚对象拥有自己的虚表指针 vptr 和数据成员。
参考文献
-
C++ Primer 中文第 5 版,第 15 章