C++ 虚继承



一、多重继承

首先介绍一下什么是多重继承?

  • 如果在定义一个派生类时,该派生类继承了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,只会拷贝一次。
因此使用虚继承不会进行重复拷贝,避免了二义性的问题。

  • 11
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值