Effective C++条款32:确定你的public继承塑模出is-a关系(Make sure public inheritance models “is-a.“)

本文主要是介绍Effective C++条款32:确定你的public继承塑模出is-a关系(Make sure public inheritance models “is-a.“),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

Effective C++条款32:确定你的public继承塑模出is-a关系(Make sure public inheritance models "is-a.")

  • 条款32:确定你的public继承塑模出is-a关系
    • 1、什么是public继承的”is-a”关系
    • 2、设计正确的继承模型
      • 2.1 更加精确的建模
      • 2.2 产生运行时的错误
      • 2.3 对比
    • 3、is-a模型的一些例外
    • 6、牢记
  • 总结


《Effective C++》是一本轻薄短小的高密度的“专家经验积累”。本系列就是对Effective C++进行通读:

第6章:继承与面向对象设计

在这里插入图片描述


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

1、什么是public继承的”is-a”关系

  • 在C++面向对象准则中最重要的一个规则是:public inheritance(公开继承)意味着“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);     // 任何人都可以吃
void study(const Student& s);            // 只有学生才到校学习
Person p;                            // p 是人
Student s;       // s 是学生
eat(p);    // 没问题,p是人
eat(s);    // 没问题,s是学生,学生也是人
study(s);        
study(p);        // error! p 不是个学生

  这个论点只对public继承才成立。只有当Student公共继承自Person的时候,其行为表现才会如上面所描述的。Private继承的意义就完全变了(见条款39),protected继承是至今都让我感到困惑的东西。

2、设计正确的继承模型

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

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

2.1 更加精确的建模

  • 对上面的代码进行修改
    在这种情况下,我们是一种不精确语言——英语——的受害者。当我们说鸟能飞,我们并没有说所有的鸟都能飞,通常情况下只有有这个能力的才行。如果更加精确一些,我们能够识别出有一些不能飞的鸟的种类,就可以使用如下的继承体系,它更好的模拟了现实:
//鸟类
class Bird {//无fly()函数
}; 
//会飞的鸟类
class FlyingBird :public Bird {
public:virtual void fly();
};//企鹅不会飞
class Penguin :public Bird {...
};

2.2 产生运行时的错误

  企鹅不会飞,但是我们仍然让Bird定义fly()函数,然后让Penguin继承于Bird,与上面不同的是,我们让Penguin在执行fly()函数的时候报出一个错误(运行期执行)。代码如下:

class Bird {
public:virtual void fly();
};void error(const std::string& msg);
class Penguin :public Bird {
public:virtual void fly() {error("Attempt to make a penguin fly!");}
};

  上面的代码并没有说,“企鹅不能飞。”而是说,“企鹅能飞,但是它们如果尝试这么做会是一个错误”。

2.3 对比

  • 为了表现“企鹅不会飞,就这样”的限制,你不可以为Penguin定义fly函数:
class Bird {//无fly()函数
};
class Penguin :public Bird {...
};
  • 如果你尝试让企鹅飞起来,编译器会谴责你的行为:
Penguin p;
p.fly();//error!

  这和采取“令程序于运行期发生错误”的解法不同。如果你使用运行时报错的方法,编译器对p.fly的调用不会有什么动作。条款18说过:好的接口应该在编译期就能够阻止无效代码,所以比起只能在运行时才能侦测出来错误的设计,你应该更加喜欢在编译期就能拒绝企鹅飞行的设计。

3、is-a模型的一些例外

  每个人都知道长方形和正方形,那么,正方形类应该public继承自长方形类么?

在这里插入图片描述

  你会说“当然应该!每个人都知道正方形是一个矩形,反之却不成立。”再真不过了,至少学校是这样教的。但是我不认为我们还在象牙塔内。

  考虑这段代码:

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); //设置新宽度assert(r.height() == oldHeight); //判断高度是否改变
}

  显然,上述的 assert 结果永远为真,makeBigger只会修改r的宽度。r的高度永远不会被修改。

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

class Square :public Rectangle { ... };
Square s; //正方形类
...
assert(s.width() == s.height()); //永远为真,因为正方形的宽和高相同
makeBigger(s); //由于继承,我们可以增加正方形的面积
assert(s.width() == s.height()); //对所有正方形来说,应该还是为真

很清楚的是第二个assert结果也应该永远为真。根据定义,一个正方形的宽度和高度应该一样。

但是现在我们有一个问题。我们怎么如何调解下面各个 assert 判断式?

  • 在调用makeBigger之前,s的高度和宽度是相同;

  • 在makeBigger里面,s的宽度被改变了,但是高度不变;

  • 再次调用assert应该还是返回真,因为此处的s为正方形(注意s被按引用传递给makeBigger,所以makeBigger修改了s本身,而不是s的拷贝)

现在我们知道:

  • 作用于基类的代码,使用派生类也可以执行。

  • 但某些施行于矩形类中的代码(例如只改变宽度而不改变高度),在长方形中却不可以实施(因为长方形的宽度和高度应该保持一致)

  • is-a并非是唯一存在于classes之间的关系。另两个常见的关系是has-a(有一个)和is-implemented-terms-of(根据某物实现出)。这些关系将在条款38和条款39介绍。在这些相互关系的塑造为is-a会造成错误设计。

6、牢记

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

总结

期待大家和我交流,留言或者私信,一起学习,一起进步!

这篇关于Effective C++条款32:确定你的public继承塑模出is-a关系(Make sure public inheritance models “is-a.“)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/260546

相关文章

【C++ Primer Plus习题】13.4

大家好,这里是国中之林! ❥前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。有兴趣的可以点点进去看看← 问题: 解答: main.cpp #include <iostream>#include "port.h"int main() {Port p1;Port p2("Abc", "Bcc", 30);std::cout <<

C++包装器

包装器 在 C++ 中,“包装器”通常指的是一种设计模式或编程技巧,用于封装其他代码或对象,使其更易于使用、管理或扩展。包装器的概念在编程中非常普遍,可以用于函数、类、库等多个方面。下面是几个常见的 “包装器” 类型: 1. 函数包装器 函数包装器用于封装一个或多个函数,使其接口更统一或更便于调用。例如,std::function 是一个通用的函数包装器,它可以存储任意可调用对象(函数、函数

C++11第三弹:lambda表达式 | 新的类功能 | 模板的可变参数

🌈个人主页: 南桥几晴秋 🌈C++专栏: 南桥谈C++ 🌈C语言专栏: C语言学习系列 🌈Linux学习专栏: 南桥谈Linux 🌈数据结构学习专栏: 数据结构杂谈 🌈数据库学习专栏: 南桥谈MySQL 🌈Qt学习专栏: 南桥谈Qt 🌈菜鸡代码练习: 练习随想记录 🌈git学习: 南桥谈Git 🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈�

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

06 C++Lambda表达式

lambda表达式的定义 没有显式模版形参的lambda表达式 [捕获] 前属性 (形参列表) 说明符 异常 后属性 尾随类型 约束 {函数体} 有显式模版形参的lambda表达式 [捕获] <模版形参> 模版约束 前属性 (形参列表) 说明符 异常 后属性 尾随类型 约束 {函数体} 含义 捕获:包含零个或者多个捕获符的逗号分隔列表 模板形参:用于泛型lambda提供个模板形参的名

6.1.数据结构-c/c++堆详解下篇(堆排序,TopK问题)

上篇:6.1.数据结构-c/c++模拟实现堆上篇(向下,上调整算法,建堆,增删数据)-CSDN博客 本章重点 1.使用堆来完成堆排序 2.使用堆解决TopK问题 目录 一.堆排序 1.1 思路 1.2 代码 1.3 简单测试 二.TopK问题 2.1 思路(求最小): 2.2 C语言代码(手写堆) 2.3 C++代码(使用优先级队列 priority_queue)

【C++高阶】C++类型转换全攻略:深入理解并高效应用

📝个人主页🌹:Eternity._ ⏩收录专栏⏪:C++ “ 登神长阶 ” 🤡往期回顾🤡:C++ 智能指针 🌹🌹期待您的关注 🌹🌹 ❀C++的类型转换 📒1. C语言中的类型转换📚2. C++强制类型转换⛰️static_cast🌞reinterpret_cast⭐const_cast🍁dynamic_cast 📜3. C++强制类型转换的原因📝

POJ1269 判断2条直线的位置关系

题目大意:给两个点能够确定一条直线,题目给出两条直线(由4个点确定),要求判断出这两条直线的关系:平行,同线,相交。如果相交还要求出交点坐标。 解题思路: 先判断两条直线p1p2, q1q2是否共线, 如果不是,再判断 直线 是否平行, 如果还不是, 则两直线相交。  判断共线:  p1p2q1 共线 且 p1p2q2 共线 ,共线用叉乘为 0  来判断,  判断 平行:  p1p

C++——stack、queue的实现及deque的介绍

目录 1.stack与queue的实现 1.1stack的实现  1.2 queue的实现 2.重温vector、list、stack、queue的介绍 2.1 STL标准库中stack和queue的底层结构  3.deque的简单介绍 3.1为什么选择deque作为stack和queue的底层默认容器  3.2 STL中对stack与queue的模拟实现 ①stack模拟实现

pip-tools:打造可重复、可控的 Python 开发环境,解决依赖关系,让代码更稳定

在 Python 开发中,管理依赖关系是一项繁琐且容易出错的任务。手动更新依赖版本、处理冲突、确保一致性等等,都可能让开发者感到头疼。而 pip-tools 为开发者提供了一套稳定可靠的解决方案。 什么是 pip-tools? pip-tools 是一组命令行工具,旨在简化 Python 依赖关系的管理,确保项目环境的稳定性和可重复性。它主要包含两个核心工具:pip-compile 和 pip