一、 基础概念
1.1 基础问题
为什么需要虚函数?
主要在于功能解耦合以及规范化的需要。我们很多时候需要做到:一个模块对达成某个功能(机制)有一组可替换的实现(策略)。也就是说,需要策略和机制分离。那么我们的模块就应该只依赖这个机制的接口,而非这个机制的具体实现。则:
相对而言
java
中更加完善,它会告诉妳这是一个接口
- 可以使用纯虚函数要求某个子类必须实现某个方法
- 也可以使用普通函数,允许子类复用这个方法,或者子类自己实现这个方法
更加具体的来说,虚函数还解决了动态分发得问题:
#include <cstdio>
class Base{
public:
void non_virtual(){printf("base\n");}
};
class Child: public Base {
public:
void non_virtual(){printf("child\n");}
};
int main (int argc, char *argv[])
{
Base *b = new Child();
b->non_virtual(); // base
return 0;
}
运行后输出base
,这里如果我们想在业务中使用Child
的功能,那么我们就必须把Child
耦合到业务代码,这里还存在一个问题就是:Base
和child
都生成了非虚的析构函数,直接delete b
可能导致释放不完全。
而虚函数可以很好的解决这个问题:
#include <cstdio>
class Base{
public:
virtual void non_virtual(){printf("base\n");}
};
class Child: public Base {
public:
virtual void non_virtual(){printf("child\n");}
};
int main (int argc, char *argv[])
{
Base *b = new Child();
b->non_virtual();
return 0;
}
输出为child
,这是怎么实现的呢?正是本文的内容。
虚函数注意事项
- 基类必须实现虚析构函数,从而保证执行正确的析构版本。
- 建议所有覆盖都显示地注明
override
。这样编译器可以帮助你进行继承检查。 - 一个派生类的函数如果覆盖了继承而来的虚函数,那么形参类型、返回值类型必须与被覆盖函数完全一致(不然编译器会认为不是同一个函数)
- 子类覆盖的函数,哪怕没有标注
virtual
,其实质上也是virtual
的(可以认为子类不需要对于覆盖的函数,显式添加virtual
)。 - 如果你希望函数不要再被子类覆盖,那么可以使用
final
关键字。
class Child: public Base {
public:
virtual void non_virtual() override final{printf("child\n");}
};
- 如果虚函数有默认实参,那么继承时也应当使用相同的默认值。
- 如果你偏要使用基类的方法,那么可以通过作用域运算符
::
做到。
b->Base::non_virtual();
访问控制
private
和public
就不说了。protect
能够让成员变成:用户访问不到,子类友元访问得到;且子类和友元只能通过派生类对象访问基类成员,不能直接用基类对象访问基类成员。
1.2 最派生类和最派生对象
// 下面代码中mostderived时最派生类
class base {};
class derived : base {};
class base2 {};
class mostderived : derived, base2 {};
mostderived md;
同样,我们把没有父类得base称为最超类。
1.3 虚调用偏移
**虚调用偏移(vcall offset)是一个偏移量。**有一些虚函数,声明于虚基类,重写于派生类。当我们需要调用在派生类中覆写得函数的时候,需要将base_this
转化为derive_this
。vcall_offset
只存在于虚继承的场景。
#include <stdio.h>
#include <stdint.h>
struct ABParent{
int64_t ab;
virtual void parent_virtual1() { printf("front ABParent\n"); }
virtual ~ABParent(){}
};
struct A: virtual ABParent{
int64_t a;
virtual void parent_virtual1() { printf("front\n"); }
};
int main (int argc, char *argv[])
{
auto a = new A();
ABParent *p = a;
printf("p-a=%ld\n", (uint64_t)p - (uint64_t)a);
p->parent_virtual1();
delete p;
return 0;
}
上述程序输出:
p-a=16
front
这对于习惯C
语言的人来说会感到违背直觉。为什么p
和a
都能调用到同一个函数,但是p
和a
却在不同的地址?
实际上,当执行p->parent_virtual1()
; 时,this
指针会进行修正,从而指向真正的对象A
。
在下面的例子中,我们会看到 vcall_offset (-16)
这表示需要将p
指针加上-16
作为真正对象A
的this
指针。
1.4 虚基偏移
虚基偏移(vbase offset
)的作用则相反,是为了从真正对象A
访问到虚基类ABParent
。只存在于虚继承的场景。
如果vbase_offset
存在,则会位于A
的vtable
的第一个slot
. 例如上面的代码生成了这样的vtable
:
Vtable for 'A' (13 entries).
0 | vbase_offset (16)
1 | offset_to_top (0)
2 | A RTTI
-- (A, 0) vtable address --
3 | void A::parent_virtual1()
4 | A::~A() [complete]
5 | A::~A() [deleting]
6 | vcall_offset (-16)
7 | vcall_offset (-16)
8 | offset_to_top (-16)
9 | A RTTI
-- (ABParent, 16) vtable address --
10 | void A::parent_virtual1()
[this adjustment: 0 non-virtual, -24 vcall offset offset]
11 | A::~A() [complete]
[this adjustment: 0 non-virtual, -32 vcall offset offset]
12 | A::~A() [deleting]
[this adjustment: 0 non-virtual, -32 vcall offset offset]
-
offset_to_top(0):表示当前这个虚函数表地址距离对象顶部地址的偏移量,因为对象的头部就是虚函数表的指针,所以偏移量为0。
-
RTTI指针:指向存储运行时类型信息(
type_info
)的地址,用于运行时类型识别,用于typeid
和dynamic_cast
。
对应于这样得layout:
*** Dumping AST Record Layout
0 | struct ABParent
0 | (ABParent vtable pointer)
8 | int64_t ab
| [sizeof=16, dsize=16, align=8,
| nvsize=16, nvalign=8]
*** Dumping AST Record Layout
0 | struct A
0 | (A vtable pointer)
8 | int64_t a
16 | struct ABParent (virtual base)
16 | (ABParent vtable pointer)
24 | int64_t ab
| [sizeof=32, dsize=32, align=8,
| nvsize=16, nvalign=8]
Layout
的offset=16
的位置(第 12 行)恰好对应 vtable
中 vbase_offset=16
。再次证明vbase_offset
就是从子类对象寻找虚基类对象的距离。
1.5 编译选项
本文采用的编译选项是:
// 查看对象布局
clang -Xclang -fdump-record-layouts -stdlib=libc++ -c main.cpp
// 查看虚函数表布局
clang -Xclang -fdump-vtable-layouts -stdlib=libc++ -c main.cpp
gdb打印相关命令:
set print object on
set print vtbl on
set print pretty on
//以上可以定义一个gdb命令的alias,简化命令
p 实例
info vtbl 实例
二、 普通继承内存布局
2.1 单一继承
2.1.1 成员变量布局
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() {}
};
内存布局:
*** 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]
可以看出,普通的单一继承,成员变量是从上到下依次排列的,并且遵循前面提到的字节对齐规则
2.1.2 虚函数表
A
中包含两个虚函数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() {}
};
内存布局如下:
*** 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]
三个类的虚函数表得内存布局如下:
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 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 for 'A' (4 entries).
0 | offset_to_top (0)
1 | A RTTI
-- (A, 0) vtable address --
2 | void A::vfuncA1()
3 | void A::vfuncA2()
可以看出,在单一继承中,子类的虚函数表通过以下步骤构造出来:
- 先拷贝上一层次父类的虚函数表。
- 如果子类有自定义虚函数(例如
B::vfuncB
,C::vfuncC
),那么直接在虚函数表后追加这些虚函数的地址。 - 如果子类覆盖了父类的虚函数,使用新地址(例如
B::vfuncA1
,C::vfuncA1
)覆盖原有地址(即A::vfunc1
)。
2.2 多继承
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() {}
};
2.2.1 内存布局
clang -Xclang -fdump-record-layouts -stdlib=libc++ -c main.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]
- 类对象得大小为
40
,包含了两个vtbl
- 继承有
primary base
父类和普通base
父类之分。
总的来说,在最底层子类的内存布局中,多继承的成员变量,以及 vtable 指针的排列规则是:
- 第一个声明得是
primary base
父类 - 按照继承的声明顺序依次排列,并需要遵循编译器的字节对齐规则。
- 最后排列最底层子类的成员变量。
2.2.2 虚函数表
clang -Xclang -fdump-vtable-layouts -stdlib=libc++ -c main.cpp
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]
9 | void B::vfuncB2()
从上面可以看出,C 的虚函数表是由 2 部分组成的:
- 首先是 「C 继承 A」,按照上述单一继承的虚函数表生成原则,生成了第一个虚函数表。此时
C::vfuncB1()
对于 A 来说是一个自定义的虚函数,因此虚函数表的第一部分有 4 个函数地址。 - 其次是「C 继承 B」,同样按照单一继承的规则生成,但不用追加
C::vfuncC()
,因为C::vfuncC()
已经在第一部分填入。 - C 和 A共享
vptr
, B 拥有独立的vptr
可以发现:
C
的虚函数表存在一个重复项C::vfuncB1()
- 虽然
C
有两个vptr
,但是只有一个虚函数表,两个vptr
指向了虚函数表得不同位置
如何验证呢?
#include <iostream>
#include <stdint.h>
using namespace std;
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() {
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 = %ld\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
}
三、 虚继承内存布局
3.1 单一继承
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() {}
};
int main(int argc, char *argv[]) {
A a;
B b;
return 0;
}
3.1.1 内存布局
A
的内存布局如下:
clang -Xclang -fdump-record-layouts -stdlib=libc++ -c main.cpp
*** Dumping AST Record Layout
0 | class Base
0 | (Base vtable pointer)
8 | char baseval
| [sizeof=16, dsize=9, align=8,
| nvsize=9, nvalign=8]
*** 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]
与普通单一继承不同,虚拟单一继承时存在两个vptr
的,而且被继承的对象排在后面
3.1.2 虚函数表
clang -Xclang -fdump-vtable-layouts -stdlib=libc++ -c main.cpp
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]
10 | void Base::vfuncBase2()
- 表的第一部分
{3,4}
,按照A
是一个单一类的规则直接构造 - 表的第二部分
{9,10}
,按照A
单一继承Base
的规则构造 A
如果要调用Base::vfuncBase2
,编译器会通过this+vbase_offset
将A
转化为base
再进行调用Base
如果要调用A::vfuncBase1()
, 编译器会通过this+vcall_offset
将Base
转化为A
再进行调用
3.2 菱形继承
class Child : public A, public B {
char childval;
virtual void vfuncC() {}
virtual void vfuncB() {}
virtual void vfuncA() {}
};
3.2.1 内存布局
*** 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
40 | class Base (virtual base)
40 | (Base vtable pointer)
48 | char baseval
| [sizeof=56, dsize=49, align=8,
| nvsize=33, nvalign=8]
- 成员变量和虚函数指针与「多继承」的情况相同。
Child
把Base
(被虚拟继承的父类)的内容排在最后(比Child
的自定义成员还要后),并且只保留了一份Base
的数据,这就是虚拟继承的作用。
3.2.2 虚函数表
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]
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]
17 | void B::vfuncBase2()
[this adjustment: 0 non-virtual, -32 vcall offset offset]
验证代码:
#include <stdint.h>
#include <stdio.h>
class Base {
char baseval;
virtual void vfuncBase1() { printf("void Base::vfuncBase1()\n"); }
virtual void vfuncBase2() { printf("void Base::vfuncBase2()\n"); }
};
class A : virtual public Base {
double aval;
virtual void vfuncBase1() { printf("void A::vfuncBase1()\n"); }
virtual void vfuncA() { printf("void A::vfuncA()\n"); }
};
class B : virtual public Base {
double bval;
virtual void vfuncBase2() { printf("void B::vfuncBase2()\n"); }
virtual void vfuncB() { printf("void B::vfuncB()\n"); }
};
class Child : public A, public B {
char childval;
virtual void vfuncC() { printf("void Child::vfuncC()\n"); }
virtual void vfuncB() { printf("void Child::vfuncB()\n"); }
virtual void vfuncA() { printf("void Child::vfuncA()\n"); }
};
int main(int argc, char *argv[]) {
A a;
B b;
Child c;
uint64_t *cvtable = (uint64_t *)*(uint64_t *)(&c);
uint64_t *cvtable1 = (uint64_t *)*((uint64_t *)(&c) + 2);
uint64_t *cvtable2 = (uint64_t *)*((uint64_t *)(&c) + 5);
typedef void (*func_t)(void);
printf("-----vtable0-----\n");
((func_t)(*(cvtable + 0)))(); // void A::vfuncBase1()
((func_t)(*(cvtable + 1)))(); // void Child::vfuncA()
((func_t)(*(cvtable + 2)))(); // void Child::vfuncC()
((func_t)(*(cvtable + 3)))(); // void Child::vfuncB()
printf("-----vtable1-----\n");
((func_t)(*(cvtable1 + 0)))(); // void B::vfuncBase2()
((func_t)(*(cvtable1 + 1)))(); // void Child::vfuncB()
printf("-----vtable2-----\n");
((func_t)(*(cvtable2 + 0)))(); // void A::vfuncBase1()
((func_t)(*(cvtable2 + 1)))(); // void B::vfuncBase2()
return 0;
}