6.继承与面向对象设计面向对象编程OOP风靡已近二十年所以你大概对继承、派生和虚函数这些概念已有一定经验。就算你只写过 C 语言想必也躲不开 OOP 的热潮。不过C 中的 OOP 可能跟你习惯的略有不同。继承可以是单继承或多重继承每一段继承关系可以是 public、protected 或 private也可以是 virtual 或 non-virtual。再说到成员函数的选项——虚函数非虚函数纯虚函数还有它们与其他语言特性之间的交互缺省参数值与虚函数如何互动继承如何影响 C 的名字查找规则设计上的选择又该怎么考量——如果一个类的行为需要被修改虚函数是不是最佳途径本章会把这些都梳理清楚。另外我还会解释 C 中各种特性究竟意味着什么——当你使用某种特定语法结构时你真正表达的设计意图是什么。例如public 继承意味着“is-a”是一种如果你试图让它表达其他含义就会遇到麻烦。同样虚函数意味着“接口必须被继承”而非虚函数则意味着“接口和实现都必须被继承”。区分不清这些含义已经给不少 C 程序员带来了不小的痛苦。如果你理解了 C 各项特性背后的含义你就会发现自己的 OOP 视角也随之改变。它将不再只是区分不同语言特性的练习而是变成一种思考你想对软件系统表达什么设计意图。一旦想清楚了这个用 C 把意图转化为代码也就不那么困难了。条款32确保 public 继承建模的是“is-a”是一种威廉·德门特在他的著作《有人醒着有人沉睡》Some Must Watch While Some Must SleepW. H. Freeman and Company1974中讲了他如何试图让学生们牢牢记住课程核心内容的故事。他告诉全班同学据说普通英国学童对历史记不了太多顶多就知道黑斯廷斯战役发生在 1066 年。德门特强调说就算孩子们别的都记不住也会记住 1066 这个年份。他接着说对于他的课程而言核心要点也只有那么几个——其中颇有意思的一条是安眠药反而会引起失眠。他恳求学生们务必记住这几个关键事实哪怕把课上讨论的其他内容全忘了也没关系整个学期中他也不断回到这些基本原理上。在课程结束时期末考试的最后一题是“请写下这门课中你会终生铭记的一点。”德门特批改试卷时整个人都愣住了——几乎所有人都写了“1066”。所以我带着极大的惶恐在此向你宣告C 面向对象编程中唯一最重要的规则就是——public 继承意味着“is-a”是一种。请务必将这条规则铭记于心。如果你写下“类 D派生类以 public 方式继承类 B基类”你就是在告诉 C 编译器以及阅读你代码的人每一个类型为 D 的对象同时也是一个类型为 B 的对象反之则不成立。你是在说B 代表一个比 D 更一般的概念D 代表一个比 B 更特殊的概念。你是在断言凡是能用 B 对象的地方就能用 D 对象——因为每个 D 对象都是一个 B 对象。反过来如果你需要一个 D 对象一个 B 对象却不能顶替每个 D 都是 B但并非每个 B 都是 D。C 也强制执行这种对 public 继承的理解。来看这个例子我们都知道每个学生都是一个人但并非每个人都是学生。这正是这个继承体系所表达的。我们预期凡是适用于一个人的性质比如有出生日期也同样适用于学生反之并非所有适用于学生的性质比如在某所特定的学校就读都适用于一般人。“人”这个概念比“学生”更一般化——学生是人这一概念的一种特殊化。在 C 的世界里任何期望接受 Person 类型参数或指向 Person 的指针、引用的函数也同样愿意接受 Student 对象或指向 Student 的指针、引用以上行为仅适用于 public 继承。只有当 Student 以 public 方式继承自 Person 时C 才会像上面那样运作。private 继承表达的完全是另一回事参见条款39而 protected 继承的含义我至今也没弄明白。public 继承等同于 is-a这听起来很简单但有时候直觉会误导你。例如我们知道企鹅是一种鸟也知道鸟会飞。如果我们天真地试图用 C 来表达就会得到这样的设计我们一下子就遇到了麻烦因为这个继承体系表明企鹅会飞而我们知道事实并非如此。问题出在哪里在这个例子中我们成了语言含糊的受害者——这里说的是英语。当我们说“鸟会飞”时并不是指所有种类的鸟都会飞而是泛指鸟类一般具备飞翔的能力。如果表达得更严谨一些我们就会意识到世界上其实有几种不会飞的鸟然后设计出下面这个更贴近现实的继承体系这个继承体系相比最初的设计要更贴合我们实际所知的情况。不过关于这些鸟类的探讨还没结束——对某些软件系统而言区分“会飞的鸟”和“不会飞的鸟”可能根本没必要。如果你的应用程序主要关注的是鸟喙和翅膀跟飞翔毫不沾边那么最初那两层的继承体系可能就已经很令人满意了。这其实反映了这样一个事实不存在一个对所有软件都普适的理想设计。最佳设计取决于系统当前以及未来需要做什么。如果你的应用压根没有涉及飞翔的需求也预料将来不会有那么不去区分会飞和不会飞的鸟可能恰恰是一个完全合理的设计决策。事实上它甚至可能优于那个区分了二者的设计因为那种区分在你试图建模的世界里根本不存在。对于如何处理我刚才所说的“所有鸟都会飞企鹅是鸟企鹅不会飞——糟糕”这个难题还有另一种思路为企鹅重新定义 fly 函数让它产生一个运行时错误重要的是要认识到这与你的想法可能有所不同。这里表达的不是“企鹅不会飞”而是“企鹅会飞但实际尝试去做是一个错误”。你如何区分二者的差异看错误在什么时候被发现。“企鹅不会飞”这个约束可以由编译器来强制执行但“企鹅实际尝试飞是一个错误”这条规则只能在运行时才能发现。要表达“企鹅不会飞——就是不会”这个约束你需要确保 Penguin 对象中没有定义这样的函数如果你现在试图让企鹅飞编译器会因你的违规而谴责你这与采用运行时错误方法得到的行为大不相同。用那种方法编译器对调用 p.fly 不会说一个字。条款18指出良好的接口能阻止无效代码通过编译所以你应该优先选择在编译期拒绝对企鹅飞翔尝试的设计而不是只在运行时才能发现问题的设计。也许你会承认自己对鸟类学的直觉可能有所欠缺但你可以依赖自己对初等几何的掌握对吧我的意思是矩形和正方形能有多复杂呢那么回答这个简单的问题类 Square 是否应该以 public 方式继承自类 Rectangle“这还用问”你可能会说“当然应该谁都知道正方形是矩形反过来则不然。”说的没错至少在学校里是这样。但我觉得我们早就不在学校了。看看下面这段代码显然这个断言永远不会失败——makeBigger 只修改了 r 的宽度高度从未变动。现在来看这段代码它通过 public 继承让正方形也能被当作矩形来处理很明显第二个断言也绝不应该失败——根据定义正方形的宽度和高度相等。但现在问题来了。我们如何协调下面这三个事实1.调用 makeBigger 之前s 的高度等于宽度2.在 makeBigger 内部s 的宽度被改变了但高度没有变3.从 makeBigger 返回后s 的高度再次等于宽度注意s 是通过引用传递给 makeBigger 的所以 makeBigger 修改的是 s 本身而非其副本。嗯欢迎来到 public 继承的美妙世界——在这里你在其他领域包括数学培养出的直觉可能并不如你期望的那样管用。本例的根本问题在于适用于矩形的东西宽度可以独立于高度被修改并不适用于正方形宽度和高度必须相等。但 public 继承断言所有适用于基类对象的东西——所有东西——也同样适用于派生类对象。在矩形和正方形这个例子中以及条款38中涉及集合和列表的例子这一断言并不成立所以用 public 继承来建模它们之间的关系是完全错误的。编译器会让你这么做但正如我们刚才所见这并不能保证代码行为正确。正如每个程序员都必须明白的有些人比其他人更需要明白这一点代码能编译不代表它就能正常工作。别担心你多年来积累的软件开发直觉在面向对象设计中不会失效。那些知识仍然宝贵只不过现在你在设计工具箱中加入了继承就需要用新的洞见来充实自己的直觉以指导你正确运用继承。假以时日看到“企鹅继承自鸟”或“正方形继承自矩形”这样的设计你也会产生一种怪异的感觉——就像现在看到有人给你展示一页好几页长的函数时心里那种不对劲一样。那种设计也许碰巧是正确的只是可能性实在不大。类之间并非只有 is-a 这一种关系。另外两种常见的关系是 has-a拥有和 is-implemented-in-terms-of根据某物实现。条款38和39会分别讨论它们。不少 C 设计之所以出问题就是因为把其中一种重要关系错误地当成了 is-a 来建模所以你一定要弄清楚这几类关系的区别并知道在 C 中该如何用最佳方式来刻画它们。切记1.public 继承意味着 is-a。所有适用于基类的东西也必须适用于派生类因为每个派生类对象都是一个基类对象。
Effective C++ 学习笔记 条款32 确定你的public继承塑模出is-a关系
发布时间:2026/7/14 20:09:34
6.继承与面向对象设计面向对象编程OOP风靡已近二十年所以你大概对继承、派生和虚函数这些概念已有一定经验。就算你只写过 C 语言想必也躲不开 OOP 的热潮。不过C 中的 OOP 可能跟你习惯的略有不同。继承可以是单继承或多重继承每一段继承关系可以是 public、protected 或 private也可以是 virtual 或 non-virtual。再说到成员函数的选项——虚函数非虚函数纯虚函数还有它们与其他语言特性之间的交互缺省参数值与虚函数如何互动继承如何影响 C 的名字查找规则设计上的选择又该怎么考量——如果一个类的行为需要被修改虚函数是不是最佳途径本章会把这些都梳理清楚。另外我还会解释 C 中各种特性究竟意味着什么——当你使用某种特定语法结构时你真正表达的设计意图是什么。例如public 继承意味着“is-a”是一种如果你试图让它表达其他含义就会遇到麻烦。同样虚函数意味着“接口必须被继承”而非虚函数则意味着“接口和实现都必须被继承”。区分不清这些含义已经给不少 C 程序员带来了不小的痛苦。如果你理解了 C 各项特性背后的含义你就会发现自己的 OOP 视角也随之改变。它将不再只是区分不同语言特性的练习而是变成一种思考你想对软件系统表达什么设计意图。一旦想清楚了这个用 C 把意图转化为代码也就不那么困难了。条款32确保 public 继承建模的是“is-a”是一种威廉·德门特在他的著作《有人醒着有人沉睡》Some Must Watch While Some Must SleepW. H. Freeman and Company1974中讲了他如何试图让学生们牢牢记住课程核心内容的故事。他告诉全班同学据说普通英国学童对历史记不了太多顶多就知道黑斯廷斯战役发生在 1066 年。德门特强调说就算孩子们别的都记不住也会记住 1066 这个年份。他接着说对于他的课程而言核心要点也只有那么几个——其中颇有意思的一条是安眠药反而会引起失眠。他恳求学生们务必记住这几个关键事实哪怕把课上讨论的其他内容全忘了也没关系整个学期中他也不断回到这些基本原理上。在课程结束时期末考试的最后一题是“请写下这门课中你会终生铭记的一点。”德门特批改试卷时整个人都愣住了——几乎所有人都写了“1066”。所以我带着极大的惶恐在此向你宣告C 面向对象编程中唯一最重要的规则就是——public 继承意味着“is-a”是一种。请务必将这条规则铭记于心。如果你写下“类 D派生类以 public 方式继承类 B基类”你就是在告诉 C 编译器以及阅读你代码的人每一个类型为 D 的对象同时也是一个类型为 B 的对象反之则不成立。你是在说B 代表一个比 D 更一般的概念D 代表一个比 B 更特殊的概念。你是在断言凡是能用 B 对象的地方就能用 D 对象——因为每个 D 对象都是一个 B 对象。反过来如果你需要一个 D 对象一个 B 对象却不能顶替每个 D 都是 B但并非每个 B 都是 D。C 也强制执行这种对 public 继承的理解。来看这个例子我们都知道每个学生都是一个人但并非每个人都是学生。这正是这个继承体系所表达的。我们预期凡是适用于一个人的性质比如有出生日期也同样适用于学生反之并非所有适用于学生的性质比如在某所特定的学校就读都适用于一般人。“人”这个概念比“学生”更一般化——学生是人这一概念的一种特殊化。在 C 的世界里任何期望接受 Person 类型参数或指向 Person 的指针、引用的函数也同样愿意接受 Student 对象或指向 Student 的指针、引用以上行为仅适用于 public 继承。只有当 Student 以 public 方式继承自 Person 时C 才会像上面那样运作。private 继承表达的完全是另一回事参见条款39而 protected 继承的含义我至今也没弄明白。public 继承等同于 is-a这听起来很简单但有时候直觉会误导你。例如我们知道企鹅是一种鸟也知道鸟会飞。如果我们天真地试图用 C 来表达就会得到这样的设计我们一下子就遇到了麻烦因为这个继承体系表明企鹅会飞而我们知道事实并非如此。问题出在哪里在这个例子中我们成了语言含糊的受害者——这里说的是英语。当我们说“鸟会飞”时并不是指所有种类的鸟都会飞而是泛指鸟类一般具备飞翔的能力。如果表达得更严谨一些我们就会意识到世界上其实有几种不会飞的鸟然后设计出下面这个更贴近现实的继承体系这个继承体系相比最初的设计要更贴合我们实际所知的情况。不过关于这些鸟类的探讨还没结束——对某些软件系统而言区分“会飞的鸟”和“不会飞的鸟”可能根本没必要。如果你的应用程序主要关注的是鸟喙和翅膀跟飞翔毫不沾边那么最初那两层的继承体系可能就已经很令人满意了。这其实反映了这样一个事实不存在一个对所有软件都普适的理想设计。最佳设计取决于系统当前以及未来需要做什么。如果你的应用压根没有涉及飞翔的需求也预料将来不会有那么不去区分会飞和不会飞的鸟可能恰恰是一个完全合理的设计决策。事实上它甚至可能优于那个区分了二者的设计因为那种区分在你试图建模的世界里根本不存在。对于如何处理我刚才所说的“所有鸟都会飞企鹅是鸟企鹅不会飞——糟糕”这个难题还有另一种思路为企鹅重新定义 fly 函数让它产生一个运行时错误重要的是要认识到这与你的想法可能有所不同。这里表达的不是“企鹅不会飞”而是“企鹅会飞但实际尝试去做是一个错误”。你如何区分二者的差异看错误在什么时候被发现。“企鹅不会飞”这个约束可以由编译器来强制执行但“企鹅实际尝试飞是一个错误”这条规则只能在运行时才能发现。要表达“企鹅不会飞——就是不会”这个约束你需要确保 Penguin 对象中没有定义这样的函数如果你现在试图让企鹅飞编译器会因你的违规而谴责你这与采用运行时错误方法得到的行为大不相同。用那种方法编译器对调用 p.fly 不会说一个字。条款18指出良好的接口能阻止无效代码通过编译所以你应该优先选择在编译期拒绝对企鹅飞翔尝试的设计而不是只在运行时才能发现问题的设计。也许你会承认自己对鸟类学的直觉可能有所欠缺但你可以依赖自己对初等几何的掌握对吧我的意思是矩形和正方形能有多复杂呢那么回答这个简单的问题类 Square 是否应该以 public 方式继承自类 Rectangle“这还用问”你可能会说“当然应该谁都知道正方形是矩形反过来则不然。”说的没错至少在学校里是这样。但我觉得我们早就不在学校了。看看下面这段代码显然这个断言永远不会失败——makeBigger 只修改了 r 的宽度高度从未变动。现在来看这段代码它通过 public 继承让正方形也能被当作矩形来处理很明显第二个断言也绝不应该失败——根据定义正方形的宽度和高度相等。但现在问题来了。我们如何协调下面这三个事实1.调用 makeBigger 之前s 的高度等于宽度2.在 makeBigger 内部s 的宽度被改变了但高度没有变3.从 makeBigger 返回后s 的高度再次等于宽度注意s 是通过引用传递给 makeBigger 的所以 makeBigger 修改的是 s 本身而非其副本。嗯欢迎来到 public 继承的美妙世界——在这里你在其他领域包括数学培养出的直觉可能并不如你期望的那样管用。本例的根本问题在于适用于矩形的东西宽度可以独立于高度被修改并不适用于正方形宽度和高度必须相等。但 public 继承断言所有适用于基类对象的东西——所有东西——也同样适用于派生类对象。在矩形和正方形这个例子中以及条款38中涉及集合和列表的例子这一断言并不成立所以用 public 继承来建模它们之间的关系是完全错误的。编译器会让你这么做但正如我们刚才所见这并不能保证代码行为正确。正如每个程序员都必须明白的有些人比其他人更需要明白这一点代码能编译不代表它就能正常工作。别担心你多年来积累的软件开发直觉在面向对象设计中不会失效。那些知识仍然宝贵只不过现在你在设计工具箱中加入了继承就需要用新的洞见来充实自己的直觉以指导你正确运用继承。假以时日看到“企鹅继承自鸟”或“正方形继承自矩形”这样的设计你也会产生一种怪异的感觉——就像现在看到有人给你展示一页好几页长的函数时心里那种不对劲一样。那种设计也许碰巧是正确的只是可能性实在不大。类之间并非只有 is-a 这一种关系。另外两种常见的关系是 has-a拥有和 is-implemented-in-terms-of根据某物实现。条款38和39会分别讨论它们。不少 C 设计之所以出问题就是因为把其中一种重要关系错误地当成了 is-a 来建模所以你一定要弄清楚这几类关系的区别并知道在 C 中该如何用最佳方式来刻画它们。切记1.public 继承意味着 is-a。所有适用于基类的东西也必须适用于派生类因为每个派生类对象都是一个基类对象。