一、多重继承
首先介绍一下什么是多重继承?
- 如果在定义一个派生类时,该派生类继承了2个或2个以上基类的特征,那么这种继承关系就称为多重继承。
以下面两张图片为例。
图一
图二
他们的基本属性:
Person:
- name, age
Student:
- id
Undergraduate:
- grade
Teacher:
- title
StudentOnJob:
- research_dir
对于图一,我们可以很简单的就实现之间的继承关系。并且可以利用友元函数Print输出Undergraduate的姓名和年龄。
class Person
{
public:
Person() = default;
Person(string _name, int _age) :name(_name), age(_age) {}
protected:
string name;
int age;
};
class Student :public Person
{
public:
Student() = default;
Student(string _name, int _age, int _id) :Person(_name, _age), id(_id) {}
protected:
int id;
};
class Undergraduate :public Student
{
friend void Print(const Undergraduate& obj)
{
cout << obj.name << " " << obj.age << endl;
}
public:
Undergraduate() = default;
Undergraduate(string _name, int _age, int _id, int _grade) :Student(_name, _age, _id), grade(_grade) {}
private:
int grade;
};
而对于图二,对于这种结构的继承,如果采用图一的方式来进行的话则会遇到一些问题。
class Person
{
public:
Person() = default;
Person(string _name, int _age) :name(_name), age(_age) {}
protected:
string name;
int age;
};
class Student :public Person
{
public:
Student() = default;
Student(string _name, int _age, int _id) :Person(_name, _age), id(_id) {}
protected:
int id;
};
class Teacher :public Person
{
public:
Teacher() = default;
Teacher(string _name, int _age, string _title) :Person(_name, _age), title(_title) {}
protected:
string title;
};
class StudentOnJob :public Student, public Teacher
{
//这个友元函数obj.name与onj.age会报错。
friend void Print(const StudentOnJob& obj)
{
cout << obj.name << " " << obj.age << endl;
}
public:
StudentOnJob() = default;
StudentOnJob(string _name, int _age, int _id, string _title, string _research) :Student(_name, _age, _id), Teacher(_name, _age, _title), research_dir(_research) {}
private:
string research_dir;
};
那么这个友元函数为什么会出现问题呢?
- 在StudentOnJob中,他会有两个name和age,一个是从Student继承的,一个是从Teacher继承的,于是这两个成员名产生了二义性。在友元函数使用它们的时候,编译器无法判断该类对象调用该名字应调用哪个基类中的版本。
二、对图二解决名字冲突的方法
-
1)使用作用域运算符::,标明该成员的作用域
在友元函数Print访问名字的时候,写成obj.Student::name,obj.Student::age。 -
2)使用虚基类解决二义性的问题
虚基类的作用—如果某个基类被声明为虚基类时,那么在被重复继承时,在派生类对象实例中只存储一个副本
class Person
{
public:
Person() = default;
Person(string _name, int _age) :name(_name), age(_age) {}
protected:
string name;
int age;
};
class Student :virtual public Person
{
public:
Student() = default;
Student(string _name, int _age, int _id) :Person(_name, _age), id(_id) {}
protected:
int id;
};
class Teacher :virtual public Person
{
public:
Teacher() = default;
Teacher(string _name, int _age, string _title) :Person(_name, _age), title(_title) {}
protected:
string title;
};
class StudentOnJob :public Student, public Teacher
{
friend void Print(const StudentOnJob& obj)
{
cout << obj.name << " " << obj.age << endl;
}
public:
StudentOnJob() = default;
StudentOnJob(string _name, int _age, int _id, string _title, string _research) :Person(_name, _age), Student(_name, _age, _id), Teacher(name, _age, _title), research_dir(_research) {}
private:
string research_dir;
};
这样通过虚基类就可以解决二义性的问题了。
三、带有虚基类的派生类的构造函数
- 先执行虚基类的构造函数,再执行不是虚基类的基类的构造函数,最后执行构造函数中新加入部分.
- 若有多个虚基类时,依派生类定义时,虚基类出现次序从左至右地执行。
- 当有多个非虚基类时,也依派生类定义时,基类出现次序,从左至右地执行。
四、虚继承实现原理
这里先粗略的介绍一下虚继承实现原理,通过虚基类指针(vbptr)与虚基类表实现,每个虚继承的子类都有一个虚基类指针和虚基类表,当虚基类的子类被当作父类继承时,虚基类指针也会被继承。
vbptr指向了一个虚基类表,在虚基类表中,记录了虚基类与本类的偏移地址,通过偏移地址找到了虚基类的成员。
通过下图可以看到图二使用虚继承的方法后,StudentOnJob的内存分布布局:
通过上图我们可以看到,StudentOnJob的内存布局。
- vbptr:继承父类Student的指针
- id:继承父类Student的成员变量
- vbptr:继承父类Teacher的指针
- title:继承父类Teacher的成员变量
- name:继承父类Person的成员变量
- age:自己的成员变量
所以我们可以发现公共基类的拷贝在多重派生子类中只保存了一份,通过vbptr实现。
vbptr实现可以参考下图:
vbptr指向了一个虚函数表,虚函数表中第二项记录了本类的vbptr与虚基类的公有成员之间的偏移量。
通过计算,对于Student,name:60 - 60 - 0 = 0,age:84 - 60 - 0 = 24
对于Teacher,name:60 - 8 - 52 = 0,age:84 - 8 - 52 = 24
所以name和age是同一个,只进行了一次拷贝。
也可以用下面方法来看:
通过这张图片,我们可以看出在Student中的虚函数表记录的偏移量是8,name的起始位置8 - 8 = 0,age的起始位置32 - 8 = 24。
通过这张图片,我们可以看出在Teacher中虚函数表记录的偏移量是28,name的起始位置 28 - 28 = 0, age的起始位置52 - 28 = 24。这与Student中的name和age起始位置一样,所以指向的是同一个name和age,只会拷贝一次。
因此使用虚继承不会进行重复拷贝,避免了二义性的问题。