区分接口继承和实现继承——条款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 的相关文章

  • javascript判断对象有没有某个属性

    定义一个动物类 function Animal name 属性 this name name Animal 实例方法 this sleep function console log this name 正在睡觉 Animal prototy
  • 确定你的public继承塑模出is-a关系——条款32

    如果你令class D Derived 以public形式继承class B Base 你便是告诉C 编译器 以及你的代码读者 说 每一个类型为D的对象同时也是一个类型为B的对象 反之不成立 你的意识是B比D表现出更一般化的概念 而D比B表
  • Java学习笔记之继承(1)

    提到继承 大家可能第一时间会想到继承遗产 Java里的继承虽然不是继承钱 但是也和继承遗产有相似之处 继承遗产一般是说子女辈获得了父辈留下的钱财 物品等 java里的继承是指子类获得了和父类一样的属性 1 类的继承格式 class 父类 q
  • 快速创建一个servlet并且在web.xml配置和使用它

    这次 我要来教大家怎么快速创建一个servlet并且在web xml配置和使用它 实际上 现在可以直接在eclipse中创建一个servlet使其继承HttpServlet 而且你还可以对其进行一定的配置 在图中红色的地方写上你的Servl
  • 807-C++多继承下,派生类对象有几张虚函数表?

    C 多继承下 派生类对象有几张虚函数表 我们看下面这个示例 include
  • JAVA子类继承父类的成员变量以及方法

    Dog继承Animal class Animal protected String name protected String description protected String say return 一只动物 class Dog e
  • 【深入理解C++】调用父类的拷贝构造函数

    文章目录 1 默认的拷贝操作 2 调用父类的拷贝构造函数 3 用子类对象初始化父类对象 1 默认的拷贝操作 默认情况下 继承体系下类对象的拷贝是每个成员变量逐个拷贝 include
  • 面向对象-继承

    继承 概念 继承父类的属性和行为 使得子类对象可以直接具有与父类相同的属性 相同的行为 子类可以直接访问父类中的非私有的属性和行为 继承是多态的前提 如果没有继承 就没有多态 特点 java只能单继承 但可以多层继承 a继承b b继承c 那
  • JAVA-面向对象

    面向对象编程与面向过程编程只是一种在思维方式上的划分 面向过程是以分步骤的方式解决问题 而面向对象是以分步骤的方式解决问题 面向对象的三大特性是 封装 继承 多态 封装 就是将客观事物封装成抽象的类 抽象类可以将自己的数据和方法只让自己信任
  • Java 子类继承父类方法的重写(或者覆盖,override)

    1 子类重写父类方法的方法名 参数类型必须与父类被重写 被覆盖 的方法相同 2 子类方法的返回值类型必须小于等于父类被重写的方法的返回值类型 3 子类抛出的异常小于等于父类方法抛出的异常 4 子类的访问权限大于等于父类的访问权限 子类重写了
  • 【JavaSE系列】第八话 —— 继承和它的边角料们

    导航小助手 思维导图 一 引出继承 二 继承的概念 三 继承的语法 四 父类成员访问 4 1 子类中访问父类的成员变量 4 2 子类访问父类的成员方法 五 super 关键字 5 1 super 成员变量 5 2 super 成员方法 5
  • Effective C++ 学习笔记 《六》

    Item 6 Explicitly disallow the use of compiler generated functions you do not want 其实这一节的内容是和item5紧密相连的 上一节的核心围绕着编译器会自动生
  • 将与参数无关的代码抽离templates——条款44

    Templates是节省时间和避免代码重复的一个奇方妙法 不再需要键入20个类似的classes而每一个带有15个成员函数 你只需键入一个class template 留给编译器去具现化那20个你需要的相关classes和300个函数 cl
  • 继承中析构和构造的调用原则

    继承与组合混搭情况下 构造和析构调用原则 先说结论 原则 先构造父类 再构造成员变量 最后构造自己 先析构自己 在析构成员变量 最后析构父类 注 先构造的对象 后释放 class my 创建一个成员类 public int a my int
  • 考虑virtual函数以外的其他选择——条款35

    假设你正在写一个视频游戏软件 你打算为游戏内的人物设计一个继承体系 你的游戏术语暴力砍杀类型 剧中人物被伤害或因其他因素而降低健康状态的情况并不罕见 你因此决定提供一个成员函数healthValue 它会返回一个整数 表示人物的健康程度 由
  • 2022-04-20 Sass学习笔记(四) Sass的混入(mixin),继承(extend)和导入(import)

    1 Sass混入 mixin 与 include mixin 指令允许我们定义一个可以在整个样式表中重复使用的样式 include 指令可以将混入 mixin 引入到文档中 语法 定义 mixin mixin name 使用 selecto
  • 类的继承层次结构的宽度和深度

    最近在项目开发中 各位兄弟对于现有的架构有所诟病 主要是继承的问题 层次比较深 层次之间没有很明确的功能划分 造成一定的混乱 我来承担工作 想出一套新的方案 满足大家平时开发的需求 先总结下现在项目的问题 一个是层次深 一个是抽象的不好 大
  • C++虚函数解析

    C 中的虚函数的作用主要是实现了多态的机制 关于多态 简而言之就是 用父类型别的指针指向其子类的实例 然后通过父类的指针调用实际子类的成员函数 这种技术可以让父类的指针有 多种形态 这是一种泛型技术 所谓 泛型技术 说白了就是 试图使用不变
  • Java面向对象三大特性:继承、封装、多态

    面向对象编程 一 继承 1 表现形式 A extends B 2 子类继承了父类的什么 BAT 面试 3 this 和 super 关键字的区别 面试 4 Java 中访问权限修饰符 5 重写 与 重载的区别 面试 6 final 的用法
  • JavaSE进阶(一)—— 面向对象进阶(static、单例、代码块、继承)

    目录 一 static静态关键字 1 static是什么 static修饰成员变量的用法 2 成员方法的分类 2 1 使用场景 3 static修饰成员方法的内存原理 4 static的注意事项 拓展 二 static应用知识 工具类 1

随机推荐

  • 修改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 继承 这两种继承的差异 很像本书导读所讨论的函