关键词: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(); dr.f();
Base *bp = &b; Base *dp = &d; bp->f(); dp->f();
br.Base::f(); dr.Base::f(); 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) +---
|