看下面这个示例代码
class A{
public:
int num=10;
A(){cout<<"A构造"<<endl;}
virtual void fun(){cout<<"A虚函数"<<endl;}
};
class B:public A{
public:
B(){cout<<"B构造"<<endl;}
void fun(){cout<<"B虚函数"<<endl;}
};
class C:public A{
public:
C(){cout<<"C构造"<<endl;}
void fun(){cout<<"C虚函数"<<endl;}
};
class D:public C,public B{
public:
D(){cout<<"D构造"<<endl;}
void fun(){cout<<"D虚函数"<<num<<endl;}
};
这段代码你会发现这段代码编译不通过,因为D类的虚函数在访问num时不知道访问哪个,将这个虚函数里面的内容清空然后执行会得到以下结果:
可以看到D多次继承了同一个基类,这样就造成了构造D时构造了多个A,问题本身没有什么影响但是如果D需要访问A中的东西时就会有两个渠道,通过B或者C,这时候加上作用域的话就可以运行了,但是这样太麻烦了,而且对共同的基类A真的没必要构造两次,为了处理这种情况引入了虚继承的概念。
之前介绍过虚函数,虚继承和它类似
- 发生虚继承的时候就会产生一个虚基表和相应的虚基表指针
- 虚基表是静态的,这点和虚函数表差不多
- 发生虚继承后对任意类只会创建一次
- 虚基表存储虚基表指针到其基类的偏移量
- 一个类只会有一个虚基表,但是可以有多个虚基表指针
这些概念后面梳理,现在先看修改后的代码
class A{
public:
int num=10;
A(){cout<<"A构造"<<endl;}
virtual void fun(){cout<<"A虚函数"<<endl;}
};
class B:virtual public A{
public:
B(){cout<<"B构造"<<endl;}
void fun(){cout<<"B虚函数"<<endl;}
};
class C:virtual public A{
public:
C(){cout<<"C构造"<<endl;}
void fun(){cout<<"C虚函数"<<endl;}
};
class D:virtual public C,virtual public B{
public:
D(){cout<<"D构造"<<endl;}
void fun(){cout<<"D虚函数"<<num<<endl;}
};
看看执行结果
可以看出问题被解决了,接下来看它在内存中的分布
在vs开发者命令行模式下输入下面的指令即可
cl /d1 reportSingleClassLayoutXXX 文件名.cpp
xxx替换为自己的类名,我这里是D
可以看到类的结构还是很清晰的,每个使用虚继承的类头部都有一个虚基表指针,同时虚函数表指针不再出现在头部,不需要过多解释自己看吧
虚继承通过减少基类的重复创建解决了多次继承同一基类造成的二义性问题,借助虚基表完成了基类的定位,可以说很是牛皮,但是缺点也很明显它破坏了类中的原有布局,它使类中的内存分布情况程序多样化,现在虚函数表指针可能有多个甚至位置也会按照继承的顺序而改变,内存的分布依赖于代码书写,所以说要慎用,虽然我们不需要过多关注内存,但是对性能的影响却是真实存在的。