C++虚拟继承一般发生在多重继承的情况下。C++允许一个类有多个父类,这样就形成多重继承。多重继承使得派生类与基类的关系变得更为复杂,其中一个容易出现问题是某个基类沿着不同的路径被派生类继承(即形成所谓“菱形继承”),从而导致一个派生类对象中存在同一个基类对象的多个拷贝。
多重继承带来同一个基类对象在派生类对象中存在多个拷贝的问题,考察如下代码。
#include <iostream>
#include <string>
using namespace std;
//人员类
class Person{
protected:
string IDPerson; //身份证号
string Name; //姓名
public:
Person(string s1, string s2){
IDPerson=s1;
Name=s2;
}
};
//学生类
class Student:public Person{
int No;
public:
Student(string s1,string s2,int n):Person(s1,s2),No(n){}
};
//员工类
class Employee:public Person{
int No;
public:
Employee(string s1,string s2,int n):Person(s1,s2),No(n){}
};
//在职研究生类
class EGStudent:public Employee,public Student{
int No;
public:
EGStudent(string s1,string s2,int n): Employee(s1,s2,n),Student(s1,s2,n),No(n){}
void show(){
cout<<Employee::IDPerson<<","<<Employee::Name<<","<<No<<endl;
}
};
int main(){
EGStudent one("332422199204047275","张三",1111);
one.show();
cout<<"sizeof(string)="<<sizeof(string)<<endl;
cout<<"sizeof(Person)="<<sizeof(Person)<<endl;
cout<<"sizeof(Student)="<<sizeof(Student)<<endl;
cout<<"sizeof(Employee)="<<sizeof(Employee)<<endl;
cout<<"sizeof(EGStudent)="<<sizeof(EGStudent)<<endl;
getchar();
}
程序运行结果:
332422199204047275,张三,1111
sizeof(string)=28
sizeof(Person)=56
sizeof(Student)=60
sizeof(Employee)=60
sizeof(EGStudent)=124
在这个程序中,EGStudent类有两个父类:Employee和Student,而Employee和Student都是Person类的派生类。通过观察这几个类的大小,可以发现如下等式。
sizeof(Student)= sizeof(Person)+sizeof(int)
sizeof(Employee)= sizeof(Person)+sizeof(int)
sizeof(EGStudent)= sizeof(Student)+ sizeof(Employee)+ sizeof(int)
也就是说,在一个EGStudent类对象中包含了两个Person类对象,一个来自Employee类对象,一个来自Student类对象。在EGStudent类的成员函数show()中,直接访问IdPerson或Name都会引发编译错误,因为编译器不知道它们指的是哪个Person对象中的成员。所以,在上面的程序中,在show()中显示的是Employee中的成员(IDPerson和Name)。实际上,在EGStudent类对象中还有来自Student类的成员(IDPerson和Name)。
从逻辑上说,一个在职研究生只可能有一个名字和一个身份证号码,所以在一个EGStudent类对象中有IDPerson和Name字段的两个拷贝是不合理的,只需要一个拷贝就可以了。
虚拟继承就是解决这个问题的,通过把继承关系定义为虚拟继承,在构造EGStudent类对象的时候,EGStudent类的祖先类Person的对象只会被构造一次,这样就可以避免存在多个IDPerson和Name的拷贝问题了。将以上代码修改如下。
#include <iostream>
#include <string>
using namespace std;
//人员类
class Person{
protected:
string IDPerson; //身份证号
string Name; //姓名
public:
Person(string s1, string s2){
IDPerson=s1;
Name=s2;
}
//添加一个默认构造
Person(){}
};
//学生类
class Student:public virtual Person{
int No;
public:
Student(string s1,string s2,int n):Person(s1,s2),No(n){}
};
//员工类
class Employee:public virtual Person{
int No;
public:
Employee(string s1,string s2,int n):Person(s1,s2),No(n){}
};
//在职研究生类
class EGStudent:public virtual Employee,public virtual Student{
int No;
public:
EGStudent(string s1, string s2, int n): Employee(s1,s2,n),Student(s1,s2,n),No(n){}
void show(){
cout<<Employee::IDPerson<<","<<Employee::Name<<","<<No<<endl;
}
};
输出结果:
,,1111
sizeof(string)=28
sizeof(Person)=56
sizeof(Student)=64
sizeof(Employee)=64
sizeof(EGStudent)=80
考察以上程序,注意如下几点。
(1)当在多条继承路径上有一个公共的基类,在这些路径中的某几条汇合处,这个公共的基类就会产生多个实例(或多个副本),若只想保存这个基类的一个实例,可以将这个公共基类说明为虚基类,就像Student和Employee中的申明那样。
(2)被虚拟继承的基类,叫做虚基类。虚基类实际指的是继承的方式,而非一个基类,是动词,而非名词。
(3)为了实现虚拟继承,派生类对象的大小会增加4。所以,在上面的程序中,
sizeof(EGStudent)=sizeof(Employee)+sizeof(Student)-sizeof(Person)+sizoef(int)+4=80。
这个增加的4个字节,是因为当虚拟继承时,无论是单虚继承还是多虚继承,派生类需要有一个虚基类表来记录虚继承关系,所以此时子类需要多一个虚基类表指针,而且只需要一个即可。
(4)虚拟继承中,虚基类对象是由最远派生类的构造函数通过调用虚基类的构造函数进行初始化的,派生类的构造函数的成员初始化列表中必须列出对虚基类构造函数的调用;如果未列出,则表示使用该虚基类的缺省构造函数。上面的程序因Person的默认构造函数啥也没做,因此IDPerson和Name字段是空字符串。
(5)在上面的程序中,如果将类EGStudent的申明改为
class EGStudent:public Employee,public Student
那么除了sizeof(EGStudent)会变成76以外,其他的什么也不会发生。因为虚拟继承只是表名某个基类的对象在派生类对象中只被构造一次,而在本例中类Student和Employee对象在EGStudent对象中本来就不会被构造多次,所以不将它们申明虚基类也是完全可以的。
[1]陈刚.C++高级进阶教程[M].武汉:武汉大学出版社,2008[8.3(P276-P280)]