継承的致命缺陷——菱形継承

目录

前文

一,单继承/多継承

 二,菱形継承

 三,菱形虚拟継承

 3.1 虚拟継承的用法

3.2 解决原理

 四,継承的总结和反思

总结


扫描二维码关注公众号,回复: 15959820 查看本文章

前文

书接上文,上篇文章我们讲解了一下継承的基础运用,这节我们讲一下継承中惹人诟病的缺点——菱形継承.

一,单继承/多継承

在将菱形継承之前我们需要先讲解一下单継承和多継承

单継承:一个子类只有一个直接父类时称这个继承关系为单继承

 多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承,多继承的出现主要是为了应对生活中的一些场景,如圣女果同时具有水果和蔬菜两种属性,但是可能祖师爷也没有想到这会引出来一个大问题——菱形継承。

 二,菱形継承

菱形継承其实就是多继承的一个变种,如下图所示

菱形継承的使用场景也是有的,但是它弊大于利,使用起来问题颇多,主要问题有两个。

1.二义性的问题,如上图,B和C都有A的元素,那么D调用A时,调用的时B中的A元素还是B中的A元素呢。

2.数据冗余的问题,如上图,B和C中都有A,这导致継承BC的D中也有两个A,这就造成了数据冗余。

class A
{
public:
	int _a;
};

class B :public A
{
protected:
	int _b;
};

class C :public A
{
protected:
	int _c;
};

class D :public B, public C
{
protected:
	int _d;
};
int main()
{
	D d;
	d._a;
	return 0;
}

如上图所示,在访问_a时由于二义性的问题会导致访问不明确,那么我们的祖师爷是怎么解决这个问题呢?

 三,菱形虚拟継承

在C++3.0中,C++引入了虚拟継承来解决菱形継承的二义性和数据冗余问题。

 3.1 虚拟継承的用法

以上面的例子为例,只需要在B和C継承A时在前面加上关键字virtual即可.

class A
{
public:
	int _a;
};

class B :virtual public A
{
protected:
	int _b;
};

class C :virtual public A
{
protected:
	int _c;
};

class D :public B, public C
{
protected:
	int _d;
};
int main()
{
	D d;
	d._a;
	return 0;
}

 成功运行

3.2 解决原理

那么虚拟継承是如何解决菱形継承的二义性和数据冗余的问题的呢?我们可以借助内存窗口观察对象成员的模型

class A
{
public:
	int _a;
};

class B :public A
{
public:
	int _b;
};
//virtual 
class C :public A
{
public:
	int _c;
};

class D :public B, public C
{
public:
	int _d;
};
int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;


	return 0;
}

此次试验借用上面代码,进行有virtual修饰的継承和无修饰的継承的对象成员内存模型的对比

 上图是没有virtual修饰継承的对象成员的内存模型

上图是有virtual修饰的成员变量的内存模型,从上图我们可以得出,D对象将A放到了最下面的空间,这个A同时属于B和C,那么B和C是怎么找到A的呢?这里是通过B和C的两个指针,指向的一张表。这里的指针叫做虚基表指针,这里的表叫做虚基表,虚基表里存着从B或者C到公共区域A的偏移量,然后B和C通过偏移量找到A。

下面是菱形虚拟継承的原理解释

 四,継承的总结和反思

1. C++语法复杂,多继承就是一个很好的体现。本来多继承的出现是为了处理生活中的某些情况,但是由于多继承的出现,就存在菱形继承,有了菱形継承就需要有菱形虚拟継承,这样底层实现就相当复杂,所以一般建议多继承可以用,但是一定不用设计出菱形継承,否则后续的问题会很难解决。

2. 多継承可以认为是C++设计的缺陷,因此后来的很多OO语言(面向对象语言)都没有多继承,如java

3. 継承和组合

1. public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。

2. 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。

3. 优先使用对象组合,而不是类继承

4. 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。

5. 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。

6.实际尽量多去用组合组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合

总结

上面就是継承的所有内容,希望铁子们可以有所收货

猜你喜欢

转载自blog.csdn.net/zcxmjw/article/details/129940973
今日推荐