读书笔记 effective c++ Item 32 确保public继承建立“is-a”模型
1. 何为public继承的”is-a”关系
在C++面向对象准则中最重要的准则是:public继承意味着“is-a”。记住这个准则。
如果你实现一个类D(derived)public继承自类B(base),你在告诉c++编译器(也在告诉代码阅读者),每个类型D的对象也是一个类型B的对象,反过来说是不对的。你正在诉说B比D表示了一个更为一般的概念,而D比B表现了一个更为特殊的概念。你在主张:任何可以使用类型B的地方,也能使用类型D,因为每个类型D的对象都是类型B的对象;反过来却不对,也就是可以使用类型D的地方却不可以使用类型B:D是B,B不是D。
C++ 会为public继承强制执行这个解释。看下面的例子:
class Person { ... };
class Student: public Person { ... };
从日常生活中我们知道每个学生都是一个人,但并不是每个人都是学生。这正是上面的继承体系所主张的。我们期望对人来说为真的任何事情——例如一个人有出生年月——对学生来说也是真的。我们不期望对学生来说为真的任何事情——例如在一个特定的学校登记入学——对普通大众来说也是真的。人的概念比学生要更加一般化;而学生是人的一个特定类型。
在C++的领域内,需要Person类型(或者指向Person的指针或者指向Person的引用)参数的任何函数也同样可以使用Student参数(或者指针或引用):
void eat(const Person& p); // anyone can eat void study(const Student& s); // only students study Person p; // p is a Person Student s; // s is a Student eat(p); // fine, p is a Person eat(s); // fine, s is a Student,
// and a Student is-a Person study(s); // fine study(p); // error! p isn’t a Student
这仅对public继承来说是有效的。C++仅仅在Student公共继承自Person的时候,其行为表现才会如上面所描述的。Private继承的意义就完全变了(Item 39),protected继承是至今都让我感到困惑的东西。
2. Public继承可能误导你——例子一,企鹅不会飞
Public继承和”is-a”是等价的听起来简单,但有时候你的直觉会误导你。举个例子,企鹅是鸟这是个事实,鸟能飞也是事实。如果尝试用C++表示,将会产生下面的代码:
class Bird {
public:
virtual void fly(); // birds can fly ... };
class Penguin: public Bird { // penguins are birds ... };
我们突然陷入了麻烦,因为这个继承体系表明了企鹅会飞,我们知道这不是真的。发生了什么?
2.1 处理上述问题的方法一——更加精确的建模,不定义fly
在这种情况下,我们是一种不精确语言——英语——的受害者。当我们说鸟能飞,我们并没有说所有的鸟都能飞,通常情况下只有有这个能力的才行。如果更加精确一些,我们能够识别出有一些不能飞的鸟的种类,就可以使用如下的继承体系,它更好的模拟了现实:
class Bird {
... // no fly function is declared };
class FlyingBird: public Bird {
public:
virtual void fly();
...
};
class Penguin: public Bird {
... // no fly function is declared };
这个继承体系比原来的设计更加忠于现实。
关于这些家禽的事情还没有完,因为对于一些软件系统来说,没有必要对能飞和不能飞的鸟进行区分。如果你的应用更加关注鸟嘴和鸟的翅膀而对会不会飞漠不关心,最开始的两个类的继承体系就足够了。这反应了一个简单的事实:没有一个理想的设计适用于所有软件。最好的设计取决于需要系统去做什么,无论是现在还是将来。如果你的应用没有与飞相关的知识,并且永远也不会有,对能不能飞不做区分或许是一个完美并且有效的设计决策。事实上,能够区分它们的设计或许更可取,因为你尝试为其建模的这种区分有一天可能会从世界上消失。
2.2 处理上述问题的方法二——产生运行时错误
有另外一个学派来处理我上面所描述的“所有的鸟能飞,企鹅是鸟,企鹅不能飞”问题。就是重新为企鹅定义fly函数,但是让其产生运行时错误:
void error(const std::string& msg); // defined elsewhere
class Penguin: public Bird {
public:
virtual void fly() { error("Attempt to make a penguin fly!"); }
...
};
上面所说的可能会和你想的不一样,能够辨别它们很重要。上面的代码并没有说,“企鹅不能飞。”而是说,“企鹅能飞,但是它们如果尝试这么做会是一个错误”。
2.3 区分二者的不同——编译期错误和运行时错误
你如何才能说出它们的不同?从错误被检测出来的时间点看,“企鹅不能飞“这个禁令能够被编译器强制执行,但是如果违反“企鹅尝试飞行是一个错误”这个规则只能够在运行时能够被检测出来。
为了表示“企鹅不能飞”这个限制,你要确保对Penguin对象来说没有这样的函数被定义:
class Bird {
... // no fly function is declared };
class Penguin: public Bird {
... // no fly function is declared };
如果你尝试让企鹅飞起来,编译器会谴责你的行为:
Penguin p; p.fly(); // error!
这同产生运行时错误的方法有着很大的不同。如果你使用运行时报错的方法,编译器对p.fly的调用不会说一句话。Item 18解释了好的接口应该在编译期就能够阻止无效代码,所以比起只能在运行时才能侦测出来错误的设计,你应该更加喜欢在编译期就能拒绝企鹅飞翔的设计。
3. Public继承可能误导你——例子二,矩形和正方形
可能你会做出让步是因为你对鸟类学知识的匮乏,但是你能够依靠你对初步几何的精通,对吧?矩形和正方形会有多复杂呢?
现在回答这个简单的问题:正方形类应该public继承自长方形类么?
你会说“当然应该!每个人都知道正方形是一个矩形,反之却不成立。”再真不过了,至少是在学校里面。但是我认为我们已经不在学校里面了。
考虑下面的代码:
class Rectangle {
public:
virtual void setHeight(int newHeight);
virtual void setWidth(int newWidth); virtual int height() const; // return current values virtual int width() const; ... }; void makeBigger(Rectangle& r) // function to increase r’s area { int oldHeight = r.height(); r.setWidth(r.width() + ); // add 10 to r’s width assert(r.height() == oldHeight); // assert that r’s } // height is unchanged
很清楚,断言永远不会出错,makeBigger只会修改r的宽度。高度永远不会被修改。
现在考虑下面的代码,使用public继承,可以使正方形被当作矩形处理:
class Square: public Rectangle { ... }; Square s; ... assert(s.width() == s.height()); // this must be true for all squares makeBigger(s); // by inheritance, s is-a Rectangle, // so we can increase its area
assert(s.width() == s.height()); // this must still be true
// for all squares
很清楚的是第二个断言永远不能失败。根据定义,一个正方形的宽度和高度应该一样。
但是现在我们有一个问题。我们怎么才能使下面的断言一致呢?
- 在调用makeBigger之前,s的高度和宽度是一样的;
- 在makeBigger里面,s的宽度被改变了,但是高度却没有;
- makeBigger返回之后,s的高度和宽度仍然相同。(注意s被按引用传递给makeBigger,所以makeBigger修改了s本身,而不是s的拷贝)
欢迎来到public继承的精彩世界,你在其它领域学习而来的直觉(包括数学),使用起来可能和你想要的不一样。上面例子的基本的难点在于适用于矩形的东西(宽度独立于高度被修改)却不适用于正方形(长宽必须相同)。但是public继承主张适用于基类对象的任何东西同样适用于派生类对象。对于长方形和正方形的情况(还有Item38中涉及到的sets和lists的例子),这个主张不再适用,所以使用public继承来为其建模是不正确的。编译器可能会让你这么做,但是正如我们刚刚看到的,我们不能够确保代码的行为是正确的。这也是每个程序员必须要学到的:编码编译通过了不代表它能工作。
4. 使用public继承要有新的洞察力
这些年里使用面向对象设计的时候软件上的直觉会让你失败,不要烦躁。这些知识仍然有价值,现在你的设计兵工厂中又添加了可供替换的继承,你必须用新的洞察力来扩大你的直觉,指导你合适的使用继承。当一些人向你展示长达几页的函数时,你会想起企鹅继承自鸟类或者正方形继承自长方形这些让你感觉有趣的事情。它可能是处理事情的正确方法,只是不是特别像。
5. 其它两种类关系
“is-a”关系不是存在类之间的仅有的关系。另外两个普通的类之间的关系是“has-a”和“is-implemented-in-terms-of”。这些关系在Item38和Item39中被介绍。C++设计出现错误并非不常见,因为其他重要的类关系有可能不正确的被建模为”is-a”,所以你应该确保能明白这些关系之间的区别,并且知道C++中如何最好的塑造它们。
6. 总结
Public继承意味着“is-a”.应用于base类的每件东西必须也能应用于派生类,因为每个派生类对象是一个基类对象。
读书笔记 effective c++ Item 32 确保public继承建立“is-a”模型的更多相关文章
- 读书笔记 effective c++ Item 34 区分接口继承和实现继承
看上去最为简单的(public)继承的概念由两个单独部分组成:函数接口的继承和函数模板继承.这两种继承之间的区别同本书介绍部分讨论的函数声明和函数定义之间的区别完全对应. 1. 类函数的三种实现 作为 ...
- 读书笔记 effective C++ Item 33 避免隐藏继承而来的名字
1. 普通作用域中的隐藏 名字实际上和继承没有关系.有关系的是作用域.我们都知道像下面的代码: int x; // global variable void someFunc() { double x ...
- 读书笔记 effective c++ Item 4 确保对象被使用前进行初始化
C++在对象的初始化上是变化无常的,例如看下面的例子: int x; 在一些上下文中,x保证会被初始化成0,在其他一些情况下却不能够保证.看下面的例子: class Point { int x,y; ...
- Effective C++ Item 32 确保你的 public 继承模子里出来 is-a 关联
本文senlie原版的,转载请保留此地址:http://blog.csdn.net/zhengsenlie 经验:"public继承"意味 is-a.适用于 base classe ...
- 读书笔记 effective c++ Item 36 永远不要重新定义继承而来的非虚函数
1. 为什么不要重新定义继承而来的非虚函数——实际论证 假设我告诉你一个类D public继承类B,在类B中定义了一个public成员函数mf.Mf的参数和返回类型并不重要,所以假设它们都是void. ...
- 读书笔记 effective c++ Item 38 通过组合(composition)为 “has-a”或者“is-implemented-in-terms-of”建模
1. 什么是组合(composition)? 组合(composition)是一种类型之间的关系,这种关系当一种类型的对象包含另外一种类型的对象时就会产生.举个例子: class Address { ...
- 读书笔记 effective c++ Item 39 明智而谨慎的使用private继承
1. private 继承介绍 Item 32表明C++把public继承当作”is-a”关系来对待.考虑一个继承体系,一个类Student public 继承自类Person,如果一个函数的成功调用 ...
- 读书笔记 effective C++ Item 40 明智而谨慎的使用多继承
1. 多继承的两个阵营 当我们谈论到多继承(MI)的时候,C++委员会被分为两个基本阵营.一个阵营相信如果单继承是好的C++性质,那么多继承肯定会更好.另外一个阵营则争辩道单继承诚然是好的,但多继承太 ...
- 读书笔记 effective c++ Item 44 将与模板参数无关的代码抽离出来
1. 使用模板可能导致代码膨胀 使用模板是节省时间和避免代码重用的很好的方法.你不需要手动输入20个相同的类名,每个类有15个成员函数,相反,你只需要输入一个类模板,然后让编译器来为你实例化20个特定 ...
随机推荐
- cgLib生成动态代理
package com.stono.cglib; import java.lang.reflect.Method; import net.sf.cglib.proxy.Enhancer; import ...
- CentOS 6一键系统优化 Shell 脚本
CentOS 6一键系统优化 Shell 脚本 脚本的内容如下: #!/bin/bash#author suzezhi#this script is only for CentOS 6#check t ...
- canvas小程序-快跑程序员
canvas不用说html5带来的好东西,游戏什么的,么么哒 记得有一天玩手机游戏,就是一个跳跃过柱子那种,其实元素很简单啊,app能开发,借助html5 canvas也可以啊, 于是就开始了. -- ...
- RabbitMQ小白菜学习之在window下的安装配置
RabbitMQ安装 首先需要下载RabbitMQ的平台环境Erlang OTP平台和RabbitMQ Server(windows版): OTP 19.1 Windows 64-bit Binary ...
- ML2 配置 OVS VxLAN - 每天5分钟玩转 OpenStack(146)
今天我们开始学习 OVS 如何实现 Neutron VxLAN,关于 VxLAN 的概念以及 Linux Bridge 实现,大家可以参考前面相关章节. Open vSwitch 支持 VXLAN 和 ...
- Java如何判断字符串中包含有全角,半角符号
首先介绍下全角跟半角之间的区别: 在计算机屏幕上,一个汉字要占两个英文字符的位置,人们把一个英文字符所占的位置称为"半角",相对地把一个汉字所占的位置称为"全角" ...
- linux python3.5.0安装并替代centos自带的python
CentOS自带2.7.3版本的Python,旧版本无法及时支持新功能,所以要安装更高版本的Python3.5.0. 1.下载#wget https://www.python.org/ftp/pyth ...
- 谁该吃药了(线性判别法LDA小故事)
一家"胡说八道医院"拥有一种治疗癌症的药物, 根据过去的记录, 该药物对一些患者非常有效, 但是会让一些患者感到更痛苦... 我们希望有一种判别准则能帮助我们判断哪些病人该吃药,哪 ...
- shell编程其实真的很简单(五)
通过前几篇文章的学习,我们学会了shell的基本语法.在linux的实际操作中,我们经常看到命令会有很多参数,例如:ls -al 等等,那么这个参数是怎么处理的呢? 接下来我们就来看看shell脚本对 ...
- svg学习之旅(2)
基本图形 circle 圆 cx基于X轴的坐标位置 cy基于y轴的坐标位置 r圆的半径 fill 填充 transparent透明 stroke 边框 stroke-width 边框宽度 st ...