1、为什么要有虚拟基类?
虚拟基类是C++中一个重要的概念,用于解决类多重继承中的菱形继承问题。
在多重继承中,如果一个派生类通过多条路径继承同一个基类,那么该基类在派生类中会有多个副本,称为菱形继承问题。
A
/ \
B C
\ /
D
在上面的图中,类D继承自类B和类C,而类B和类C都继承自类A。在这种情况下,类D中将包含两个类A的副本,会导致资源浪费和不必要的复杂性。
2、代码示例
#include<iostream>
#include<string.h>
using namespace std;
class A{
private:
int n;
public:
A(int x):n(x){
cout << "A()" << endl;
}
~A(){
cout << "~A()" << endl;
}
};
class B: public A{
private:
int n;
public:
B(int x):A(x), n(x){
cout << "B()" << endl;
}
~B(){
cout << "~B()" << endl;
}
};
class C: public A{
private:
int n;
public:
C(int x):A(x), n(x){
cout << "C()" << endl;
}
~C(){
cout << "~C()" << endl;
}
};
class D: public C, public B{
private:
int n;
public:
D(int x):C(x), B(x), n(x){
cout << "D()" << endl;
}
~D(){
cout << "~D()" << endl;
}
};
int main()
{
D *ptr = new D(10);
cout << sizeof(D) << endl;
delete ptr;
return 0;
}
类D继承自类B和类C,而类B和类C都继承自类A。在这种情况下,类D中将包含两个类A的副本,在vscode中调试如下:
输出结果:
A()
C()
A()
B()
D()
sizeof(D):20
~D()
~B()
~A()
~C()
~A()
3、引入虚基类
为了解决这个问题,C++引入了虚拟继承的概念,它允许类在继承关系中只保留一个基类的副本,而不是像普通继承那样继承多个副本。
虚拟继承是通过在继承关系中的某些基类前加上virtual关键字来实现的,这些基类称为虚拟基类。例如,上面的继承关系可以改写为:
类A是虚拟基类,类B和类C都继承自虚拟基类A。在类D中,虚拟基类A只有一个副本,它被类B和类C所共享。这样就避免了菱形继承问题,同时减少了内存开销。
代码修改:
#include<iostream>
#include<string.h>
using namespace std;
class A{
private:
int n;
public:
A(int x):n(x){
cout << "A()" << endl;
}
~A(){
cout << "~A()" << endl;
}
};
class B: virtual public A{
private:
int n;
public:
B(int x):A(x), n(x){
cout << "B()" << endl;
}
~B(){
cout << "~B()" << endl;
}
};
class C: virtual public A{
private:
int n;
public:
C(int x):A(x), n(x){
cout << "C()" << endl;
}
~C(){
cout << "~C()" << endl;
}
};
class D: public C, public B{
private:
int n;
public:
D(int x):A(x + 1), C(x), B(x), n(x){//确保虚拟基类在所有的子对象之前被初始化
cout << "D()" << endl;
}
~D(){
cout << "~D()" << endl;
}
};
int main()
{
D *ptr = new D(10);
cout << sizeof(D) << endl;
delete ptr;
return 0;
}
虚拟基类的初始化顺序需要特别注意。在派生类的构造函数中,虚拟基类的构造函数会在其它基类的构造函数执行之前被调用,以确保虚拟基类在所有的子对象之前被初始化。
运行结果:A()只调用了一次
A()
C()
B()
D()
40
~D()
~B()
~C()
~A()
3、在使用虚拟继承时,需要注意以下几点:
- 虚拟基类只会被构造一次。 虚拟基类的构造函数必须在派生类的构造函数中通过成员初始化列表来调用。
- 虚拟基类的构造函数必须在最先被调用,以确保正确初始化。
- 由于虚拟基类只会被构造一次,因此派生类中的多个构造函数不能对虚拟基类进行不同的初始化。
- 使用虚拟继承可能会影响性能,因为需要额外的指针来维护虚拟继承的关系。
虚拟继承可能会影响性能,不使用虚拟继承:sizeof(D):20,使用虚拟继承:sizeof(D):40
在虚拟继承中,派生类会继承多个基类的虚表指针,这些虚表指针需要存储在对象的内存中,导致对象的内存空间变得更大,同时也会增加对象的构造和析构的时间开销。此外,由于虚表指针需要额外的间接跳转操作,也会影响程序的执行速度。
此外,由于虚拟继承的实现需要维护虚表指针的一致性,也会带来额外的运行时开销。
因此,在设计类继承关系时,需要权衡继承层次结构的复杂性和性能开销之间的平衡,尽量避免过度使用虚拟继承。