一、什么是虚继承
虚继承就是子类中只有一份间接父类的数据。用于解决多继承中的父类为非虚继承时出现的二义性问题,即菱形继承问题。继承方式需要加上virtual关键字。
二、虚继承的特性
以菱形继承为例:
1.不使用虚继承
根据输出的大小和关系图,可以看出student和employee继承的是同一个person,但是在结构中有两个person,造成了二义性。
class Person {
private:
int _pid;
char _sex;
};
class Student :public Person {
private:
int _sid;
};
class Employee :public Person {
private:
int _eid;
};
class EStudent :public Student, public Employee {
private: int _egid;
};
int main() {
cout << sizeof(Person) << endl;
cout << sizeof(Student) << endl;
cout << sizeof(Employee) << endl;
cout << sizeof(EStudent) << endl;
}
2.使用虚继承
使用虚继承之后,就可以看出student和employee继承的是同一个person,并且不会导致二义性
class Person {
private:
int _pid;
char _sex;
};
class Student :virtual public Person {
private:
int _sid;
};
class Employee :virtual public Person {
private:
int _eid;
};
class EStudent :public Student,public Employee {
private: int _egid;
};
三、虚继承原理
在上述实例中,我们发现父类数据并不存放在虚继承的子类中,那么子类怎么找到父类数据呢?
原因是在虚继承的类中,会定义一个虚基表指针vbptr,指向虚基表。而虚基表中会存在偏移量,这个量就是表的地址到父类数据地址的距离。当一个类虚继承自另一个类时,子类对象中会包含一个虚基类指针(vbptr)。这个指针指向一个虚基类表(vbtable)。虚基类表中存储了虚基类子对象相对于子类对象的偏移量等信息。通过虚基类指针找到虚基类表,再根据表中的偏移量信息,就可以在子类对象的内存空间中定位到虚基类(父类)的数据成员。
四、虚函数与虚继承
虚函数和虚继承并不是一个概念:
- 虚函数:是在基类中使用
virtual
关键字声明的成员函数,它允许在派生类中被重新定义(重写),以实现不同的行为。 - 虚继承:是一种继承方式,用于解决多继承中可能出现的菱形继承问题,确保从多个基类继承而来的同一个基类子对象在派生类中只有一份拷贝。
原理:
- 虚函数:通过虚函数表(vtable)和虚函数表指针(vptr)来实现。每个包含虚函数的类都有一个虚函数表,其中存储了该类所有虚函数的地址。对象中包含一个虚函数表指针,指向所属类的虚函数表,以便在运行时能够动态地查找和调用正确的虚函数。
- 虚继承:编译器会为虚继承的基类子对象创建一个隐藏的指针,称为虚基表指针(vbptr),它指向一个虚基表(vbtable)。虚基表中存储了虚基类子对象相对于派生类对象的偏移量等信息,通过这些信息可以在运行时正确地访问虚基类子对象。
我们再来说一下虚函数表的存储方式:
虚函数表在编译时产生,存放在只读数据段(每一个类的虚表不可更改),每一个类对应一个虚表,而每一个类的所有对象共享同一个虚表。
虚函数表的存储方式是一个指针数组,它本质上是一个数组,而每一个数组元素都是一个指针,指向对应的虚函数地址。
当通过对象指针或引用调用虚函数时,程序会先通过对象内的虚表指针找到虚表,再从虚表中获取对应虚函数的指针,进而调用该函数,以此实现运行时多态。
五、构造函数和析构函数是否可以写为虚继承
1.构造函数
构造函数的用途:1)创建对象;2)初始化对象中的属性;3)类型转换;4)在定义对象时构造函数设置虚表指针指向虚函数表
2.析构函数
析构函数是类的一个特殊的成员函数:
- 当一个对象的生命周期结束时,系统会自动调用析构函数注销该对象并进行善后工作,对象自身也可以调用析构函数;
- 析构函数的善后工作是:释放对象在生命周期内获得的资源(如动态分配的内存,内核资源);
- 析构函数也用来执行对象即将被撤销之前的任何操作
根据赋值兼容规则,可以用基类的指针指向派生类对象,如果使用基类型指针指向动态创建的派生类对象,由该基类指针撤销派生类对象,则必须将析构函数定义为虚函数,实现多态性,自动调用派生类析构函数,否则可能存在内存泄漏问题
类中没有虚函数,就不要把析构函数定义为虚。