确定你的public继承塑模出is-a关系——条款32

2023-10-31

 

        如果你令class D(“Derived”)以public形式继承class B(“Base”),你便是告诉C++编译器(以及你的代码读者)说,每一个类型为D的对象同时也是一个类型为B的对象,反之不成立。你的意识是B比D表现出更一般化的概念,而D比B表现出更特殊化的概念。你主张“B对象可派上用场的任何地方,D对象一样可以派上用场”,因为每一个D对象都是一种B对象。反之如果你需要一个D对象,B对象无法效劳,虽然每个D对象都是一个B对象,反之并不成立。

       C++对于“public继承”严格奉行上述见解。考虑以下例子:

class Person { ... };
class Student:public Person { ... };

        根据生活经验我们知道,每个学生都是人,但并非每个人都是学生。这便是这个继承体系的主张。我们预期,对人可以成立的每一件事——例如每个人都有生日——对学生也都成立。但我们并不预期对学生科成立的每一件事——例如他或她就读于某所学校——对人也成立。人的概念比学生更一般化,学生是人的一种特殊形式。

        于是,承上所述,在C++领域中,任何函数如果期望获得一个类型为Person的实参,都也愿意接受一个Student对象:

void eat(const Person& p);      // 任何人都会吃
void study(const Student& s);   // 只有学生才到校学习
Person p;                       // p是人
Student s;                      // s是学生
eat(p);                         // 没问题,p是人
eat(s);                         // 没问题,s是学生,而学生也是(is-a)人
study(s);                       // 没问题,s是学生
study(p);                       // 错误!,p不是学生

        这个论点只对public继承才成立,只有当Student以public形式继承Person,C++的行为才会如我所描述。private继承的意义与此完全不同(见条款39),至于protected继承,那是一种其意义至今仍然困惑我的东西。

        public继承和is-a之间的等价关系听起来颇为简单,但有时候你的直觉可能会误导你,例如,企鹅是一种鸟,这是事实。鸟可以飞,这也是事实。如果我们天真地以C++描述这层关系,结果如下:

class Bird {
public:
	virtual void fly();         //鸟可以飞
	...
};
class Penguin:public Bird {     // 企鹅是一种鸟
	...
};

        在这个例子中,我们成了不严谨语言下的牺牲品。当我们说鸟会飞的时候,我们真正的意思并不是说所有的鸟都会飞,我们要说的只是一般的鸟都有飞行能力。如果严谨一点,我们应该承认一个事实:有数种鸟不会飞。我们来到以下继承关系,它塑模出较佳的真实性:

class Bird {
	...                           // 没有声明fly函数
};
class FlyingBird:public Bird {
public:
	virtual void fly();         
	...
};
class Penguin:public Bird {     
	...                          // 没有声明fly函数
};

        这样的继承体系比原先的设计更能忠实反映我们真正的意思。即便如此,此刻我们仍然未能完全处理好这些鸟事,因为对某些软件系统而言,可能不需要区分会飞的鸟和不会飞的鸟。这反映出一个事实,世界上并不存在一个“适用于所有软件”的完美设计。所谓最佳设计,取决于系统希望做什么事,包括现在与未来。

        在看一个例子,class Square应该以public形式继承Rectangle class吗?

                                                            

        你说,“当然应该如此!每个人都知道正方形是一种矩形,反之则不一定”,这是真理,至少学校是这么教的。但是我不认为我们还在象牙塔内。

        考虑这段代码:

class Rectangle {
public:
	virtual void setHeight(int newHeight);
	virtual void setWidth(int newWidth);
	virtual void height() const;
	virtual void width() const;
	...
};
void makeBigger(Rectangle& r)           // 这个函数用以增加r的面积
{
	int oldHeight = r.height();
	r.setWidth(r.width() + 10);         // 为r的宽度加10
	assert(r.height() == oldHeight);    // 判断r的高度是否未曾改变
}

        显然,上述的assert结果永远为真。因为makeBigger只改变r的宽度;r的高度从未被更改。

        现在考虑这段代码,其中使用public继承,允许正方形被视为一种矩形:

class Square:public Rectangle { ... };
Square s;
...
assert(s.width() == s.height());  // 这对所有正方形一定为真
makeBigger(s);                    // 由于继承,s是一种(is-a)矩形,所有我们可以增加面积
assert(s.width() == s.height());  // 对所有正方形应该仍然为真

        这也很明显,第二个assert结果也应该永远为真。因为根据定义,正方形的高度和宽度相同。

        但现在我们遇上了一个问题。我们如何调解下面各个assert判断式:

  • 调用makeBigger之前,s的高度和宽度相同;
  • 在makeBigger函数内,s的宽度改变,但高度不变;
  • makeBigger返回之后,s的高度再度和其宽度相同。(主要s是以by references方式传给makeBigger,所以makeBigger修改的是s自身,不是s的副本。)

        怎么样?结果不是我们所期待的。因为最后第二个assert的结果不为真了~

        不要因为你发展经年的软件直觉与面向对象观念打交道的过程中失去效用,便心慌意乱起来。那些知识还是有价值的,但现在你已经为你的“设计”军械库加上继承这门大炮,你必须为你的直觉添加新的洞察力,以便引导你适当运用“继承”这一支神兵利器。

        is-a并非是唯一存在于classes之间的关系。另两个常见的关系是has-a(有一个)和is-implemented-terms-of(根据某物实现出)。这些关系将在条款38和39讨论。将上述这些重要的相互关系中的任何一个误塑为is-a而造成的错误设计,在C++中并不罕见,所以你应该确定你确实了解这些个“classes相互关系”之间的差异,并知道如何在C++中最好地塑造它们。

请记住

  • “public继承”意味is-a。适用于base classes身上的每一件事情一定也适用于derived classes身上,因为每一个derived class对象也都是一个base class对象。

上述关于鸟儿和矩形的例子并不是说我们在设计的时候不能这样用继承关系,因为没有适用于所有软件的完美设计。我们设计项目时根据实际情况出发,同时要考虑到可能出现像鸟儿和矩形那种特殊情况,避免出现设计错误,最后能达到最佳设计效果。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

确定你的public继承塑模出is-a关系——条款32 的相关文章

  • Effective C++学习笔记——宁以传引用替换传值

    目录 一 传值效率可能会很低 二 传值可能发生割裂问题 三 适用于传值的情况和注意事项 相关博客 C 引用知识归纳 一 传值效率可能会很低 我们假设有这样两个类 class Human public string name string s
  • 编写new和delete时需固守常规——条款51

    条款50已解释什么时候你会想要写个自己的operator new和operator delete 但并没有解释当你那么做时必须遵守什么规则 这些规则不难奉行 但其中一些并不直观 所以知道它们究竟是些什么很重要 让我们从operator ne
  • 考虑写出一个不抛异常的swap函数——条款25

    swap是个有趣的函数 原本它只是STL的一部分 而后成为异常安全性编程 exception safe programing 见条款29 的脊柱 以及用来处理自我赋值可能性 见条款11 的一个常见机制 由于swap如此有用 适当的实现很重要
  • 区分接口继承和实现继承——条款34

    表面上直截了当的public继承概念 经过严密的检查之后 发现它由两部分组成 函数接口 function interfaces 继承和函数实现 function implementations 继承 这两种继承的差异 很像本书导读所讨论的函
  • 运用成员函数模板接受所有兼容类型——条款45

    所谓智能指针是 行为像指针 的对象 并提供指针没有的机能 例如条款13曾经提及std auto ptr和tr1 shared ptr如何能够被用来在正确时机自动删除heap based资源 STL容器的迭代器几乎总是智能指针 无疑地你不会奢
  • 透彻了解inlining的里里外外——条款30

    Inline函数 多棒的点子 它们看起来像函数 动作像函数 比宏好得多 见条款2 可以调用它们又不需要蒙受函数调用所招致的额外开销 你还能要求更多吗 你实际获得的比想到的还多 因为 免除函数调用成本 只是故事的一部分而已 编译器最优化机制通
  • 《Effective C++》学习笔记——区分接口继承和实现继承

    派生类public继承自基类 其中函数均是接口继承 实现继承又分为缺省继承与强制继承 对应着虚函数与非虚函数 我们在做项目时 对于任何事物都要抱有先描述再组织的心态 因此 当描述事物为其构建框架尤其是存在继承 is a 关系时 一定要搞清楚
  • 令operator=返回一个reference to *this——条款10

    关于赋值 有趣的是你可以把它们写成连锁形式 int x y z x y z 15 同样有趣的是 赋值采用右结合律 所以上述连锁赋值被解析为 x y z 15 这里15先被赋值给z 然后其结果 更新后的z 再被赋值给y 然后其结果 更新后的y
  • 尽可能延后变量定义式的出现时间——条款26

    只要你定义了一个变量而其类型带有一个构造函数或析构函数 那么当程序控制流 control flow 到达这个变量定义式时 你便得承受构造成本 当这个变量离开其作用域时 你便得承受析构成本 即使这个变量最终未被使用 仍需耗费这些成本 所以你应
  • 将文件间的编译依存关系降至最低——条款31

    假设你对C 程序的某个class实现文件做了些轻微修改 注意 修改的不是class接口 而是实现 而且只改private成分 然后重新建置这个程序 并预计只花数秒就好 毕竟只有一个class被修改 你按下 Build 按钮或键入make 或
  • 条款13: 以对象管理资源

    结论 为防止资源泄漏 请使用RAII对象 它们在构造函数中获得资源并在析构函数中释放资源 两个常被使用的RAII classes分别是tr1 share ptr和auto ptr 前者通常是较佳选择 因为其copy行为比较直观 若选择aut
  • Effective C++改善程序与设计的55个具体做法笔记

    Scott Meyers大师Effective三部曲 Effective C More Effective C Effective STL 这三本书出版已很多年 后来又出版了Effective Modern C More Effective
  • Effective C++ 条款十二:复制对象时勿忘其每一个成分

    这句话包含两部分的意思 第一部分是要考虑到所有成员变量 特别是后加入的 相应的拷贝构造函数和赋值运算符要及时更新 第二部分是在存在继承时 不要遗忘基类部分的复制 先看第一部分的意思 举个例子 class SampleClass privat
  • Effective C++ 学习笔记 《六》

    Item 6 Explicitly disallow the use of compiler generated functions you do not want 其实这一节的内容是和item5紧密相连的 上一节的核心围绕着编译器会自动生
  • 将与参数无关的代码抽离templates——条款44

    Templates是节省时间和避免代码重复的一个奇方妙法 不再需要键入20个类似的classes而每一个带有15个成员函数 你只需键入一个class template 留给编译器去具现化那20个你需要的相关classes和300个函数 cl
  • 考虑virtual函数以外的其他选择——条款35

    假设你正在写一个视频游戏软件 你打算为游戏内的人物设计一个继承体系 你的游戏术语暴力砍杀类型 剧中人物被伤害或因其他因素而降低健康状态的情况并不罕见 你因此决定提供一个成员函数healthValue 它会返回一个整数 表示人物的健康程度 由
  • 写了placement new也要写placement delete——条款52

    placement new和placement delete并非C 兽栏中最常见的动物 如果你不熟悉它们 不要感到挫折或忧虑 回忆条款16和17 当你写一个new表达式像这样 Widget pw new Widget 共有两个函数被调用 一
  • 明智而审慎地使用多重继承——条款40

    当多重继承 multiple inheritance MI 进入设计景框 程序有可能从一个以上的base classes继承相同名称 如函数 typedef等等 那会导致较多的歧义机会 例如 class BorrowableItem 图书馆
  • 掌握 Effective C++ : 条款01

    背景 Effective C 是每个 C 程序员都应该读的经典之作 书中涵盖了 C 编程中的一系列最佳实践 包括了面向对象设计 模板 STL 异常处理等方面的内容 由于 C 的发展非常迅速 书中的某些内容可能已经过时 但依然是值得好好学习的
  • Effective C++——尽可能使用const

    const允许指定一个语义约束 也就是指定一个 不该被改动 的对象 而编译器会强制实施这项约束 只要保持某个值不变是事实 就应该说出来 以获得编译器的协助 保证不被违反 const与指针 注意const的写法 const char p p可

随机推荐

  • 最优服务次序问题(贪心法)

    最优服务次序问题 贪心法 问题描述 设有n个顾客同时等待一项服务 顾客i所需要的服务时间为ti 应如何安排顾客的服务次序 才能使平均等待时间最短 平均等待时间是n个顾客等待服务时间的总和除以n 解题思路 1 算法主要思想 本题我们直接采用贪
  • 【小白学习日记】如何使用matlab实现多矩阵排列组合

    matlab实现穷举法 今天在写程序时 需要实现一个穷举 即现有一个1 8的矩阵 第一个元素有四个值 第二到七个元素分别从0 1 1里面取值 要列出所有情况 即有8748种情况 在论坛上遇到一位提出利用ndgrid函数即可解决上述问题 原文
  • python关于字符串下面说法错误的是_Python计算机等级考试中易出错概念问题6(附答案),稳基,修炼,之,易错,含答案...

    1 关于Python对文件的处理 以下选项中描述错误的是 A Python能够以文本和二进制两种方式处理文件 B Python通过解释器内置的open 函数打开一个文件 C 当文件以文本方式打开时 读写按照字节流方式 D 文件使用结束后要用
  • Spring Boot静态资源访问和配置全解析

    一 默认静态资源映射规则 二 自定义静态资源映射规则 2 1 自定义静态资源映射类 2 2 在application properties中进行配置 2 2 1 配置静态资源访问路径 2 2 2 配置静态资源目录 原文 在web开发中 静态
  • 笔试真题之判断一个数字是否是素数(算法详解及python实现)

    题 explain 判断一个数字是否是素数 input 数字n output 如果是素数返回True 否则返回False 示例 判断112272535095293是否是素数 算法思想 首先判断是不是1或2 1不是素数 2是素数 其次判断是不
  • ts文件运行环境搭建和运行步骤

    1 全局安装 TypeScript 语言的编译器 window r gt cmd gt 命令提示行 下输入npm i g typescript 只需首次安装即可 2 在vscode的终端里先运行tsc init tsc是ts语言的编译器 如
  • C++初步

    定义一个命名空间Myspace 包含以下函数 将一个字符串中的所有单词进行反转 并输出反转后的结果 例如 输入字符串为 Hello World 输出结果为 olleH dlroW 并在主函数内测试该函数 include
  • node-sass安装失败番外篇

    node sass安装失败番外篇 工作 环境 错误现象 原因 总结 工作 环境 win10 npm 使用淘宝镜像源 错误现象 报错信息如下 我在不同电脑的不同 编辑器 webstom vscdoe cmd git bash 下显示的错误不太
  • MySQL学习总结--WAL日志

    MySQL学习总结 WAL日志 WAL redo log VS binlog 日志的两阶段提交 组提交机制 group commit redo log 重做日志 binlog 归档日志 binlog 的三种格式对比 WAL 导致了内存脏页
  • JS中用window.open()方式打开,使新页面全屏、居中的代码

    window open 的介绍 1 基本语法 window open pageURL name parameters 2 各项参数 其中yes no也可使用1 0 pixel value为具体的数值 单位象素 参数 取值范围 说明 alwa
  • 基于亚马逊云科技无服务器服务快速搭建电商平台——性能篇

    使用 Serverless 构建独立站的优势 在传统架构模式下 如果需要进行电商大促需要提前预置计算资源以支撑高并发访问 会造成计算资源浪费并且增加运维工作量 本文介绍一种新的部署方式 将 WordPress 和 WooCommerce 部
  • java实现“两数之和”

    java代码如下 import java util Arrays import java util HashMap import java util Map import java util Scanner 问题 两数之和 给定一个数组 和
  • 凸优化系列——约束优化问题

    1 KKT条件 局部最优解 全局最优解 严格最优解 注意几类非光滑函数的转化 约束优化最优解的特征 最优解的一阶必要条件 Karush Kuhn Tucker KKT条件
  • CentOS 7 源码制作openssh 9.4p1 rpm包 —— 筑梦之路

    参考之前的博客 centos 7 制作openssh8 7 8 8 8 9 9 0 9 1 9 2 9 3 p1 rpm包升级 筑梦之路 openssh rpm包 筑梦之路的博客 CSDN博客 需要说明的是9 4版本必须要openssl 1
  • Windows2003系统漏洞提权复现

    操作系统 Microsoft Windows Server 2003 Web服务器 IIS V6 0 第一步 将大马文件上传至服务器根目录 第二步 访问大马文件 进入大马的控制界面 第三步 查看漏洞补丁信息 第四步 上传漏洞工具 将提权工具
  • 在Vue2项目中使用Vant组件库

    Vant 2 Mobile UI Components built on Vuehttps vant contrib gitee io vant v2 zh CN home1 安装vant包 Vue2项目下必须这么写 不能直接写npm i
  • MySQL 排序

    排序数据 1 排序规则 使用ORDER BY 字句排序 在其后面加所需字段 ASC ascend 升序 DESC descend 降序 ORDER BY 字句在SELECT语句的结尾 注意 数据库中默认按照先后添加顺序存储数据 在查询时 也
  • python中的sort的用法

    一 sort的两种用法 1 a sort 对原列表进行原址排序 原址排序的意思是原列表被改变了 排序的规则 数字 字符串按照ASCII 中文按照unicode从小到大排序 a 2 3 6 7 1 a sort print a 1 2 3 6
  • chrony系统授时时,几条重要命令输出信息的含义

    原文 https docs fedoraproject org en US Fedora 18 html System Administrators Guide sect Checking if chrony is synchronized
  • 确定你的public继承塑模出is-a关系——条款32

    如果你令class D Derived 以public形式继承class B Base 你便是告诉C 编译器 以及你的代码读者 说 每一个类型为D的对象同时也是一个类型为B的对象 反之不成立 你的意识是B比D表现出更一般化的概念 而D比B表