C++:虚函数内存布局解析(以 clang 编译器为例)

找遍中文全网,竟然没找到一篇把虚函数、虚继承讲透的。因此花了十多个小时写成此文。基于 Itanium C++ ABI 讲解。对于 clang 和 gcc 基本一致。

基础概念

基础问题

为什么需要虚函数?

主要在于功能解耦合以及规范化的需要。我们很多时候需要做到:一个模块对达成某个功能(机制)有一组可替换的实现(策略)。也就是说,需要策略和机制分离。那么我们的模块就应该只依赖这个机制的接口,而非这个机制的具体实现。则:

  1. 可以使用纯虚函数要求子类必须实现某个方法。

  2. 或者使用普通虚函数,允许子类复用这个方法,也可以自己实现这个方法。

实际上,虚函数也并非必须的,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 ,这是怎么实现的呢?正是本文的内容。

虚函数的注意事项

  1. 基类(无论虚不虚)都必须实现虚析构函数。从而确保执行正确的析构版本,并避免生成移动构造和移动赋值函数。

  2. 建议所有覆盖都显示地注明 override。这样编译器才能帮到你。

  3. 一个派生类的函数如果覆盖了继承而来的虚函数,那么形参类型、返回值类型必须与被覆盖函数完全一致

  4. 子类覆盖的函数,哪怕没有标注 virtual,其实质上也是 virtual 的。

  5. 如果你希望函数不要再被子类覆盖,那么可以使用 final 关键字。

    1class Child : public Base {
    2 public:
    3  //                           v-- here
    4  virtual void virt() override final{ printf("child\n"); } 
    5};
    
  6. 如果虚函数有默认实参,那么继承时也应当使用相同的默认值。

  7. 如果你偏要使用基类的方法,那么可以通过作用域运算符 :: 做到。

    1b->Base::virt();
    

访问控制

privatepublic 就不说了。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()

结论:

简单含虚函数对象产生的虚函数表,内容依次为:

  1. offset_to_top 距离对象起始地址的偏移量。

    当含有多个虚函数表时,这个字段就有意义了

  2. RTTItype_info 对象地址。

  3. 各个虚函数的指针。

非虚函数不会出现在这个表中。纯虚函数会出现在这个表中。

由于不存在虚继承,所以 vtable 里不会有 vbase_offsetvcall_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()

发现有两处变化:

  1. Part I 中的 parent_virtual1 函数没了。

  2. 第二个 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 的虚表又由三个部分组成。

  1. 自己的,以及 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()
    
  2. 父类 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]
    
  3. 父类 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 虚表的第一部分。

  1. 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]
    
  2. 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_offsetvcall_offset

  • vbase_offset 用于找虚基类对象。(虚基类对象并不是一个真实的独立对象,只是在此对象内 this 指针偏移后形成的一个对象,从而可以调用虚基类对象上的方法)

  • vcall_offset 表示继承的对应位置的虚函数如果想要被调用,this 指针应该偏移的位置。

  • 每个虚表可以从顶部开始、往后由多个部分组成,各部分之间可以通过 offset_to_top 划分。offset_to_top 自身也代表了距离顶部的距离。

而在非多重继承中,共享虚表才是可能的。

我们主要关注 ABChild(钻石底)的虚表。

  • ABChild 虽然总共有三个父类,按理说虚表应该有四部分。但第一个父类(Primary Base) A 和 ABChild 自身一起划入了第一个部分虚表。剩下 B 和 ABParent 各占一部分虚表。

  • 每个部分虚表由一系列 slot 构成,从上到下依次是:

    1. vcall_offset(如果有)

    2. vbase_offset(如果有)

    3. offset_to_top

    4. RTTI(存放了类型信息 type_info 静态对象)

    5. 成员函数

  • 除此之外,编译器还会生成一些 Thunk 代码,用于临时调整指针的位置。

还有一些小细节,比如:Base* p = child; 实际上暗含一个指针调整,导致 p 指向的是虚基类对象,child 指向的是真·对象。因此,对于向下转换(基类指针转派生类)我们不能用 static_cast 或者 C 风格括号转换表达式进行转换,而应该用 dynamic_cast。转换之后用 if 判断转换是否成功。

而在内存布局中,ABChild 中依次排列 A、B、ABParent 三个虚对象,每个虚对象拥有自己的虚表指针 vptr 和数据成员。

参考文献

  1. Itanium C++ ABI

  2. C++ Primer 中文第 5 版,第 15 章