C++继承与组合的区别
C++程序开发中,设计孤立的类比较容易,设计相互关联的类却比较难,这其中会涉及到两个概念,一个是继承(Inheritance),一个是组合(Composition)。因为二者有一定的相似性,往往令程序员混淆不清。类的组合和继承一样,是软件重用的重要方式。组合和继承都是有效地利用已有类的资源。但二者的概念和用法不同。
如果类B 有必要使用A 的功能,则要分两种情况考虑:
1.继承
若在逻辑上B 是一种A (is a kind of),则允许B 继承A 的功能,它们之间就是Is-A 关系。如男人(Man)是人(Human)的一种,女人(Woman)是人的一种。那么类Man 可以从类Human 派生,类Woman也可以从类Human 派生。示例程序如下:
class Human
{
…
};
class Man : public Human
{
…
};
class Woman : public Man
{
…
};
在UML的术语中,继承关系被称为泛化(Generalization),类Man和Woman与类Human的UML关系图可描述如下:
继承在逻辑上看起来比较简单,但在实际应用上可能遭遇意外。比如在OO界中著名的“鸵鸟不是鸟”和“圆不是椭圆”的问题。这样的问题说明了程序设计和现实世界存在逻辑差异。从生物学的角度,鸵鸟(Ostrich)是鸟(Bird)的一种,既然是Is-A的关系,类COstrich应该可以从类CBird派生。但是鸵鸟不会飞,但从CBird那里继承了接口函数fly,如下所示:
class CBird{
public:
virtual void fly(){}
};
class COstrich{
public:
...
};
“圆不是椭圆”同样存在类似的问题,圆从椭圆类继承了无用的长短轴数据成员。所以更加严格的继承应该是:若在逻辑上B是A的一种,并且A的所有功能和属性对B都有意义,则允许B继承A的所有功能和属性。
类继承允许我们根据自己的实现来覆盖重写父类的实现细节,父类的实现对于子类是可见的,所以我们一般称之为白盒复用。继承易于修改或扩展那些被复用的实现,但它这种白盒复用却容易破坏封装性。因为这会将父类的实现细节暴露给子类。
2.组合
若在逻辑上A 是B 的“一部分”(a part of),则不允许B 继承A 的功能,而是要用A和其它东西组合出B,它们之间就是“Has-A关系”。例如眼(Eye)、鼻(Nose)、口(Mouth)、耳(Ear)是头(Head)的一部分,所以类Head 应该由类Eye、Nose、Mouth、Ear 组合而成,不是派生而成。示例程序如下:
class Eye
{
public:
void Look(void);
};
class Nose
{
public:
void Smell(void);
};
class Mouth
{
public:
void Eat(void);
};
class Ear
{
public:
void Listen(void);
};
// 正确的设计,冗长的程序
class Head
{
public:
void Look(void) { m_eye.Look(); }
void Smell(void) { m_nose.Smell(); }
void Eat(void) { m_mouth.Eat(); }
void Listen(void) { m_ear.Listen(); }
private:
Eye m_eye;
Nose m_nose;
Mouth m_mouth;
Ear m_ear;
};
如果允许Head 从Eye、Nose、Mouth、Ear 派生而成,那么Head 将自动具有Look、Smell、Eat、Listen 这些功能:
// 错误的设计
class Head : public Eye, public Nose, public Mouth, public Ear
{
};
上述程序十分简短并且运行正确,但是这种设计却是错误的。所以我们要经的起“继承”的诱惑,避免犯下设计错误。
在UML中,上面类的UML关系图可描述如下:
实心菱形代表了一种坚固的关系,被包含类的生命周期受包含类控制,被包含类会随着包含类创建而创建,消亡而消亡。组合属于黑盒复用,被包含对象的内部细节对外是不可见的,所以它的封装性相对较好,实现上相互依赖比较小,并且可以通过获取其它具有相同类型的对象引用或指针,在运行期间动态的定义组合。而缺点就是致使系统中的对象过多。
综上所述,Is-A关系用继承表示,Has-A关系用组合表示,GoF在《设计模式》中指出OO设计的一大原则就是:优先使用对象组合,而不是类继承。
3.解决“圆不是椭圆”继承问题,杜绝不良继承
封装、继承、多态是面向对象技术的三大机制,封装是基础、继承是关键、多态是延伸。继承是作为关键的一部分,如果我们理解不够深刻,则容易造成程序设计中的不良继承,影响程序质量。
上文中“圆不是椭圆”这一著名问题,实际上在数学上圆是一种特殊的椭圆,于是会出现下面的继承:
class CEllipse{
public:
void setSize(float x,float y){}
};
class CCircle:public CEllipse{};
椭圆存在一个设置长短轴的成员函数setSize,而圆则不需要。椭圆能做某些圆不能做的事,所以圆继承自椭圆是不合理的类设计。那么面对“圆是/不是一种椭圆”这个两难的问题,我们如何解决。主要有几下几种方法:
(1)使用代码技巧来弥补设计缺陷。在子类CCircle中重新定义setSize抛出异常,或终止程序,或做其他的异常处理,但这些技巧会让用户吃惊不已,违背了接口设计的“最小惊讶原则”;
(2)改变观点,人为圆是不对称的。这对于我们思维严谨的程序员来说,有点不可接受;
(3)将基类的成员函数setSize删除。但这回影响椭圆对象的正常使用。
(4)去掉它们之间的继承关系。推荐做法,既然圆继承椭圆是一种不良类设计,我们就应该杜绝。去掉继承关系,并不代表圆与椭圆就没有关系,两个类可以继承自同一个类COvalShape,不过该类不能执行不对称的setSize计算,如下图所示:
class COvalShape{
public:
void setSize(float x);
};
class CEllipse:public COvalShape{
public:
void setSize(float x,float y);
};
class CCircle:public COvalShape{
};
其中,椭圆增加了特有的setSize(float x,float y)运算。
不良继承出现的根本原因在于对继承的理解不够深刻,错把直觉中的“是一种(Is-A)”当成了学术中的“子类型(subtype)”概念。在继承体系中,派生类对象是可以取代基类对象的。而在椭圆和圆的问题上,椭圆类中的成员函数setSize(x,y)违背了这个可置换性,即Liskov替换原则。
所有不良继承都可以归结为“圆不是椭圆”这一著名具有代表性的问题上。在不良继承中,基类总会有一些额外能力,而派生类却无法满足它。这些额外的能力通常表现为一个或多个成员函数提供的功能。要解决这一问题,要么使基类弱化,要么消除继承关系,需要根据具体情形来选择。
参考文献
[1]C++中继承和组合区别与使用
[2]李健.编写高质量代码:改善C++程序的150个建议.第一版.北京:机械工业出版社,2012.1:303-310
C++继承与组合的区别的更多相关文章
- java中继承和组合的区别
子类继承父类,父类的所有属性和方法都可以被子类访问和调用.组合是指将已存在的类型作为一个新建类的成员变量类型,又叫"对象持有". 通过组合和继承,都可以实现系统功能的重用和代码的复 ...
- c++ 继承和组合的区别
.什么是继承 A继承B,说明A是B的一种,并且B的所有行为对A都有意义 eg:A=WOMAN B=HUMAN A=鸵鸟 B=鸟 (不行),因为鸟会飞,但是鸵鸟不会. .什么是组合 若在逻辑上A是B的“ ...
- Objective-C的继承与组合
Objective-C的继承与组合 Objective-C与Java继承上的区别 区别 Objective-C Java 成员变量 Objective-C继承不允许子类和父类拥有相同名称的成员变量 J ...
- <Java中的继承和组合之间的联系和区别>
//Java中的继承和组合之间的联系和区别 //本例是继承 class Animal { private void beat() { System.out.println("心胀跳动...& ...
- Inheritance, Association, Aggregation, and Composition 类的继承,关联,聚合和组合的区别
在C++中,类与类之间的关系大概有四种,分别为继承,关联,聚合,和组合.其中继承我们大家应该都比较熟悉,因为是C++的三大特性继承Inheritance,封装Encapsulation,和多态Poly ...
- Java中的继承与组合(转载)
本文主要说明Java中继承与组合的概念,以及它们之间的联系与区别.首先文章会给出一小段代码示例,用于展示到底什么是继承.然后演示如何通过“组合”来改进这种继承的设计机制.最后总结这两者的应用场景,即到 ...
- Java中的继承与组合
本文主要说明Java中继承与组合的概念,以及它们之间的联系与区别.首先文章会给出一小段代码示例,用于展示到底什么是继承.然后演示如何通过“组合”来改进这种继承的设计机制.最后总结这两者的应用场景,即到 ...
- Go如何使用实现继承的组合
Go它提供了一个非常值得称道的并发支持,但Go它不支持完全面向对象的.这并不意味着Go不支持面向对象,,和Go的OO系统做的很轻巧,学习降至最低成本.向对象让Go失去了一些OO的方便特性,可是更高的效 ...
- 三张图搞懂JavaScript的原型对象与原型链 / js继承,各种继承的优缺点(原型链继承,组合继承,寄生组合继承)
摘自:https://www.cnblogs.com/shuiyi/p/5305435.html 对于新人来说,JavaScript的原型是一个很让人头疼的事情,一来prototype容易与__pro ...
随机推荐
- 国密算法--Openssl 实现国密算法(加密和解密)
上一次讲了产生密钥,这次我们讲一下加密解密的实现. 先说一下加密解密的流程,一下这些内容都是从国密局发布的国密标准文档里面摘录出来的.大家可以去国密局的网站上自己下载. 下列符号适用于本部分. A,B ...
- 【RL系列】马尔可夫决策过程——状态价值评价与动作价值评价
请先阅读上两篇文章: [RL系列]马尔可夫决策过程中状态价值函数的一般形式 [RL系列]马尔可夫决策过程与动态编程 状态价值函数,顾名思义,就是用于状态价值评价(SVE)的.典型的问题有“格子世界(G ...
- unset命令详解
基础命令学习目录首页 功能说明:unset是一个内建的Unix shell命令,在Bourne shell家族(sh.ksh.bash等)和C shell家族(csh.tcsh等)都有实现.它可以取消 ...
- (转)Django 数据库
转:https://blog.csdn.net/ayhan_huang/article/details/77575186 目录 数据库说明 配置数据库 在屏幕输出orm操作对应的s ...
- 实验五Java网络编程及安全——20135337朱荟潼
实验五 Java网络编程及安全 结对伙伴:20135317韩玉琪(负责服务器方)http://www.cnblogs.com/hyq20135317/p/4567241.html 实验内容 1.掌握S ...
- 课堂实践ASL博客
实践博客 二分法查找元素 1.首先定义三个位置min,mid,max 2.每次从所有元素所处位置的中间开始查找(所有元素必须以由小及大顺序排列完毕) 3.当中间元素大于所查找元素时,从中间元素(mid ...
- Date 类的使用
package com.Date.Math; import java.text.ParseException; import java.text.SimpleDateFormat; import ja ...
- 内网php项目访问(切换在线解决)
之前内网访问出现过问题: 可参考手机访问本地php项目遇到的问题及解决(2015-06-20 09:41) 后来重装wamp之后,要访问还是出现问题 即http://192.168.191.1/mui ...
- sqlserver结束和监视耗时的sql
在对象资源管理器中右击服务器地址选择“活动和监视器”. 点击最近耗费大量资源的查询
- KEIL C51 printf格式化输出特殊用法
作者:dragoniye 发布:2014-02-15 12:44 分类:硬件 抢沙发 /*******************************************KEI ...