虚基类

本文最后更新于:2022年10月28日 下午

问题引出

问题:A中数据,在D中保存了两份。

虚继承

虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类(Virtual Base Class)

虚派生只影响从指定了虚基类的派生类中进一步派生出来的类,它不会影响派生类本身。(本例中,虚继承只影响D,对B和C无影响)

C++实例

C++标准库中的 iostream 类就是一个虚继承的实际应用案例。iostream 从 istream 和 ostream 直接继承而来,而 istream 和 ostream 又都继承自一个共同的名为 base_ios 的类,是典型的菱形继承。此时 istream 和 ostream 必须采用虚继承,否则将导致 iostream 类中保留两份 base_ios 类的成员。

二义性

图2中的菱形继承为例,假设 A 定义了一个名为 x 的成员变量,当我们在 D 中直接访问 x 时,会有三种可能性:

  • 如果 B 和 C 中都没有 x 的定义,那么 x 将被解析为 A 的成员,此时不存在二义性。
  • 如果 B 或 C 其中的一个类定义了 x,也不会有二义性,派生类的 x 比虚基类的 x 优先级更高。
  • 如果 B 和 C 中都定义了 x,那么直接访问 x 将产生二义性问题。

内存

对于每一个多态类型,其所有的虚函数的地址都以一个表格的方式存放在一起,每个函数的偏移量在基类型和导出类型中均相同,这使得虚函数相对于表格首地址的偏移量在可以在编译时确定。虚函数表格的首地址储存在每一个对象之中,称为虚(表)指针(vptr)或者虚函数指针(vfptr),这个虚指针始终位于对象的起始地址。使用多态类型的引用或指针调用虚函数时,首先通过虚指针和偏移量计算出虚函数的地址,然后进行调用。

单继承

例子:

1
2
3
4
5
6
7
8
9
10
11
12
struct A
{
int ax; // 成员变量
virtual void f0() {}
virtual void f1() {}
};

struct B : public A
{
int bx; // 成员变量
void f0() override {}; // 重写f0
};

内存模型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct A
object A VTable (不完整)
0 - vptr_A --------------------------------> +--------------+
8 - int ax | A::f0() |
sizeof(A): 16 align: 8 +--------------+
| A::f1() |
+--------------+

struct B
object
0 - struct A B VTable (不完整)
0 - vptr_A ------------------------------> +--------------+
8 - int ax | B::f0() |
12 - int bx +--------------+
sizeof(A): 16 align: 8 | A::f1() |
+--------------+

对于多态类型,除了要在运行时确定虚函数地址外,还需要提供运行时类型信息(Run-Time Type Identification, RTTI)的支持。一个显然的解决方案是,将类型信息的地址加入到虚表之中。为了避免虚函数表长度对其位置的影响,g++将它放在虚函数表的前。

在单链继承中,每一个派生类型都包含了其基类型的数据以及虚函数,这些虚函数可以按照继承顺序,依次排列在同一张虚表之中,因此只需要一个虚指针即可。并且由于每一个派生类都包含它的直接基类,且没有第二个直接基类,因此其数据在内存中也是线性排布的,这意味着实际类型与它所有的基类型都有着相同的起始地址。

多继承

与单链继承不同,由于两个父类完全独立,它们的虚函数没有顺序关系,即父类的第一个函数有着相同的偏移量,不可以顺序排布。 并且父类中的成员变量也是无关的,因此基类间也不具有包含关系。这使得两个父类必须要处于两个不相交的区域中,同时需要有两个虚指针分别对它们虚函数进行索引。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
                                                C Vtable (7 entities)
+--------------------+
struct C | offset_to_top (0) |
object +--------------------+
0 - struct A (primary base) | RTTI for C |
0 - vptr_A -----------------------------> +--------------------+
8 - int ax | C::f0() |
16 - struct B +--------------------+
16 - vptr_B ----------------------+ | C::f1() |
24 - int bx | +--------------------+
28 - int cx | | offset_to_top (-16)|
sizeof(C): 32 align: 8 | +--------------------+
| | RTTI for C |
+------> +--------------------+
| Thunk C::f1() |
+--------------------+

虚继承

上述的模型中,对于派生类对象,它的基类相对于它的偏移量总是确定的,因此动态向下转换并不需要依赖额外的运行时信息。

而虚继承破坏了这一条件。它表示虚基类相对于派生类的偏移量可以依实际类型不同而不同,且仅有一份拷贝,这使得虚基类的偏移量在运行时才可以确定。因此,我们需要对继承了虚基类的类型的虚表进行扩充,使其包含关于虚基类偏移量的信息。

还是ABCD的菱形继承:

对于形式类型为B的引用,在编译时,无法确定它的基类A它在内存中的偏移量。 因此,需要在虚表中额外再提供一个实体,表明运行时它的基类所在的位置,这个实体称为vbase_offset,位于offset_to_top上方。

Ref

该图片由Larisa KoshkinaPixabay上发布

https://zhuanlan.zhihu.com/p/41309205


虚基类
https://blogoasis.github.io/post/76401623.html
作者
phInTJ
发布于
2022年10月28日
许可协议