虚基类
本文最后更新于: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 |
|
内存模型:
1 |
|
对于多态类型,除了要在运行时确定虚函数地址外,还需要提供运行时类型信息(Run-Time Type Identification, RTTI)的支持。一个显然的解决方案是,将类型信息的地址加入到虚表之中。为了避免虚函数表长度对其位置的影响,g++将它放在虚函数表的前。
在单链继承中,每一个派生类型都包含了其基类型的数据以及虚函数,这些虚函数可以按照继承顺序,依次排列在同一张虚表之中,因此只需要一个虚指针即可。并且由于每一个派生类都包含它的直接基类,且没有第二个直接基类,因此其数据在内存中也是线性排布的,这意味着实际类型与它所有的基类型都有着相同的起始地址。
多继承
与单链继承不同,由于两个父类完全独立,它们的虚函数没有顺序关系,即父类的第一个函数有着相同的偏移量,不可以顺序排布。 并且父类中的成员变量也是无关的,因此基类间也不具有包含关系。这使得两个父类必须要处于两个不相交的区域中,同时需要有两个虚指针分别对它们虚函数进行索引。
1 |
|
虚继承
上述的模型中,对于派生类对象,它的基类相对于它的偏移量总是确定的,因此动态向下转换并不需要依赖额外的运行时信息。
而虚继承破坏了这一条件。它表示虚基类相对于派生类的偏移量可以依实际类型不同而不同,且仅有一份拷贝,这使得虚基类的偏移量在运行时才可以确定。因此,我们需要对继承了虚基类的类型的虚表进行扩充,使其包含关于虚基类偏移量的信息。
还是ABCD的菱形继承:
对于形式类型为B
的引用,在编译时,无法确定它的基类A
它在内存中的偏移量。 因此,需要在虚表中额外再提供一个实体,表明运行时它的基类所在的位置,这个实体称为vbase_offset
,位于offset_to_top
上方。
Ref
该图片由Larisa Koshkina在Pixabay上发布