读书笔记 effective c++ Item 32 确保public继承建立“is-a”模型

小编 2026-06-23 阅读:137 评论:0
 1. 何为public继承的”is-a”关系在C++面向对象准则中最重要的准则是:public...

 

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继承强制执行这个解释。看下面的例子:

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

从日常生活中我们知道每个学生都是一个人,但并不是每个人都是学生。这正是上面的继承体系所主张的。我们期望对人来说为真的任何事情——例如一个人有出生年月——对学生来说也是真的。我们不期望对学生来说为真的任何事情——例如在一个特定的学校登记入学——对普通大众来说也是真的。人的概念比学生要更加一般化;而学生是人的一个特定类型。

在C++的领域内,需要Person类型(或者指向Person的指针或者指向Person的引用)参数的任何函数也同样可以使用Student参数(或者指针或引用):

 1 void eat(const Person& p);     // anyone can eat 2  3 void study(const Student& s);            // only students study 4  5 Person p;                            // p is a Person 6  7   8  9 Student s;       // s is a Student10 11 eat(p);    // fine, p is a Person12 13 eat(s);    // fine, s is a Student,14 // and a Student is-a Person15 16 study(s);        // fine17 18 study(p);        // error! p isn’t a Student

 

这仅对public继承来说是有效的。C++仅仅在Student公共继承自Person的时候,其行为表现才会如上面所描述的。Private继承的意义就完全变了(Item 39),protected继承是至今都让我感到困惑的东西。

 

2. Public继承可能误导你——例子一,企鹅不会飞

 

Public继承和”is-a”是等价的听起来简单,但有时候你的直觉会误导你。举个例子,企鹅是鸟这是个事实,鸟能飞也是事实。如果尝试用C++表示,将会产生下面的代码: 

 1 class Bird { 2 public: 3 virtual void fly();            // birds can fly 4  5 ...                                    6  7 }; 8 class Penguin: public Bird {    // penguins are birds 9 10 ...                                   11 12 13 };

 

我们突然陷入了麻烦,因为这个继承体系表明了企鹅会飞,我们知道这不是真的。发生了什么?

2.1 处理上述问题的方法一——更加精确的建模,不定义fly

在这种情况下,我们是一种不精确语言——英语——的受害者。当我们说鸟能飞,我们并没有说所有的鸟都能飞,通常情况下只有有这个能力的才行。如果更加精确一些,我们能够识别出有一些不能飞的鸟的种类,就可以使用如下的继承体系,它更好的模拟了现实:

 1 class Bird { 2 ...                                       // no fly function is declared 3  4 }; 5 class FlyingBird: public Bird { 6 public: 7 virtual void fly(); 8 ... 9 };10 class Penguin: public Bird {11 ...                                       // no fly function is declared12 13 14 };

 

这个继承体系比原来的设计更加忠于现实。 

关于这些家禽的事情还没有完,因为对于一些软件系统来说,没有必要对能飞和不能飞的鸟进行区分。如果你的应用更加关注鸟嘴和鸟的翅膀而对会不会飞漠不关心,最开始的两个类的继承体系就足够了。这反应了一个简单的事实:没有一个理想的设计适用于所有软件最好的设计取决于需要系统去做什么,无论是现在还是将来。如果你的应用没有与飞相关的知识,并且永远也不会有,对能不能飞不做区分或许是一个完美并且有效的设计决策。事实上,能够区分它们的设计或许更可取,因为你尝试为其建模的这种区分有一天可能会从世界上消失。

2.2 处理上述问题的方法二——产生运行时错误

有另外一个学派来处理我上面所描述的“所有的鸟能飞,企鹅是鸟,企鹅不能飞”问题。就是重新为企鹅定义fly函数,但是让其产生运行时错误:

 

1 void error(const std::string& msg); // defined elsewhere2 class Penguin: public Bird {3 public:4 virtual void fly() { error("Attempt to make a penguin fly!"); }5 ...6 };7 8  

 

上面所说的可能会和你想的不一样,能够辨别它们很重要。上面的代码并没有说,“企鹅不能飞。”而是说,“企鹅能飞,但是它们如果尝试这么做会是一个错误”。

 

2.3 区分二者的不同——编译期错误和运行时错误

你如何才能说出它们的不同?从错误被检测出来的时间点看,“企鹅不能飞“这个禁令能够被编译器强制执行,但是如果违反“企鹅尝试飞行是一个错误”这个规则只能够在运行时能够被检测出来。

 

为了表示“企鹅不能飞”这个限制,你要确保对Penguin对象来说没有这样的函数被定义:

1 class Bird {2 ...                                     // no fly function is declared3 4 };5 class Penguin: public Bird {6 ...                                     // no fly function is declared7 8 9 };

 

如果你尝试让企鹅飞起来,编译器会谴责你的行为:

1 Penguin p;2 3 p.fly();                                      // error!4 5  

 

这同产生运行时错误的方法有着很大的不同。如果你使用运行时报错的方法,编译器对p.fly的调用不会说一句话。Item 18解释了好的接口应该在编译期就能够阻止无效代码,所以比起只能在运行时才能侦测出来错误的设计,你应该更加喜欢在编译期就能拒绝企鹅飞翔的设计。

 

3. Public继承可能误导你——例子二,矩形和正方形

 

可能你会做出让步是因为你对鸟类学知识的匮乏,但是你能够依靠你对初步几何的精通,对吧?矩形和正方形会有多复杂呢?

 

现在回答这个简单的问题:正方形类应该public继承自长方形类么?

 

你会说“当然应该!每个人都知道正方形是一个矩形,反之却不成立。”再真不过了,至少是在学校里面。但是我认为我们已经不在学校里面了。

考虑下面的代码:

 1 class Rectangle { 2 public: 3   virtual void setHeight(int newHeight); 4   virtual void setWidth(int newWidth); 5  6 virtual int height() const;        // return current values 7  8 virtual int width() const;        9 10 ...                                            11 12 };                                           13 14  15 16 void makeBigger(Rectangle& r)   // function to increase r’s area17 18 {                                                  19 20 int oldHeight = r.height();           21 22  23 24 r.setWidth(r.width() + 10);   // add 10 to r’s width25 26 assert(r.height() == oldHeight);         // assert that r’s27 28  29 30 }                                                             // height is unchanged

 

很清楚,断言永远不会出错,makeBigger只会修改r的宽度。高度永远不会被修改。

 

现在考虑下面的代码,使用public继承,可以使正方形被当作矩形处理:

 1 class Square: public Rectangle { ... }; 2  3 Square s; 4  5 ... 6  7 assert(s.width() == s.height()); 8  9 // this must be true for all squares10 11 makeBigger(s);12 13 // by inheritance, s is-a Rectangle,14 15 16 // so we can increase its area17 assert(s.width() == s.height()); // this must still be true18 // 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类的每件东西必须也能应用于派生类,因为每个派生类对象是一个基类对象。


作者:HarlanC

博客地址:http://www.cnblogs.com/harlanc/
个人博客: http://www.harlancn.me/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出, 原文链接

如果觉的博主写的可以,收到您的赞会是很大的动力,如果您觉的不好,您可以投反对票,但麻烦您留言写下问题在哪里,这样才能共同进步。谢谢!

版权声明

本文仅代表作者观点,不代表百度立场。
本文系作者授权百度百家发表,未经许可,不得转载。

热门文章
  • 机房智能化温湿度解决方式之POE供电以太网温湿度传感器

    机房智能化温湿度解决方式之POE供电以太网温湿度传感器
    机房智能化温湿度解决方式之POE供电以太网温湿度传感器 北京盈创力和电子科技有限公司 智能型TCP网口温湿度记录仪 北京IP网络温湿度记录仪厂家,北京盈创力和 北京智能型TCP网口温湿度记录仪IP网络温湿度记录仪是一种新型的基于TCP/IP协议双绞线以太网标准温湿度采集模块,利用它可以实现现场温度值、相对湿度值的采集,同时利用其自身的RJ45通信接口可以方便地和机房监控主机或交换机集线器进行联网。 工作于-40℃~85℃工业级带...
  • Sequential Monte Carlo Methods (SMC) 序列蒙特卡洛/粒子滤波/Bootstrap Filtering

    Sequential Monte Carlo Methods (SMC) 序列蒙特卡洛/粒子滤波/Bootstrap Filtering
    Problem Statement 我们考虑一个具有马尔可夫性质、非线性、非高斯的状态空间模型(State Space Model):对于一个时间序列上的观测结果{yt,t∈N}\\{ y_t , t \\in N \\}{yt​,t∈N},我们认为每个观测结果yty_tyt​的生成依赖于一个无法直接观察的隐变量xt∈{xt,t∈N}x_t \\in \\{x_t , t \\in N \\}xt​∈{xt​,t∈N},即:p(...
  • HTTP状态保持的原理

    HTTP状态保持的原理
    a)在用户登录之后,浏览器返回响应的时候会在响应中添加上cookieb)浏览器接收到cookie之后会自动保存c)当用户再次请求同一服务器中的其他网页的时候,浏览器会自动带上之前保存的cookied)服务接收到请求之后可以请 request 对象中取到cookie 判断当前用户是否登录  Http是无状态的,就是连接时数据互通,关闭后...
  • Hive 系统函数及示例

    Hive 系统函数及示例
    查看所有系统函数 show functions; 函数分类 内置函数【系统函数】 数学函数: floor、round、ceil、cos、log2等 字符串函数: length、reverse、trim、lower、get_json_object、repeat等 收集函数: size 转换函数: cast 日期函数: year、month、datediff、date、date_add等 条件函数: coalesce、case…w...
  • CSRF的原理和防范措施

    CSRF的原理和防范措施
    a)攻击原理:i.用户C访问正常网站A时进行登录,浏览器保存A的cookieii.用户C再访问攻击网站B,网站B上有某个隐藏的链接或者图片标签会自动请求网站A的URL地址,例如表单提交,传指定的参数iii.而攻击网站B在访问网站A的时候,浏览器会自动带上网站A的cookieiv.所以网站A在接收到请求之后可判断当前用户是登录状态,所以...
标签列表