
虚继承也是有机制的,推荐两篇不错的博文:
C++继承详解(三):抽象类和纯虚函数、多重继承与虚基类的底层实现原理详解_C/C++_ZY-JIMMY-优快云博客blog.youkuaiyun.com
我们知道虚函数其实现原理即虚函数指针 vfptr 和虚函数表vftable,那么虚基类的实现其实就是产生虚基类表指针 vbptr 与虚基类表 vbtable。编译器gcc的做法是将虚基类放在对象末尾, 在虚表中添加一项, 记录基类对象在对象中的偏移, 从而获得其地址.

虚继承底层实现原理与编译器相关,一般通过虚基类指针和虚基类表实现,每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(不占用类对象的存储空间)。
需要强调的是,虚基类依旧会在派生类里面存在拷贝,只是仅仅只存在一份而已,并不是不在派生类里面了;当虚继承的派生类被当做基类继承时,虚基类指针也会被继承。
实际上,vbptr指的是虚基类表指针(virtual base table pointer),该指针指向了一个虚基类表(virtual table),虚基类表中记录了虚基类与本类的偏移地址;
通过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类(虚基类)的两份同样的拷贝,节省了存储空间。
需要明确的是虚基类和虚函数有两点必要重要的区别:
- 虚基类实例化出来的成员依旧存在继承类实例化对象中,占用存储空间,只不过位置由虚基类表指出,派生类对象寻找虚基类的成员时需要通过虚基类表指针取得虚基类表然后寻址到虚基类的成员;而虚函数不占用实例化对象的存储空间,它在代码段,不在数据段,它由虚函数表指针指向的虚函数表中的函数指针说明地址。
- 虚基类表存储的是虚基类相对直接继承类的偏移;而虚函数表存储的是虚函数地址。
开局先说下VS2019的小功能,可以查看类在内存中的分布。

命令格式如下: cl –d1reportSingleClassLayout[classname] xxx.cpp
- classname 为类名,-d1reportSingleClassLayout[classname] 之间没有空格。
- xxx.cpp为源代码文件名
例如我图中所示就是在.cpp文件所在的目录下运行cmd命令:
cl –d1reportSingleClassLayoutD CppTest.cpp
#include<iostream>
using namespace std;
class A //大小为4
{
public:
int a;
};
class B :virtual public A //大小为12,变量a,b共8字节,虚基类表指针4
{
public:
int b;
virtual void afun() { cout << 'b' << endl; }
};
class C :virtual public A //与B一样12
{
public:
int c;
virtual void afun() { cout << 'c' << endl; }
};
class D :public B, public C //24,变量a,b,c,d共16,B的虚基类指针4,C的虚基类指针
{
public:
int d;
void afun() override { cout << 'd' << endl; }
};
int main()
{
A a;
B b;
C c;
D d;
cout << sizeof(a) << endl;
cout << sizeof(b) << endl;
cout << sizeof(c) << endl;
cout << sizeof(d) << endl;
system("pause");
return 0;
}

现在我们聊聊菱形虚基类继承情况下的虚函数继承:
类A有个虚函数afun(),然后类B、C分别没有虚继承它,然后类D又多继承了类B、C。
#include

可以看到,基类A的成员也被建立了两份,虚函数表指针看似有两个,实际你去仔细看地址变化,其实是有且仅有一个虚函数表指针。
相比之下,然后类B、C分别虚继承了类A,然后类D又多继承了类B、C。
#include

这时,如果类D覆写了虚函数afun(),那么D的实例化对象头部会有且仅有一个虚函数表指针,它直接由类A的虚函数表而来,只不过虚函数afun()的指针发生了改变,指向了类A定义的虚函数afun()的函数体。且虚基类A的数据只被建立了一份。
除此之外还有个问题需要说明,而当类D中没有覆写虚函数afun(),那么它继承的类B、C同时有虚函数afun()的覆写定义,会引发歧义,也就是二义性错误,不明确继承。
#include

当然类B、C没有被D继承的话,分别继承类A是没有问题的,编译通过不会发生二义性错误。
那么,回到类B、C被类D继承,但是类D、C没有覆写类A的虚函数afun(),仅有类B覆写了虚函数afun(),那么也不会发生二义性错误,编译通过。类D的实例化对象中虚函数表指针,虚函数表中指针指向类B中覆写的虚函数afun()的函数体。

我觉的还需要说明的两个问题:
一、为什么VS2019里指针是4个字节,而我的另一篇文章中的指针是8个字节的大小,主要是因为那个文章实验用的CLion,在CLion中用的MinGW64,所谓编译器都是64位,我觉得是编译器的不同造成这个区别,VS2019的编译器兼容32位机,所以是4个字节。
袁三丰:(多态)虚函数和重载实现机制细致讲解zhuanlan.zhihu.com
二、虚函数表指针是放在对象内存头部位置的。
(虚函数表指针位置是由编译器决定的, gcc将其放在对象头部, 这导致对象不能兼容C语言中的struct, 但是在多重继承中, 通过类成员指针访问虚函数会更容易实现. 如果放在对象末尾则可以保证兼容性, 但是就需要在执行期间获得各个vptr在对象中的偏移, 在多重继承中尤其会增加额外负担。)
那么虚基类表指针呢,从之前的类内存分布图可以看出,指针应该放在虚基类被嵌套出来的位置,占指针大小的内存空间,内容就是到对象尾部实例化类A部分的偏移量。
再说两种情况。情况一,类A被类B、C虚继承,然后再被类D继承,类B、C中有同名虚函数被类D覆写:
#include

情况而,类A被类B、C虚继承,然后再被类D继承,类A、C中有不同名虚函数被类D覆写:
#include

这两种情况时都是有两个虚函数指针,并且有两个虚基类指针。说明类实例化对象中虚函数指针数量只有建立初始化虚函数的类数量相关,和它被覆写了几次或覆写继承再覆写无关。