C++菱形继承
Posted 好想有猫猫
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++菱形继承相关的知识,希望对你有一定的参考价值。
前言
学习继承之后,我们认识到了继承的好处,同样还有继承存在的问题 – 多继承可能存在的
菱形继承
。
本篇博客会已菱形继承展开一系列问题和现象的讨论。
那么话不多说,马上开始今天的学习
文章目录
一. 菱形虚拟继承
我们首先还是模拟一个菱形继承,Person类,Student类,Teacher类,Assistant类
但是我们在Student类
和Teacher类
继承Person类
时,使用虚继承
——关键字virtual
//菱形虚拟继承
class Person
public:
string _name;
;
class Student : virtual public Person
protected:
int _num;//学号
;
class Teacher : virtual public Person
protected:
int _id;//工号
;
class Assistant :public Student, public Teacher
protected:
string _majorCoures;
;
void Test()
Assistant a;
a._name = "peter";
int main()
Test();
return 0;
按照我们上篇学习的,如果是普通继承,这时会造成
数据冗余
和二义性
,Assistant内部会有两个_name
,直接访问也会报错,因为编译器并不知道要访问哪个_name。
但是使用虚继承后,可以直接访问。
接下来,我们通过监视窗口查看一下Assistant的变量情况
我们发现a中的_name都被赋值成peter了!这是怎么做到的呢?
但是监视窗口所查看的是编译器加工过得,不一定是实际的存储情况,接下来我们再深入其底层了解。
二. 虚继承的底层
接下来,我们换一个简单一些的菱形继承,方便我们了解底层
class A
public:
int _a;
;
class B :virtual public A
public:
int _b;
;
class C :virtual public A
public:
int _c;
;
class D :public B, public C
public:
int _d;
;
void Test2()
D d;
d.B::_a = 1;
d.B::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
首先,我们通过内存窗口,查看一下
菱形继承D的数据存储
D中同时拥有两份_a,数据冗余
接下来,我们再来看
菱形虚拟继承的D的内存图
我们发现,原本B::_a和C::_a的位置并没有再存储相应的_a的数据,而是存储了其他的东西
_a也被单独分离出来,只存在了一份。
仅从现象来看,虚继承确实解决
了菱形继承的数据冗
余和二义性
,但是乍一看,好像D的大小还变大了?
接下来,我们先来了解原本存储B::_a和C::_a的位置,存储的新的东西是什么
1. 偏移量
上图的b0 cb 3d 00和24 cc 3d 00其实是两个地址
我们再通过内存窗口去查看这两个地址的内容
PS:注意,一般内存图看的是小端存储
,从左到右是低地址到高地址
,数据的存储是低位放在低地址,高位放在高地址
,所以我们在查找地址时,要输入的其实是003dcdb0
和003dcc24
这两个值都是十六进制
的,所以是20
和12
这两个值,其实是偏移量/偏移地址
通过计算,我们发现B的起始地址加上20
,C的起始地址加上12
,刚好就会到A
PS:第一个位置是预留给多态
的,所以存在第二个位置
这个是D的内存图
当我们通过访问D的_a变量时候,编译器会根据B或者C的_a位置的地址去访问偏移量,进而找到_a在D中存储的位置
接下来我们查看一下虚继承下B的内存图
B b;
b._a=1;
b._b=2;
B中同样遵循
一样的存储方式
,B::_a的位置存储地址,然后第二个存储偏移量,起始地址+偏移量可以找到_a
因为这样,编译器看待这些虚继承的类的数据存储的视角是一样的,所形成的汇编指令也一样。
先找地址,再找偏移量,进而找到虚继承的成员变量
B*ptrbb = &d;
ptrbb->_a++;
B b;
B*ptrdb = &b;
ptrdb->_a++;
同样都是访问_a,汇编指令大致相同
2. 小细节
(1). 成本
我们发现,如果是普通菱形继承,比如最开始A,B,C,D。D中是有
5个变量的
,B::_a,B::_b,C::_a,C::_c,_d。
虚继承后的是B::_b,C::_c,_a,_d还有两个指针,总共6个
。这是不是使得D变大了呢?
这样看确实是这样的,但是这仅仅是A只有一个变量的情况
,如果A有多个变量
,那么因为数据冗余
,会多一倍
的数据,但是虚继承的话,就只会多两个指针
,这样数据相对来说不就节省了吗。
(2). 偏移量表
虚继承后,类里面会有如同B::_a位置的指针,指向存储偏移量的表。这个表示每个对象都有不同的表吗?
答案是不是
我们发现,D实例化的不同对象,其使用的偏移量表都一样!
(3). 多个成员变量
如果A里面有多个变量,那指针和偏移量会有多个吗?
答案是都不会
偏移量表依然只有一个,偏移量也依然只有一个
即使A的变量还有结构体,内存对齐
等规则,但编译器也知道内存对齐的规则
,所以编译器同样也可以通过偏移量
,自己去找
3. 总结
即使虚继承解决了数据冗余和二义性,但在效率上会有所减慢,因为编译器还需要通过偏移量去找数据。
三. 小试牛刀
class A
public:
A(const char*s)
cout << s << endl;
~A()
;
class B :virtual public A
public:
B(const char*s1, const char*s2)
:A(s1)
cout << s2 << endl;
~B()
;
class C :virtual public A
public:
C(const char*s1, const char*s2)
:A(s1)
cout << s2 << endl;
~C()
;
class D :public B,public C
public:
D(const char*s1, const char*s2,const char*s3,const char*s4)
:B(s1,s2)
,C(s1,s3)
,A(s1)
cout << s4 << endl;
~D()
;
int main()
D*p = new D("class A", "class B", "class C", "class D");
delete p;
return 0;
请问,打印内容是什么?
此处我们需要关联前面的知识点。
初始化顺序
跟初始化列表没有关系
,而是跟变量的声明顺序
有关。
而在继承
这里,谁先继承谁就先声明
:
A是最先被B和C继承的
,所以最先调用的构造函数也是A,然后在D的继承是先继承B,再继承C的
,所以其次构造函数调用是先调用B再调用C,最后调用D
所以打印的内容是,class A class B,class C,class D
如果我们将题目改一下
D的继承顺序还是先继承B,再继承C,但是初始化列表先调用C的构造函数,再调用B的构造函数。结果又将变成什么呢?
答案依旧是class A class B,class C,class D
因为构造的顺序和初始化列表的顺序无关,跟声明的顺序有关,我们没有改变继承的顺序,声明的顺序就没有改变。
这次我们改变继承的顺序
结果当然就跟着变了
四. 公有继承和组合
另外,在实际的开发中,在复用父类,减少代码冗余,除了使用
继承
,还可以使用组合
以下是继承和组合的设计方法
继承,是B继承A的所有属性
–is-a
组合,是D中有一个C的对象
–has-a
这两种模式,亲密度,或者说耦合度,继承会比组合高。
因为B是完全继承A,一旦A的保护成员改变
,那么B的也需要跟着改变。
但是C只是D中的一个变量
,在D中无法显示调用C的保护和私有成员
,所以C的改变对D的影响相对来说更小。
接着,我们在来谈论继承的耦合度比组合更高。
假如我们定义一个鸟类,使用继承的方法,我们可以分出很多不同的鸟类。但是对于
一些部分的差异
,我们究竟要不要写在父类
中呢?
- 继承:
比如,大部分鸟都会飞,那我们就要在鸟类提供Fly()的接口吗?但是还有很多鸟类不会飞呀,比如鸵鸟,企鹅等。或许你说,我们还可以将鸟分为会飞的鸟和不会飞的鸟,行,这确实可以解决问题,但是还有很多其他的差异化,难道我们都需要一个一个去细分吗?那这样实现的子类会不会太多太复杂了。
同时,这样也破坏了封装
的特性,因为子类的使用需要完全依赖父类,这需要我们不断的向上查找
某个接口最初实现的父类- 组合:
组合的思路和继承有所差异,正如上面我们简单的样例,我们仅仅是在别的类中实例化出一个类,使用这个类提供的公共接口
,而对于其没有的接口,我们可以自己额外设计
,不用完全依赖
于这一个类
再拿生活中的一个实际的例子举例
继承就像是跟团旅游,整个团体是一个整体,如果一个人迟到了,我们需要等待哪一个人;到达一个景点,即使你觉得不好玩,但依然得跟着大部队待在那里;遇到好玩的景点,你想多玩会,恐怕也不行,得跟上大部队。
组合就像自由旅游,导游大致讲解这个地方的游玩项目,你可以根据自己的喜好分配游玩顺序和时间,但是在指定时间需要集合,也不能出指定范围。
在设计的时候,我们都秉承低耦合,高内聚
。
所以,我们如果在选择继承和选择时,大多会选择组合。
实际尽量多去用组合。组合的耦合度低
,代码维护性好
。不过继承也有用武之地,有些关系适合继承就用继承,另外要实现多态,也必须使用继承
。类之间的关系可以使用继承,可以用组合,就用组合。
结束语
如果觉得本篇文章对你有所帮助的话,不妨点个赞支持一下博主,拜托啦,这对我真的很重要。
C++之菱形继承
当我们谈C++时,我们谈些什么?
封装,继承,多态。这是C++语言的三大特性,而每次在谈到继承时我们不可避免的要谈到一个很重要的问题——菱形继承。
a.菱形继承是什么
如上图,菱形继承即多个类继承了同一个公共基类,而这些派生类又同时被一个类继承。这么做会引发什么问题呢,让我们来看一段代码吧!
#include<iostream> using namespace std; class Base { protected: int _base; public: void fun() { cout << "Base::fun" << endl; } }; class A:public Base { protected: int _a; }; class B : public Base { protected: int _b; }; class D :public A, public B { private: int _d; }; int main() { D d; d.fun();//编译器报错:调用不明确 getchar(); }
我们可以看见D的对象模型里面保存了两份Base,当我们想要调用我们从Base里继承的fun时就会出现调用不明确问题,并且会造成数据冗余的问题,明明可以只要一份就好,而我们却保存了两份。
那么我们可以怎样解决呢?
第一种解决方法,使用域限定我们所需访问的函数
int main() { D d; d.A::fun(); d.B::fun(); getchar(); }
这样的做法是没有问题的,但是,这样做非常的不方便,并且当程序十分大的时候会造成我们思维混乱
于是,C++给了我们一个别的解决方案——虚继承
b.虚继承
虚继承是什么?
如上图,虚继承即让A和B在继承Base时加上virtural关键字,这里需要记住不是D使用虚继承
那么,虚继承又是怎么解决这些烦人的问题的呢?
我们可看见在A和B中不再保存Base中的内容,保存了一份偏移地址,然后将Base的数据保存在一个公共位置处这样保证了数据冗余性的降低同时,我们也能直接的使用d.fun()来调用Base里的fun函数。
#include<iostream> using namespace std; class Base { protected: int _base; public: void fun() { cout << "Base::fun" << endl; } }; class A:virtual public Base { protected: int _a; }; class B :virtual public Base { protected: int _b; }; class D :public A, public B { private: int _d; }; int main() { D d; d.fun(); getchar(); }
*虚继承和虚函数是完全不同的两个概念,希望大家不要随意搞混,想要了解虚函数的同学可以看看博主的另一篇博文《C++的继承&多态》http://zimomo.blog.51cto.com/10799874/1752936
本文出自 “Zimomo” 博客,请务必保留此出处http://zimomo.blog.51cto.com/10799874/1784074
以上是关于C++菱形继承的主要内容,如果未能解决你的问题,请参考以下文章
C++进阶第十五篇—C++中的继承(继承的概念+菱形继承+虚拟继承+组合)