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 Human
{…
};

在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++ 继承与组合的区别相关推荐

  1. python中继承和组合的区别_Py修行路 python基础 (十五)面向对象编程 继承 组合 接口和抽象类...

    一.前提回忆: 1.类是用来描述某一类的事物,类的对象就是这一类事物中的一个个体.是事物就要有属性,属性分为 1:数据属性:就是变量 2:函数属性:就是函数,在面向对象里通常称为方法 注意:类和对象均 ...

  2. 面向对象设计原则——优先使用对象组合,而不是继承(组合以及与继承的区别)

    看到面向对象设计原则中的合成复用原则: 优先使用对象组合,而不是继承 类继承:也叫白箱复用 对象组合:也叫黑箱复用. 继承某种程度上破坏了封装性,子父类之间的耦合性过高. 对象组合只要求被组合的对象具 ...

  3. 详细分析如何在java代码中使用继承和组合

    文章目录 继承与组合 何时在Java中使用继承 何时在Java中使用组合 继承与组成:两个例子 用Java继承重写方法 Java不具有多重继承 使用super访问父类方法 构造函数与继承一起使用 类型 ...

  4. C++ 面向对象(一)继承:继承、对象切割、菱形继承、虚继承、继承与组合

    目录 继承 继承的概念 继承方式 基类与派生类的赋值转换 作用域与隐藏 派生类的默认成员函数 友元与静态成员 友元 静态成员 多继承 菱形继承 虚继承 继承和组合 什么是组合 如何选择组合和继承 继承 ...

  5. UML中关联,聚合,组合的区别及C++实现

    类间关系 在类图中,除了需要描述单独的类的名称.属性和操作外,我们还需要描述类之间的联系,因为没有类是单独存在的,它们通常需要和别的类协作,创造比单独工作更大的语义.在UML类图中,关系用类框之间的连 ...

  6. 10玩rust_C++工程师的Rust迁移之路(5)- 继承与组合 - 下

    2020-11-25 更新: 修正了C++ 20中的concept语法 在上一篇文章 https://zhuanlan.zhihu.com/p/76740667 中,我介绍多态.静态分发和动态分发的概 ...

  7. 【设计模式之美】<Reading Notes>继承与组合

    继承缺点 继承是面向对象的四大特性之一,用来表示类之间的 is-a 关系,可以解决代码复用的问题.虽然继承有诸多作用,但继承层次过深.过复杂,也会影响到代码的可维护性.在这种情况下,我们应该尽量少用, ...

  8. C++继承和组合——带你读懂接口和mixin,实现多功能自由组合

    摘要: 本文详细介绍了C++继承的三种方式和相关重要概念,整理了众多继承与组合中的注意问题.在C++继承存在不安全的默认实现,非虚函数的覆盖,多重继承的函数名冲突.菱形继承等众多问题下,如何实现多个功 ...

  9. Java中的继承与组合

    本文主要说明Java中继承与组合的概念,以及它们之间的联系与区别.首先文章会给出一小段代码示例,用于展示到底什么是继承.然后演示如何通过"组合"来改进这种继承的设计机制.最后总结这 ...

最新文章

  1. 功能很全的图书馆管理系统
  2. python开发移动应用_什么是移动应用开发的最佳编程语言?(一)Python?c++?
  3. Thinkphp5.0快速入门笔记(1)
  4. 粒子群PSO优化算法学习笔记 及其python实现(附讲解如何使用python语言sko.PSO工具包)
  5. Ghost 2.16.3 发布,基于 Markdown 的在线写作平台
  6. 基于MySQL和DynamoDB的强一致性分布式事务实践
  7. SAP Fiori Launchpad tile里显示的数字的刷新间隔是在服务器端什么地方配置的
  8. 自己动手写事件总线(EventBus)
  9. Tensorboard --logdir=logs 无法显示图像的处理办法
  10. Linux操作系统yum常用命令
  11. 网站 html 中英文切换 - API 总结篇
  12. yolov3目标检测android,目标检测 | YOLOv3训练自己的数据全流程
  13. 西瓜书+实战+吴恩达机器学习(二二)概率图模型之马尔可夫随机场
  14. Linux下查看网卡信息及确定网卡位置以及更改网卡名称
  15. 模拟电子技术基础》期中考试试题,仅供参考
  16. 中国大学计算机专业排名教育部,全国计算机专业学校排名!别选错学校了
  17. Verilog永无止境
  18. java 图像处理截圆形的图形(修改版)
  19. HDOJ2502月之数
  20. 01.JDK安装+JDK环境配置+验证JDK安装是否成功

热门文章

  1. 用foobar进行码率转换 适用与sacd-r转成低码率
  2. 插头DP题目泛做(为了对应WYD的课件)
  3. UVA 10572 Black White
  4. Spring----AOP的术语
  5. 工作日志(一)、jquery上传插件uploadify的使用
  6. 用Python解压tgz文件
  7. 多线程之wait和notify使用注意事项
  8. L3-029 还原文件 (30 分)-PAT 团体程序设计天梯赛 GPLT
  9. [Java] 蓝桥杯BASIC-24 基础练习 龟兔赛跑预测
  10. 蓝桥杯 ADV-89 算法提高 输出九九乘法表