关键词:C++


在使用 C++ 类的继承时,经常会使用到 virtual 关键字,无论是声明 虚函数 还是 虚继承

基础概念

virtual 说明符指定非静态成员函数为虚函数并支持动态调用派发。

虚函数

虚函数是可在派生类中覆盖其行为的成员函数,解决函数重名(重写)的调用问题。

  • 与非虚函数相反,即使没有关于该类实际类型的编译时信息,仍然保留被覆盖的行为。
  • 当使用到基类的指针或引用来处理派生类时,对被覆盖的虚函数的调用,将会调用定义于派生类中(重写)的函数版本。
  • 当使用有限定名字查找(即函数名出现在作用域解析运算符 :: 的右侧)时,使用的是限定查找的函数版本。

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <iostream>

class Base {
public:
virtual void f() {
std::cout << "base\n";
}
};

class Derived : public Base {
public:
void f() override {
std::cout << "derived\n";
}
};

int main() {
Base b;
Derived d;

// 通过引用调用虚函数
Base &br = b;
Base &dr = d;
br.f(); // base
dr.f(); // derived

// 通过指针调用虚函数
Base *bp = &b;
Base *dp = &d;
bp->f(); // base
dp->f(); // derived

// 非虚函数调用
br.Base::f(); // base
dr.Base::f(); // base
return 0;
}

注意要把父类的析构函数声明为虚函数,这样子类析构时会正确调用子类的析构函数,避免了内存泄漏的风险。

虚函数指针和虚函数表

虚函数指针(vfptr)指向一个虚函数表(vftable),虚函数表记录了虚函数的地址。

虚表是属于类的,一个类只需要一个虚表即可,同一个类的所有对象都使用同一个虚表。

虚继承

虚继承解决的是 C++ 多重继承带来的问题。

  • 从不同途径继承来的同一基类,会在子类中存在多份拷贝,既浪费存储空间,也存在二义性。

虚继承底层实现与编译器相关,一般通过虚基指针和虚基表实现。

  • 每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间)和虚基类表(不占用类对象的存储空间)。

虚基指针和虚基表

虚基指针(vbptr)指向一个虚基表(vbtable),虚基表记录了虚基类与本类的偏移地址,通过偏移地址找到虚基类成员,而不用持有拷贝浪费空间。

与虚函数指针和虚函数表相比:

  • 虚基类依旧存在继承类中,占用存储空间;虚函数不占用存储空间。
  • 虚基类表存储的是虚基类相对直接继承类的偏移;而虚函数表存储的是虚函数地址。

从内存布局看

无继承

单纯一个类时:

1
2
3
4
5
6
7
8
class A {
public:
void f() {
std::cout << "A\n";
}
private:
int m_a;
};

VS 所输出内存布局为:

1
2
3
4
class A	size(4):
+---
0 | m_a
+---

内存大小为一个 int 变量的大小。

单继承

1.非虚继承无虚函数

情况如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
class A {
public:
virtual void f() {
std::cout << "A\n";
}
};

class B : public A {
public:
void f() override {
std::cout << "B\n";
}
};

基类 A 的内存布局:

1
2
3
4
class A	size(4):
+---
0 | m_a
+---
  • 在数据区域,不存在 {vfptr} 虚函数指针,只存在 m_a 变量(4 字节)。所以占内存 4 字节。

派生类 B 的内存布局:

1
2
3
4
5
6
7
class B	size(8):
+---
0 | +--- (base class A)
0 | | m_a 基类数据成员
| +---
4 | m_b 子类数据成员
+---
  • 在数据区域,不存在 {vfptr} 虚函数指针,只存在 m_b 变量(4 字节)和继承得到的 m_a 变量,所以占内存 8 字节。

结果:

  • 由于不存在虚函数表,故并不会调用定义于派生类中(重写)的函数版本。

2.非虚继承有虚函数

情况如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class A {
public:
virtual void f() {
std::cout << "A\n";
}
private:
int m_a;
};

class B : public A {
public:
void f() override {
std::cout << "B\n";
}
private:
int m_b;
};

基类 A 的内存布局:

1
2
3
4
5
6
7
8
9
10
class A	size(16):
+---
0 | {vfptr}
8 | m_a
| <alignment member> (size=4)
+---
A::$vftable@:
| &A_meta
| 0
0 | &A::f
  • 在数据区域,存在 {vfptr} 虚函数指针(8 字节,64位系统)、 m_a 变量(4 字节)。同时进行内存对齐(+4 字节),所以占内存 16 字节。
  • 在虚函数表中,存在虚函数 f

子类 B 的内存布局:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class B	size(24):
+---
0 | +--- (base class A)
0 | | {vfptr} 虚函数指针
8 | | m_a 基类数据成员
| | <alignment member> (size=4)
| +---
16 | m_b 子类数据成员
| <alignment member> (size=4)
+---
B::$vftable@: 虚函数表
| &B_meta
| 0
0 | &B::f
  • 在数据区域,存在 {vfptr} 虚函数指针(8 字节,64位系统)、 m_a 变量(4 字节),同样进行内存对齐(+4 字节),再加上子类自身的成员变量 m_b (4 字节),再加以内存对齐(+4 字节),所以占内存 24 字节。
  • 在虚函数表中,存在虚函数 f,指明了函数版本。

结果:

  • 虚函数表指明了函数版本,调用定义于派生类中(重写)的函数版本。

3.虚继承无虚函数

情况如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class A {
public:
void f() {
std::cout << "A\n";
}
private:
int m_a;
};

class B : virtual public A {
public:
void f() {
std::cout << "B\n";
}
private:
int m_b;
};

父类 A 的内存布局如下:

1
2
3
4
class A	size(4):
+---
0 | m_a
+---
  • 只有数据区域的变量。

子类 B 的内存布局如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class B	size(24):
+---
0 | {vbptr} 虚基指针
8 | m_b 子类数据成员
| <alignment member> (size=4)
| <alignment member> (size=4)
+---
+--- (virtual base A)
16 | m_a 基类数据成员
+---
B::$vbtable@:
0 | 0
1 | 16 (Bd(B+0)A) 表示类 B 的虚基类 A 位于偏移 16 + 0 = 16 处
vbi: class offset o.vbptr o.vbte fVtorDisp
A 16 0 4 0
  • 在数据区域,存在虚基指针 {vbptr},且虚基类位于子类存储空间的末尾。
  • 存在虚基表 {vbtable}

结果:

  • 由于不存在虚函数表,不会调用定义于派生类中(重写)的函数版本。

4.虚继承有虚函数

情况如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class A {
public:
virtual void f() {
std::cout << "A\n";
}
private:
int m_a;
};

class B : virtual public A {
public:
void f() override {
std::cout << "B\n";
}
private:
int m_b;
};

父类 A 的内存布局如下:

1
2
3
4
5
6
7
8
9
10
class A	size(16):
+---
0 | {vfptr}
8 | m_a
| <alignment member> (size=4)
+---
A::$vftable@:
| &A_meta
| 0
0 | &A::f
  • 在数据区域,存在 {vfptr} 虚函数指针(8 字节,64位系统)、 m_a 变量(4 字节)。同时进行内存对齐(+4 字节),所以占内存 16 字节。
  • 在虚函数表中,存在虚函数 f

子类 B 的内存布局如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class B	size(32):
+---
0 | {vbptr} 虚基指针
8 | m_b 子类数据成员
| <alignment member> (size=4)
+---
+--- (virtual base A)
16 | {vfptr} 虚基类虚函数指针
24 | m_a 虚基类数据成员
| <alignment member> (size=4)
+---
B::$vbtable@:
0 | 0
1 | 16 (Bd(B+0)A) 表示类 B 的虚基类 A 位于偏移 16 + 0 = 16 处
B::$vftable@:
| -16
0 | &B::f
B::f this adjustor: 16
vbi: class offset o.vbptr o.vbte fVtorDisp
A 16 0 4 0
  • 在数据区域,存在 {vbptr} 虚基指针 和 {vfptr} 虚函数指针。
  • 在虚函数表中,存在虚函数 f,指明了函数版本。
  • 如果派生类没有独立的虚函数,此时派生类对象不会产生虚函数指针。

若派生类中有独立的虚函数,会产生虚函数指针:

如下情形:

1
2
3
4
5
6
7
8
9
10
11
12
class B : virtual public A {
public:
void f() override {
std::cout << "B\n";
}

virtual void f1() { // 派生类中独立的虚函数
std::cout << "tmp\n";
}
private:
int m_b;
};

导致内存布局为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class B	size(40):
+---
0 | {vfptr} 虚函数指针
8 | {vbptr} 虚基指针
16 | m_b 子类数据成员
| <alignment member> (size=4)
+---
+--- (virtual base A)
24 | {vfptr} 虚基类虚函数指针
32 | m_a 虚基类数据成员
| <alignment member> (size=4)
+---
B::$vftable@B@: 类 B 的虚函数表
| &B_meta
| 0
0 | &B::f1
B::$vbtable@:
0 | -8
1 | 16 (Bd(B+8)A) 表示类 B 的虚基类 A 位于偏移 16 + 8 = 24 处
B::$vftable@A@: 类 A 的虚函数表
| -24
0 | &B::f
B::f this adjustor: 24
B::f1 this adjustor: 0
vbi: class offset o.vbptr o.vbte fVtorDisp
A 24 8 4 0
  • 如果派生类拥有自己的虚函数,此时派生类对象就会产生自己本身的虚函数指针 {vfptr},并且该虚函数指针位于派生类对象存储空间的最开始位置。
  • 虚函数指针 {vfptr} 放在了虚基指针 {vbptr} 的前面,为了加快虚函数的查找速度。

结果:

  • 虚函数表指明了函数版本,调用定义于派生类中(重写)的函数版本。

多继承

1.简单多继承

情况如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class A {
public:
virtual void f1() {
std::cout << "A\n";
}

virtual void f2() {
std::cout << "A\n";
}
private:
int m_a;
};

class B {
public:
virtual void f1() {
std::cout << "B\n";
}

virtual void f2() {
std::cout << "B\n";
}
private:
int m_b;
};

class C : virtual public A, virtual public B {
public:

virtual void myVirtual() {}

virtual void f1() override {
std::cout << "C\n";
}

virtual void f2() override {
std::cout << "C\n";
}
private:
int m_c;
};

此时基类 A 和 B 的内存布局为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class A	size(16):
+---
0 | {vfptr}
8 | m_a
| <alignment member> (size=4)
+---
A::$vftable@:
| &A_meta
| 0
0 | &A::f1
1 | &A::f2

class B size(16):
+---
0 | {vfptr}
8 | m_b
| <alignment member> (size=4)
+---
B::$vftable@:
| &B_meta
| 0
0 | &B::f1
1 | &B::f2

子类 C 的内存布局为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class C	size(56):
+---
0 | {vfptr} 虚函数指针
8 | {vbptr} 虚基指针
16 | m_c
| <alignment member> (size=4)
+---
+--- (virtual base A)
24 | {vfptr} 虚基类虚函数指针
32 | m_a 虚基类数据成员
| <alignment member> (size=4)
+---
+--- (virtual base B)
40 | {vfptr} 虚基类虚函数指针
48 | m_b 虚基类数据成员
| <alignment member> (size=4)
+---
C::$vftable@C@: 类 C 的虚函数表
| &C_meta
| 0
0 | &C::myVirtual
C::$vbtable@:
0 | -8
1 | 16 (Cd(C+8)A) 表示类 C 的虚基类 A 位于偏移 16 + 8 = 24 处
2 | 32 (Cd(C+8)B) 表示类 C 的虚基类 A 位于偏移 32 + 8 = 40 处
C::$vftable@A@: 类 A 的虚函数表
| -24
0 | &C::f1
1 | &C::f2
C::$vftable@B@:
| -40
0 | &thunk: this-=16; goto C::f1
1 | &thunk: this-=16; goto C::f2
C::myVirtual this adjustor: 0
C::f1 this adjustor: 24
C::f2 this adjustor: 24
vbi: class offset o.vbptr o.vbte fVtorDisp
A 24 8 4 0
B 40 8 8 0
  • 数据区域照常继承。
  • 虚函数表区域中存在三个虚函数表, vftable@C@vftable@A@vftable@B@;存在一个虚基表 $vbtable@
  • 派生类会覆盖基类的虚函数,只有第一个虚函数表(此处为 vftable@A@ )中存放的是真实的被覆盖的函数的地址;其它的虚函数表中(如 vftable@B@ )存放的并不是真实的对应的虚函数的地址,而只是一条跳转指令。

2.棱形继承(钻石继承)

情况如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class A {
public:
virtual void f() {
std::cout << "A\n";
}

private:
int m_a;
};

class B : virtual public A {
public:
virtual void f() override {
std::cout << "B\n";
}
private:
int m_b;
};

class C : virtual public A {
public:

virtual void f() override {
std::cout << "C\n";
}

private:
int m_c;
};

class D : public B, public C {
public:

virtual void myVirtual() {}

virtual void f() override {
std::cout << "D\n";
}

private:
int m_d;
};

此时 A 类的内存布局如下:

1
2
3
4
5
6
7
8
9
10
class A	size(16):
+---
0 | {vfptr}
8 | m_a
| <alignment member> (size=4)
+---
A::$vftable@:
| &A_meta
| 0
0 | &A::f

B 类和 C 类内存布局如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class B	size(32):
+---
0 | {vbptr}
8 | m_b
| <alignment member> (size=4)
+---
+--- (virtual base A)
16 | {vfptr}
24 | m_a
| <alignment member> (size=4)
+---
B::$vbtable@:
0 | 0
1 | 16 (Bd(B+0)A)
B::$vftable@:
| -16
0 | &B::f
B::f this adjustor: 16
vbi: class offset o.vbptr o.vbte fVtorDisp
A 16 0 4 0

class C size(32):
+---
0 | {vbptr}
8 | m_c
| <alignment member> (size=4)
+---
+--- (virtual base A)
16 | {vfptr}
24 | m_a
| <alignment member> (size=4)
+---
C::$vbtable@:
0 | 0
1 | 16 (Cd(C+0)A)
C::$vftable@:
| -16
0 | &C::f
C::f this adjustor: 16
vbi: class offset o.vbptr o.vbte fVtorDisp
A 16 0 4 0

子类 D 的内存布局如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class D	size(64):
+---
0 | {vfptr} 虚函数指针
8 | +--- (base class B)
8 | | {vbptr} 基类虚基指针
16 | | m_b 基类数据成员
| | <alignment member> (size=4)
| +---
24 | +--- (base class C)
24 | | {vbptr} 基类虚基指针
32 | | m_c 基类数据成员
| | <alignment member> (size=4)
| +---
40 | m_d 子类数据成员
| <alignment member> (size=4)
+---
+--- (virtual base A)
48 | {vfptr} 虚基类虚函数指针
56 | m_a 虚基类数据成员
| <alignment member> (size=4)
+---
D::$vftable@D@: 类 D 的虚函数表
| &D_meta
| 0
0 | &D::myVirtual
D::$vbtable@B@:
0 | 0
1 | 40 (Dd(B+0)A) 表示类 B 的虚基类 A 位于偏移 40 + 0 = 24 处
D::$vbtable@C@:
0 | 0
1 | 24 (Dd(C+0)A) 表示类 C 的虚基类 A 位于偏移 24 + 0 = 24 处
D::$vftable@B@: 类 B 的虚函数表(因为类 B 和类 C 都有一样的函数,虚继承时只保留类 B 的虚函数表)
| -48
0 | &D::f
D::myVirtual this adjustor: 0
D::f this adjustor: 48
vbi: class offset o.vbptr o.vbte fVtorDisp
A 48 8 4 0

再来一个棱形继承:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class A {
public:
virtual void f() {
std::cout << "A\n";
}
private:
int val;
};

class B : virtual public A {
public:
virtual void f() override {
std::cout << "B\n";
}
virtual void g() {}
private:
int m_b;
};

class C : virtual public A {
public:
virtual void f() override {
std::cout << "C\n";
}
virtual void h() {}
private:
int m_c;
};

class D : public B, public C {
public:
virtual void myVirtual() {}
virtual void f() override {}
void g() override {}
void h() override {}
private:
int m_d;
};

针对类 D 的内存布局如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class D	size(72):
+---
0 | +--- (base class B)
0 | | {vfptr} 基类虚函数指针
8 | | {vbptr} 基类虚基指针
16 | | m_b 基类数据成员
| | <alignment member> (size=4)
| +---
24 | +--- (base class C)
24 | | {vfptr} 基类虚函数指针
32 | | {vbptr} 基类虚基指针
40 | | m_c 基类数据成员
| | <alignment member> (size=4)
| +---
48 | m_d 子类数据成员
| <alignment member> (size=4)
+---
+--- (virtual base A)
56 | {vfptr} 虚基类虚函数指针
64 | val 虚基类数据成员
| <alignment member> (size=4)
+---
D::$vftable@B@: 类 B 的虚函数表
| &D_meta
| 0
0 | &D::g
1 | &D::myVirtual
D::$vftable@C@: 类 C 的虚函数表
| -24
0 | &D::h
D::$vbtable@B@:
0 | -8
1 | 48 (Dd(B+8)A) 表示类 B 的虚基类 A 位于偏移 48 + 8 = 56 处
D::$vbtable@C@:
0 | -8
1 | 24 (Dd(C+8)A) 表示类 C 的虚基类 A 位于偏移 24 + 8 = 32 处
D::$vftable@A@: 类 A 的虚函数表
| -56
0 | &D::f
D::myVirtual this adjustor: 0
D::f this adjustor: 56
D::g this adjustor: 0
D::h this adjustor: 24
vbi: class offset o.vbptr o.vbte fVtorDisp
A 56 8 4 0

如果不使用虚继承,出现的问题是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class D	size(56):
+---
0 | +--- (base class B)
0 | | +--- (base class A)
0 | | | {vfptr}
8 | | | val
| | | <alignment member> (size=4)
| | +---
16 | | m_b
| | <alignment member> (size=4)
| +---
24 | +--- (base class C)
24 | | +--- (base class A)
24 | | | {vfptr}
32 | | | val 存在两次 val
| | | <alignment member> (size=4)
| | +---
40 | | m_c
| | <alignment member> (size=4)
| +---
48 | m_d
| <alignment member> (size=4)
+---