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:注意,一般内存图看的是小端存储从左到右是低地址到高地址,数据的存储是低位放在低地址,高位放在高地址,所以我们在查找地址时,要输入的其实是003dcdb0003dcc24

这两个值都是十六进制的,所以是2012
这两个值,其实是偏移量/偏移地址
通过计算,我们发现B的起始地址加上20C的起始地址加上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的影响相对来说更小。

接着,我们在来谈论继承的耦合度比组合更高。

假如我们定义一个鸟类,使用继承的方法,我们可以分出很多不同的鸟类。但是对于一些部分的差异,我们究竟要不要写在父类中呢?

  1. 继承:
    比如,大部分鸟都会飞,那我们就要在鸟类提供Fly()的接口吗?但是还有很多鸟类不会飞呀,比如鸵鸟,企鹅等。或许你说,我们还可以将鸟分为会飞的鸟和不会飞的鸟,行,这确实可以解决问题,但是还有很多其他的差异化,难道我们都需要一个一个去细分吗?那这样实现的子类会不会太多太复杂了。
    同时,这样也破坏了封装的特性,因为子类的使用需要完全依赖父类,这需要我们不断的向上查找某个接口最初实现的父类
  2. 组合:
    组合的思路和继承有所差异,正如上面我们简单的样例,我们仅仅是在别的类中实例化出一个类,使用这个类提供的公共接口,而对于其没有的接口,我们可以自己额外设计,不用完全依赖于这一个类

再拿生活中的一个实际的例子举例
继承就像是跟团旅游,整个团体是一个整体,如果一个人迟到了,我们需要等待哪一个人;到达一个景点,即使你觉得不好玩,但依然得跟着大部队待在那里;遇到好玩的景点,你想多玩会,恐怕也不行,得跟上大部队。
组合就像自由旅游,导游大致讲解这个地方的游玩项目,你可以根据自己的喜好分配游玩顺序和时间,但是在指定时间需要集合,也不能出指定范围。

在设计的时候,我们都秉承低耦合,高内聚
所以,我们如果在选择继承和选择时,大多会选择组合。
实际尽量多去用组合。组合的耦合度低代码维护性好。不过继承也有用武之地,有些关系适合继承就用继承,另外要实现多态,也必须使用继承。类之间的关系可以使用继承,可以用组合,就用组合。

结束语

如果觉得本篇文章对你有所帮助的话,不妨点个赞支持一下博主,拜托啦,这对我真的很重要。

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++进阶第十五篇—C++中的继承(继承的概念+菱形继承+虚拟继承+组合)

C++之继承总结(继承的定义与格式,赋值转换,默认成员函数,菱形继承及菱形虚拟继承)

[C/C++]详解C++中的继承

[C/C++]详解C++中的继承

C ++中的非对称虚拟继承菱形