用ppt做网站方法恩施做网站多少钱
文章目录
- 前言
 - 1. 继承的形式
 - 1.1 单继承
 - 1.2 多继承
 
- 2. 菱形继承
 - 2.1 菱形继承的产生
 - 2.2 virtual虚拟继承现象和语法
 - 2.3 虚拟继承的底层
 - 2.3.1 单虚拟继承
 - 2.3.2 虚拟继承 — 虚基表、虚基表指针
 - 2.3.3 菱形虚拟继承的底层分析
 
- 2.4 小结
 
前言
关于菱形继承和虚拟继承这个话题是C++学习者必须经历的一条路。小编的这篇文章将从:
- 继承的类型
 - 菱形继承的问题
 - 虚拟继承是如何解决菱形继承的问题的?
 - ……
 
角度出发,带大家对虚拟继承的底层实现方式有一定的了解!
相信各位读者在阅读之后一定会大有收获!
1. 继承的形式
在之前小编和大家探讨基础语法的是时候列出了许多的示例,但是这些示例都是一些单子单父的情况。小编也在语法的时候提到一句:一个派生类可以继承多个基类。所以这也让我们的继承方式变得多样起来。
1.1 单继承
- 单继承:一个子类只有一个直接父类的继承关系为单继承
 
如下图:

1.2 多继承
- 多继承:一个子类有两个及以上的直接父类的继承关系被称为多继承。
 
如下图:

不管继承方式是单继承还是多继承,我们都应该注意到:我们对待这个子类为一个新的类型,一定要注意好如何设计好这个类的构造、析构等函数,以及各种接口!
2. 菱形继承
2.1 菱形继承的产生
刚刚了解到单继承和多继承的形式,我们可以想到这样一个场景:我现在有一个person类来描述一个学校的人,那么我用student类继承这个person类,再由teacher类继承这个person类。我们学校有助教(assistant类),助教既是一个学生,也是一个老师……我们可以采用assistant继承person和teacher……
大概继承关系如下图:
class person
{};class student : public person
{};class teacher : public person
{};class assistant : public student , public teacher
{};
 

观察这个图像的形状——一个菱形。这就是菱形继承名字的由来,这样的继承体系的结构十分地像一个菱形,所以被命名为菱形继承。
我们现在对这个菱形继承的一些成员和方法进行完善:
例1:
下面我们为这个继承关系中的类,进行一些字段的填写:
#include<iostream>
#include<string>
using namespace std;
class person
{
public://……
protected:string _name = "zhangsan"; //姓名int _ID = 1; //身份证
};class student : public person
{
public:void study() //学习方法{}
protected:int _sid = 2; //学生id
};class teacher : public person
{
public:void teach() //教学方法{}
protected:int _tid = 3; //教师id
};class assistant : public student, public teacher
{
public:void check() //助教的方法{}
protected:int _age = 4; //年龄—这里是故意将年龄在这里充当一个字段的
};int main()
{assistant a;cout << "DEBUG" << endl;return 0;
}
 
来看这个继承体系的继承模型:
 
很显然,我们可以得到如下结论:
-  
person的字段在assistant中有两份。person类被student类和teacher类继承下来,对于student类和teacher类都有一份person类的字段,那么再由assistant继承他们,那么就导致了assistant就有了两份person类的字段!这就导致了一个来自菱形继承的问题:
-  
数据冗余和二义性。
- 数据冗余:两份来自于基类
person的字段。 - 二义性:派生类在访问基类字段的时候,是访问的基类
student还是基类teacher呢? 
 - 数据冗余:两份来自于基类
 
 -  
 
我们从内存角度看看这个存储结构:

完全符合我们的模型和预期!!!
我们从观察内存存储角度出发,发现这个菱形继承的问题所在!
2.2 virtual虚拟继承现象和语法
那么我们应该如何去解决这个问题呢?
我们现在需要解决的问题:
- 菱形继承的二义性。
 - 菱形继承的数据冗余。
 
菱形继承的二义性我们可以通过指明类域的方式来进行分别。
如果是我们设计这样的一个方法来解决这样的问题,我们大概会怎么样去解决呢?
既然这个样的一个
assistant中有两份的person对象,那么我们想办法只保留一份即可。
C++处理二义性和数据冗余的核心也是将这样的数据保留一份即可!
- 关键字:
virtual - 使用:用于在分支子类的地方上。例如:上面的例1的
student和teacher。 - 下图标红的类

 
例2:
#include<iostream>
#include<string>
using namespace std;
class person
{
public://……
protected:string _name = "zhangsan";int _ID = 1;
};class student : virtual public person
{
public:void study(){}
protected:int _sid = 2;
};class teacher : virtual public person //虚拟继承
{
public:void teach(){}
protected:int _tid = 3;
};class assistant : public student, public teacher 
{
public:void check(){}
protected:int _age = 4;
};int main()
{assistant a;cout << "DEBUG" << endl;return 0;
}
 
上面的代码,使用了
virtual关键字。由student和teacher虚拟继承person,这就是“虚拟继承”
这里,我们从内存存储角度来看,菱形继承的问题是否得到了解决呢?
如下图:

- 现象和问题:
 
- 这样的结果完美符合我们想要的预期—最后的
assistant类只保留了一份来自于person的字段。 - 有两个字段:
002df1d0和002dec14是什么呢? person字段被保存到了高地址处。整个对象的最高地址处。这是为什么呢?
接下来,小编和大家一起分析这个虚拟继承的底层。
2.3 虚拟继承的底层
在分析上面问题之前,我们先来谈一个单独继承的模型!
- 注意到上面的
student和teacher字段:他们的首地址的四字节(X86环境下)处都是一个“看起来的乱码”。这就是C++解决虚拟继承的关键。 
问题又来了:当我们这样处理的时候,对于这样的场景我们应该如何去查找对应的分支处基类的字段呢?
例3:
#include<iostream>
using namespace std;class A
{
public:int _a = 1;
};class B : virtual public A
{
public:int _b = 2;
};class C : virtual public A
{
public:int _c = 3;
};class D : public B, public C
{
public:int _d = 4;
};int main()
{D d;B* ptr1 = &d;ptr1->_a; //语句一C* ptr2 = &d;ptr2->_a; //语句二return 0;
}
 
上面的代码中,语句一和语句二是如何找到这个共同的
_a呢?
我们稍后解决……
2.3.1 单虚拟继承
虚拟继承都是用于多继承的情况下,解决数据冗余问题。不过小编为了从简单了解,所以直接采用单继承的虚拟继承原理开始谈起。
我们从简单的一个单继承的虚拟继承模型开始谈起。
例4:
#include<iostream>
using namespace std;
class A
{
public:int _a = 1;
};class B : virtual public A
{
public:int _b = 2;
};int main()
{B b;cout << "DEBUG" << endl;return 0;
}
 
来看内存存储:

-  
这就是一个指针字段。地址为:0x00299b30
 -  
来看这个字段包含的东西:

 -  
进一步思考:0x012FFA34 - 0x012FFA2C == 8(十六进制)
 -  
答案呼之欲出了:
这个字段就是一个指针。这个指针指向了一个表,这个表存放着所继承下来的父类存放在当前指针位置的偏移量!
 
2.3.2 虚拟继承 — 虚基表、虚基表指针
接下来为上面的答案揭秘
- 实际上,通过virtual虚拟继承,我们将分支的父类的成员变量放在了一个公共的区域。
 - 对于原来的对象,我们需要找到这个公共的区域是需要通过偏移量的。而这个偏移量被存储在了一个公共的区域形成了一张表,该表被称为:虚基表。而这个首字段的指针指向这个表,被称为:虚基表指针。
 - 这个存储模型如下:
 

这就是上面的结果!!
现在我们了解到了单继承下的虚拟继承行为,那么多继承就是利用了这样的方式来解决问题!我们迁移到多继承的条件下!
2.3.3 菱形虚拟继承的底层分析
现在回到例3的场景
#include<iostream>
using namespace std;class A
{
public:int _a = 1;
};class B : virtual public A
{
public:int _b = 2;
};class C : virtual public A
{
public:int _c = 3;
};class D : public B, public C
{
public:int _d = 4;
};int main()
{D d;B* ptr1 = &d;ptr1->_a; //语句一C* ptr2 = &d;ptr2->_a; //语句二return 0;
}
 
我们如何来分析,编译器是如何通过语句一、语句二来找到_a的呢?
-  
小编直接给出这个问题的内存存储结构和抽象模型:
- 内存存储结构:

 - 抽象模型:

 
箭头代表从指针开始找到虚基表,再找到基类成员
 - 内存存储结构:
 
	D d;B* ptr1 = &d;ptr1->_a; //语句一C* ptr2 = &d;ptr2->_a; //语句二
 
-  
再来回答上面的问题:语句一(
ptr1->_a;)和语句二(ptr2->_a;)是如何找到对应的_a成员的呢?- 由于
ptr1,ptr2是B类和C类的指针。访问_a的时候,识别到虚基表指针,找到虚基表指针指向的虚基表,得到A类对象的偏移量,最后通过偏移量找到A类对象距离当前指针位置的地址,最后成功访问到A类对象。 
 - 由于
 
现在,我们再来看之前的例2:
#include<iostream>
#include<string>
using namespace std;
class person
{
public://……
protected:string _name = "zhangsan";int _ID = 1;
};class student : virtual public person
{
public:void study(){}
protected:int _sid = 2;
};class teacher : virtual public person //虚拟继承
{
public:void teach(){}
protected:int _tid = 3;
};class assistant : public student, public teacher 
{
public:void check(){}
protected:int _age = 4;
};int main()
{assistant a;cout << "DEBUG" << endl;return 0;
}
 
你能说清楚这个例子的存储模型是如何存储的吗?
 

2.4 小结
-  
首先来小结一下菱形虚拟继承到底是如何进行的:
- 将公共继承的父类,存放到一个共同的区域。
 - 将原有的父类内容改作一个虚基表指针。
 - 这个虚基表指针指向这个共同区域,虚基表。
 - 这个虚基表存放在当前对象到父类对象的偏移量。
 - 通过偏移量找到父类对象。
 - 访问父类对象。
 
 
小编再次提出一个问题:
-  
这个虚基表和偏移量的意义是什么呢?
- 对于不同的同类对象,可以共用一张虚基表。
 - 来看下面的输出:

但是如果你细心的话,你会发现:在D这个对象中B和C对象都是分裂的!即所占用的空间不是连续的空间了!
所以如果不通过偏移量这样的一个统一的模型,编译器是无法找到对应的A类对象是在那个位置的,也就是无法访问成功。
结论:偏移量为访问基类成员提供了统一的方法:先找偏移量再找成员。 
 
完。
希望这篇文章能够帮助你!

