区分接口继承和实现继承——条款34

2023-10-29

        表面上直截了当的public继承概念,经过严密的检查之后,发现它由两部分组成:函数接口(function interfaces)继承函数实现(function implementations)继承。这两种继承的差异,很像本书导读所讨论的函数声明与函数定义之间的差异。
       身为class设计者,有时候你回希望derived classes只继承成员函数的接口(也就是声明);有时候你又希望derived classes同时继承函数的接口和实现,但又希望能够覆写(override)它们所继承的实现;有时候你希望derived classes同时继承函数的接口和实现,并且不允许覆写任何东西。
        为了更好地感觉上述选择之间的差异,让我们考虑一个展现绘图程序中各种几何形状的class继承体系:

class Shape {
    public:
    virtual void draw() const = 0;
    virtual void error(const std::string& msg);
    int objectID() const;
    ...
};
class Rectangle: public Shape { ... };
class Ellipse: public Shape { ... };

        Shape是个抽象class;它的pure virtual函数draw使它成为一个抽象class。所以客户不能够创建Shape class的实体,只能创建其derived classes的实体。尽管如此,Shape还是强烈影响了所有以public形式继承它的derived classes,因为:

  • 成员函数的接口总是会被继承。一如条款32所说,public继承意味is-a(是一种),所以对base class为真的任何事情一定也对其derived classes为真。因此如果某个函数可施行于某class身上,一定也可施行于其derived class身上。

        Shape class声明了三个函数。第一个是draw,于某个隐喻的视屏中画出当前对象。第二个是error,准备让那些“需要报导某个错误”的成员函数调用。第三个是objectID,返回当前对象的一个独一无二的整数识别码。每个函数的声明方式都不相同:draw是个pure virtual函数;error是个简朴的(非纯)impure virtual函数;objectID是个non-virtual函数。这些不同的声明带来什么样的暗示呢?

首先考虑pure virtual函数draw:

class Shape {
    public:
    virtual void draw() const = 0;
    ...
};

        pure virtual函数有两个最突出的特性:它们必须被任何“继承了它们”的具象class重新声明,而且它们在抽象class中通常没有定义。把这两个性质摆在一起,你就会明白:

  • 声明一个pure virtual函数的目的是为了让derived classes只继承函数接口。

        这对Shape::draw函数是再合理不过对事了,因为所有Shap对象都应该是可以绘出的,这是合理的要求。但Shape class无法为此函数提供合理的缺省实现,毕竟椭圆形绘法迥异于矩形绘法。Shape::draw的声明式乃是对具象derived classes设计者说,“你必须提供一个draw函数,但我不干涉你怎么实现它。”

        令人意外的是,我们竟然可以为pure virtual函数提供定义。也就是说你可以为Shape::draw供应一份实现代码,C++并不会发出怨言,但调用它的唯一途径是“调用时明确指出其class名称”:

Shape *ps = new Shape;          // 错误!Shape是抽象的
Shape *ps1 = new Rectangle;     // 没问题
ps1->draw();                    // 调用Rectangle::draw
Shape *ps2 = new Ellipse;       // 没问题
ps2->draw();                    // 调用Ellipse::draw
ps1->Shape::draw();             // 调用Shape::draw
ps2->Shape::draw();             // 调用Shape::draw

         简朴的impure virtual函数背后的故事和pure virtual函数有点不同。一如往常,derived classes继承其函数接口,但impure virtual函数会提供一份实现代码,derived classes可能覆写(override)它。稍加思索,你就会明白:

  • 声明简朴的(非纯)impure virtual函数的目的,是让derived classes继承该函数的接口和缺省实现。

        考虑Shape::error这个例子:

class Shape {
public:
	virtual void error(const std::string& msg);
	...
};

        Shape::error的声明式告诉derived classes的设计者,“你必须支持一个error函数,但如果你不想自己写一个,可以使用Shape class提供的缺省版本”。

        但是允许impure virtual函数同时指定函数声明和函数缺省行为,却有可能造成危险。欲探讨原因,让我们考虑XYZ航空公司设计的飞机继承体系。该公司只有A型和B型两种飞机,两者都以相同方式飞行。因此XYZ设计出这样的继承体系:

class Airport { ... };    // 用以表现机场
class Airplane {
	public:
	virtual void fly(const Airport& destination);
	...
};
void Airplane::fly(const Airport& destination)
{
	// 缺省代码,将飞机飞至指定目的地
}
class ModelA:public Airplane { ... };
class ModelB:public Airplane { ... };

        为了表示所有飞机都一定能飞,并阐明“不同型飞机原则上需要不同的fly实现”,Airplane::fly被声明为virtual。然而为了避免在ModelA和ModelB中撰写相同的代码,缺省飞行行为由Airplane::fly提供,它同时被ModelA和ModelB继承。

        这是个典型的面向对象设计。两个classes共享一份相同性质(也就是它们实现fly的方式),所以共同性质被搬到base class中,然后被这两个classes继承。

        现在,假设XYZ盈余大增,决定购买一种新式C型飞机。C型和A型以及B型有某些不同。更明确地说,它的飞行方式不同。

         XYZ公司的程序员在继承体系中针对C型飞机添加了一个class,但由于他们急着让新飞机上线服务,竟忘了重新定义其fly函数:

class ModelC:public Airplane {
	...        // 未声明fly函数
};

        然后代码中有一些诸如此类的动作:

Airport PDX(...);               // PDX是我家附近的机场
Airplane* pa = new ModelC;
... 
pa->fly(PDX);                   // 调用Airplane::fly

        这将酿成大灾难;这个程序试图以ModelA或ModelB的飞行方式来飞ModelC。问题不在Airplane::fly有缺省行为,而在于ModelC在未明白说出“我要”的情况下就继承了该缺省行为。幸运的是我们可以轻易做到“提供缺省实现给derived classes,但除非它们明白要求,否则免谈”。此间伎俩在于切断“virtual 函数接口”和其“缺省实现”之间的连接。下面是一种做法:

class Airplane {
public:
	virtual void fly(const Airport& destination) = 0;
	...
protected:
	void defaultFly(const Airport& destination);
};
void Airplane::defaultFly(const Airport& destination)
{
	// 缺省行为,将飞机飞至指定目的地
}

        请注意,Airplane::fly已被改为一个pure virtual函数,只提供飞行接口。其缺省行为也会出现在Airplane class中,但此次系以独立函数defaultFly的姿态出现。若想使用缺省实现(例如ModelA和ModelB),可以在其fly函数中对defaultFly做一个inline调用(但请注意条款30所言,inline和virtual函数之间的交互关系):

class ModelA:public Airplane {
	public:
	virtual void fly(const Airport& destination)
	{ defaultFly(destination); }
	...
};
class ModelB:public Airplane {
	public:
	virtual void fly(const Airport& destination)
	{ defaultFly(destination); }
	...
};

        现在ModelC class不可能意外继承不正确的fly实现代码了,因为Airplane中的pure virtual函数迫使ModelC必须提供自己的fly版本:

class ModelC:public Airplane {
	public:
	virtual void fly(const Airport& destination);
	...
};
void ModelC::fly(const Airport& destination)
{
	// 将C型飞机飞至指定目的地
}

        这几乎和前一个设计一模一样,只不过pure virtual函数Airplane::fly替换了独立函数Airplane::defaultFly。本质上,现在的fly被分割为两个基本要素:其生命部分表现的是接口(那是derived classes必须使用的),其定义部分则表现出缺省行为(那是derived classes可能使用的,但只有它们明确提出申请时才是)。如果合并fly和defaultFly,就丧失了“让两个函数享有不同保护级别”的机会;习惯上被设为protected的函数(defaultFly)如今成了public(因为它在fly之中)。

        最后,让我们看看Shape的non-virtual函数objectID:

class Shape {
public:
	int objectID() const;
	...
};

        如果成员函数是个non-virtual函数,意味是它并不打算在derived classes中有不同的行为。实际上一个non-virtual成员函数的不变性凌驾其特异性,因为它表示不论derived class变得多么特异化,它的行为都不可以改变。就其自身而言:

  • 声明non-virtual函数的目的是为了令derived classes继承函数的接口及一份强制性实现。

        pure virtual函数、impure virtual函数、non-virtual函数之间的差异,使你得以精确指定你想要derived classes继承的东西:只继承接口,或是继承接口和一份缺省实现,或是继承接口和一份强制实现。由于这些不同类型的声明意味根本意义不相同的事情,当你声明你的成员函数时,必须谨慎选择。如果你确实履行,应该能够避免经验不足的class设计者最常犯的两个错误。

        第一个错误是将所有函数声明为non-virtual。这使得derived classes没有余裕的空间进行特化工作。non-virtual析构函数尤其会带来问题(见条款7)。如果你关系virtual函数的成本,请容许我介绍所谓的80-20法则(也可见条款30)。这个法则说,一个典型的程序有80%的执行时间花费在20%的代码身上。此法则十分重要,因为它意味着,平均而言你的函数调用中可以有80%是vvirtual而不冲击程序的大体效率。所以当你担心是否有能力负担virtual函数的成本之前,请先将心力放在那举足轻重的20%代码上头,它才是真正的关键。

        第二个常见错误是将所有成员函数声明为virtual。有时候这样做是正确的,例如条款31的interface classes。然而这也可能是class设计者缺乏坚定立场的前兆。某些函数就是不该在derived class中被重新定义,果真如此你应该将那些函数声明为non-virtual。

请记住

  • 接口继承和实现继承不同。在public继承之下,derived classes总是继承base class的接口。
  • pure virtual函数只具体指定接口继承。
  • 简朴的(非纯)impure virtual函数具体指定接口继承即缺省实现继承。
  • non-virtual函数具体指定接口继承以及强制性实现继承。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

区分接口继承和实现继承——条款34 的相关文章

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

    目录 一 传值效率可能会很低 二 传值可能发生割裂问题 三 适用于传值的情况和注意事项 相关博客 C 引用知识归纳 一 传值效率可能会很低 我们假设有这样两个类 class Human public string name string s
  • Kotlin中继承、类型转换、Any超类、object关键字详解

    博主前些天发现了一个巨牛的人工智能学习网站 通俗易懂 风趣幽默 忍不住也分享一下给大家 点击跳转到教程 一 继承 类型转换 Any超类 继承 Kotlin中类默认都是封闭的 要让某个类开放继承 必须使用open关键字修饰它 open cla
  • C++派生类的不同继承方式对基类的访问权限

    经过我细心的整理 形成了这张表 一张表说明派生类的不同继承方式 对基类的访问权限 总的来说 对类的访问权限范围public
  • Python 在子类中调用父类方法详解(单继承、多层继承、多重继承)

    测试环境 win7 64位 Python版本 Python 3 3 5 代码实践 1 在子类中通过 类名 调用父类的方法 class FatherA def init self print init action in father cla
  • 《Effective C++》学习笔记——区分接口继承和实现继承

    派生类public继承自基类 其中函数均是接口继承 实现继承又分为缺省继承与强制继承 对应着虚函数与非虚函数 我们在做项目时 对于任何事物都要抱有先描述再组织的心态 因此 当描述事物为其构建框架尤其是存在继承 is a 关系时 一定要搞清楚
  • 什么是多态?如何实现?只看这一篇就够了

    1 多态的概念 1 1 概念 多态的概念 通俗来说 就是多种形态 具体点就是去完成某个行为 当不同的对象去完成时会产生出不同的状态 2 多态的定义及实现 2 1多态的构成条件 多态是在不同继承关系的类对象 去调用同一函数 产生了不同的行为
  • 尽量以const、enum、inline替换 #define——条款02

    这个条款或许改为 宁可以编译器替换预处理器 比较好 因为或许 define 不能被视为语言的一部分 一 比如定义一个宏 define ASPECT RATIO 1 653 这个ASPECT RATIO也许从未被编译器看见 也许在编译器开始处
  • 将文件间的编译依存关系降至最低——条款31

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

    结论 为防止资源泄漏 请使用RAII对象 它们在构造函数中获得资源并在析构函数中释放资源 两个常被使用的RAII classes分别是tr1 share ptr和auto ptr 前者通常是较佳选择 因为其copy行为比较直观 若选择aut
  • 多重继承和多继承, super, __mro__

    继承 父类派生子类 子类继承父类 通过继承 可以让子类去拥有父类中的属性和方法 而不必重新编写相同的代码 并且可以在父类的基础上添加新的属性和功能 在继承的同时 子类还可以重写父类中的方法 从而获取与父类不同的功能 实现多态 在 Pytho
  • Effective C++ 条款十二:复制对象时勿忘其每一个成分

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

    继承与组合混搭情况下 构造和析构调用原则 先说结论 原则 先构造父类 再构造成员变量 最后构造自己 先析构自己 在析构成员变量 最后析构父类 注 先构造的对象 后释放 class my 创建一个成员类 public int a my int
  • line-height 百分比和数值设置行高

    一 line height的值为百分比 子集元素继承父级元素的距离 1 例如
  • Objective-C中的继承与多态, Category, Extension

    1 继承与多态 先要理解实例变量的作用域 再看继承 即 子类的方法和属性 从父类继承得到的方法和属性 子类新增的方法和属性 例子 Person h import
  • C++类的三大特性之继承

    目录 一 继承的概念与使用 lt 1 gt 什么是继承 lt 2 gt 如何使用 二 基类与派生类间的转换 三 继承的作用域 四 派生类的默认成员函数 lt 1 gt 构造函数 lt 2 gt 拷贝构造 lt 3 gt 赋值运算符重载 lt
  • 2022-04-20 Sass学习笔记(四) Sass的混入(mixin),继承(extend)和导入(import)

    1 Sass混入 mixin 与 include mixin 指令允许我们定义一个可以在整个样式表中重复使用的样式 include 指令可以将混入 mixin 引入到文档中 语法 定义 mixin mixin name 使用 selecto
  • CSS之继承

    1 什么是css继承 继承是css中非常重要的一个概念 当你为HTML中的某个元素赋予CSS样式时 这些样式不仅仅会影响当前元素 有的样式还会影响该元素的子元素 这些下层子元素继承上层祖先元素样式属性的特点 就称为css继承 2 css继承
  • 写了placement new也要写placement delete——条款52

    placement new和placement delete并非C 兽栏中最常见的动物 如果你不熟悉它们 不要感到挫折或忧虑 回忆条款16和17 当你写一个new表达式像这样 Widget pw new Widget 共有两个函数被调用 一
  • Java面向对象三大特性:继承、封装、多态

    面向对象编程 一 继承 1 表现形式 A extends B 2 子类继承了父类的什么 BAT 面试 3 this 和 super 关键字的区别 面试 4 Java 中访问权限修饰符 5 重写 与 重载的区别 面试 6 final 的用法
  • 明智而审慎地使用多重继承——条款40

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

随机推荐

  • 修改Jenkins以Root用户运行

    简单操作如下 vim etc sysconfig jenkins JENKINS USER root chown R root root var lib jenkins chown R root root var cache jenkins
  • Linux查看文件及文件夹大小

    du sh 查看当前目录下各个文件及目录占用空间大小 du sh 查看当前目录的总大小 df h 查看系统中文件的使用情况 Size 分割区总容量 Used 已使用的大小 Avail 剩下的大小 Use 使用的百分比 Mounted on
  • Uiautomator2

    https github com openatx uiautomator2 官方文档 第一步 先准备一台开启了开发者选项的安卓手机 连接上电脑 确保执行adb devices可以看到连接上的设备 不要开启charles 否则会导致下载失败
  • Window触发器和Delta触发器在大数据处理中的应用

    大数据处理是指处理海量数据的技术和方法 在大数据处理中 窗口触发器 Window Trigger 和Delta触发器 Delta Trigger 是常用的工具 用于按照一定的规则触发数据处理操作 本文将介绍这两种触发器的概念 应用场景 并给
  • 使用java实现http多线程下载

    下载工具我想没有几个人不会用的吧 前段时间比较无聊 花了点时间用java写了个简单的http多线程下载程序 纯粹是无聊才写的 只实现了几个简单的功能 而且也没写界面 今天正好也是一个无聊日 就拿来写篇文章 班门弄斧一下 觉得好给个掌声 不好
  • linux下通过V4L2驱动USB摄像头

    目录 文章目录 目录 前言 v4l2 解析 v4l2 介绍 应用程序通过 V4L2 接口采集视频数据步骤 相关结构体解析 总结 参考链接 前言 在移植罗技C270摄像头到6818的过程中 内核已经检测到了USB摄像头 但是直接用OpenCV
  • /proc/sys/kernel/hung_task_timeout_secs问题

    具体的问题如下 判定是磁盘写入的问题 正在找照成文件卷hung的原因
  • 一维码和二位码主要原理

    1 条码主要分类 Code39码 标准39码 Codabar码 库德巴码 Code25码 标准25码 ITF25码 交叉25码 Matrix25码 矩阵 25码 UPC A码 UPC E码 EAN 13码 EAN 13国际商品条码 EAN
  • EEPROM读写测试实验

    EEPROM是一种用于计算机系统的非易失性存储器 也常在嵌入式领域中作为数据的存储设备 在物联网及可穿戴设备等需要存储少量数据的场景中也有广泛应用 实验任务 本节的实验任务是先向EEPROM AT24C64 的存储器地址0至255分别写入数
  • MongoDB 使用总结

    简介 java系列技术分享 持续更新中 初衷 一起学习 一起进步 坚持不懈 如果文章内容有误与您的想法不一致 欢迎大家在评论区指正 希望这篇文章对你有所帮助 欢迎点赞 收藏 留言 更多文章请点击 文章目录 一 MongoDB简介 二 Mon
  • 臻识科技用全智能相机,把智慧城市的交通/安防/工业制造做到极致

    俨然 智慧城市已经是一个技术密集 资本密集 巨头密集 关注度密集的大热门领域 从技术层面来看 智慧城市对当下热门技术进行了综合 Cloud Big Data AI AR VR 5G IoT Quantum Computing Edge Co
  • 极域课堂管理系统软件V6.0 2016 豪华版

    百度网盘链接地址 https pan baidu com s 1ZXClL84 iFl8klR3Kme5 w 地址链接失效请及时联系本人 QQ 395648542
  • 超实用!深度比较Python对象之间的差异

    本文完整示例代码及文件已上传至Github仓库https github com CNFeffery PythonPracticalSkills 很多情况下我们需要对两条数据之间的差异进行比较 如果仅仅是针对数值型对象 那么两者的差值就是所谓
  • 面试经:一线城市搬砖,又面软件测试岗,5000就知足了...

    今天有个大专生来我公司面试软件测试 他说在 地下城 64开搬砖 一个月能赚7万多 就在上星期 所有的号全被封了 所以来公司上班了 目前有一年多软件测试工作经验 来面试的这个大专生他的自我介绍是这样的 他说 学历大专 大专学的专业是软件技术
  • 《数学建模实战攻略》

    专栏策划 一 目标受众 数学建模实战攻略 面向数学建模初学者 参加数学建模竞赛的学生以及对数学建模有兴趣的研究者和开发者 二 专栏目录 引言 专栏简介与目标 数学建模的重要性及应用领域 数学建模基本概念与方法论 问题抽象与建模过程 常见数学
  • Linux中断原理、上半部和下半部、硬中断和软中断

    目录 1 中断简介 1 1 作用 1 2 物理实现 1 3 中断请求线IRQ 1 4 异常 2 中断处理程序 2 1 作用 2 2 上半部和下半部 2 3 中断上下文 3 中断系统 3 1 中断机制的实现 3 2 中断控制 4 下半部和软中
  • python skimage图像处理(一)

    本文转自 python数字图像处理 基于python脚本语言开发的数字图片处理包 比如PIL Pillow opencv scikit image等 PIL和Pillow只提供最基础的数字图像处理 功能有限 opencv实际上是一个c 库
  • mipi dsi接口_Camera MIPI接口详解2

    简介 上一篇文章中 我们简单的介绍了camera接口的类型 有串口和并口和LVDS接口 以及MIPI接口一些电气特性的一些简单的技术探讨 那么我们现在常用的都是mipi接口 需要深入一点去理解MIPI接口的电气特性 有助于我们接下来理解MI
  • 类脑导航的机理、算法、实现与展望

    类脑导航 CBN 是一种新型的导航方式 其机理基于对大脑和动物行为的理解 与传统导航系统不同的是 CBN借鉴了大脑神经元与突触的工作原理 通过人工神经网络学习和模拟动物的行为 使导航过程更加具有灵活性和适应性 CBN涉及到的算法主要是基于机
  • 区分接口继承和实现继承——条款34

    表面上直截了当的public继承概念 经过严密的检查之后 发现它由两部分组成 函数接口 function interfaces 继承和函数实现 function implementations 继承 这两种继承的差异 很像本书导读所讨论的函