Effective C++条款34:区分接口继承和实现继承

news2024/11/9 0:03:20

Effective C++条款34:区分接口继承和实现继承(Differentiate between inheritance of interface and inheritance of implementation)

  • 条款34:区分接口继承和实现继承
    • 1、纯虚函数
    • 2、虚函数(非纯)
      • 2.1 将默认实现分离成单独函数
      • 2.2 利用纯虚函数提供默认实现
    • 3、普通成员函数(非虚)
    • 4、class设计者常犯的两个错误
      • 4.1 第一个错误
      • 4.2 第二个错误
    • 5、牢记
  • 总结


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

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

在这里插入图片描述


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

  public继承由两部分组成:函数接口继承和函数实现继承。当我们设计类时,对于基类的成员函数可以大致做下面三种方式的处理:

  • ①纯虚函数:基类定义一个纯虚函数,然后让派生类去实现。

  • ②非纯虚的virtual虚函数:基类定义一个非纯虚的virtual虚函数,然后让派生类去重写覆盖(override)。

  • ③普通的成员函数:基类定义一个普通的成员函数,并且不希望派生类去隐藏。

  为了对这些不同的选择有一个更好的理解,考虑表示几何图形的类继承体:系:

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 { ... };

1、纯虚函数

  首先考虑纯虚函数draw:

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

  纯虚函数的两个最具特色的特征是:它们必须被继承它们的任何具象类重新声明;在抽象类中它们通常情况下没有定义。将这两个特征放在一起,你就会发现:

  • 声明纯虚函数的目的是让派生类只继承函数接口

  这对Shape::draw函数是再合理不过的事了,因为所有的Shape对象来说都是能够画出的,这是一个合理的需要,但是Shape类不能为这个函数提供合理的缺省实现,比如,画一个椭圆的算法和画一个矩形的算法是不一样的。Shape::draw的声明对派生具现类的设计者说,“你必须提供一个draw函数,但是我并不知道你该如何实现它。”

  我们可以为一个纯虚函数提供一个定义。也就是你可以为Shape::draw提供一个实现,C++不会发出抱怨,但是调用它的唯一方式是在函数名前加上类名限定符:

Shape *ps = new Shape;     // error! 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

  这项性质除了能给别人留下一个深刻的印象外,用途有限。

2、虚函数(非纯)

  简单虚函数背后的故事同纯虚函数有些不太一样。通常,派生类继承函数接口,但是虚函数会提供一份实现代码,派生类可能覆写它。

  • 声明一个简单虚函数的目的是让派生类继承一个函数接口和缺省实现。

  考虑Shape::error这个例子:

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

  这个接口表示,每个class都必须支持一个“当遇到错误时可调用的函数”,但是每个类对错误如何进行自由的处理。如果一个类不想做任何特殊的事情,那么调用基类Shape中error的默认实现就可以了。也就是Shape::error的声明对派生类的设计者说,“你可以支持error函数,但如果你不想自己实现,你可以使用Shape类中的默认版本。”

  先来看一个虚函数的演示案例,假设某航天公司设计一个飞机继承体系,该公司现在只有A型和B型两种飞机,代码如下:

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函数,还有不同型号的飞机可能需要fly的不同实现,因此Airplane::fly被声明为virtual。然而,为了避免在ModelA和ModelB中实现同一份代码,我们为Airplane::fly提供了默认实现,ModelA和ModelB可以同时继承。

  这是典型的面向对象设计。两个类共享同一个特征(实现fly的方式),所以一般的特征都会移到基类中,然后被派生类继承。这种设计使得类的普通特性比较清晰,防止代码重复,可以促进将来的增强实现,使长期维护更加容易——这是面向对象如此受欢迎的原因。

  现在假设XYZ公司界定购入新式C型飞机,型号C和型号A和B不一样,具体说是,它的飞行方式变了。

  XYZ的程序员为Model C在继承体系中添加了新类,但是他们如此匆忙的添加新类,以至于忘了重新定义fly函数:

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

  然后代码中有这些动作:

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

  这会是一个灾难:型号C的飞机尝试用型号A或者型号B的飞行方式去飞行。这不是增加旅客信心的行为。

2.1 将默认实现分离成单独函数

  问题不在于Airplane::fly有默认的行为,而在于允许 Model C在没有明确说明它需要基类行为的情况下继承了基类的行为。幸运的是,很容易为派生类提供只有在它们需要的情况下才为其提供的默认行为,这种技术在于切断“virtual函数”和其“默认实现”之间的连接。代码如下:

class Airplane {
public:
    virtual void fly(const Airport& destination) = 0;
    ...
protected:
    void defaultFly(const Airport& destination);
};
void Airplane::defaultFly(const Airport& destination) {
	//飞机飞往指定的目的地(默认行为)
}

  注意,在A和B的类的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);
    }
    ...
};

  现在C型飞机,或者别的添加的飞机就不会意外继承默认的飞行行为了(因为我们将默认的飞行行为封装到一个defualtFly函数中了),自己可以在fly中定义飞行行为了

class ModelC :public Airplane {
public:
    virtual void fly(const Airport& destination);
};

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

  Airplane::defaultFly是一个非虚函数同样重要。因为没有派生类可以重定义这个函数,如果defaultFly是虚函数,就会有一个循环问题:万一某些派生类忘记重新定义defaultFly,会怎样?

2.2 利用纯虚函数提供默认实现

  有人反对以不同的函数分别提供接口和缺省实现,像上面我们将fly()接口和实现(defaultFly()函数)分开来实现,有些人可能会反对这样做,因为这样会因过度雷同的函数名称而引起class命名空间污染。

  如果不想将上述两个行为分开,那么可以为纯虚函数进行定义,在其中给出defaultFly()函数的相关内容。例如:

class Airplane {
public:
    //实现纯虚函数
    virtual void fly(const Airport& destination) = 0;
    ...
};
void Airplane::fly(const Airport& destination) {// 纯虚函数实现
	//缺省(默认)行为,将飞机飞至指定的目的地
}

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

  这几乎和前一个设计一模一样,只不过在派生类的fly()函数中用纯虚函数Airplane::fly替换了独立函数Airplane::defaultFly。这种合并行为丧失了“让两个函数享有不同保护级别”的机会:例如上面的defaultFly()函数从protected变为了public(因为它在fly之中)。

3、普通成员函数(非虚)

  最后,看看 Shape 的非虚函数objectID:

class Shape {
public:
    int objectID()const; //普通成员函数,不希望派生类隐藏
};
 
class Rectangle :public Shape {};
class Ellipse :public Shape {};

如果成员函数是个非虚函数:

  • 意味是它并不打算在派生类中有不同的行为。

  • 实际上一个普通的成员函数所表现的不变性凌驾其特异性,因为它表示不论派生类变得多特特异化,它的行为都不可以改变。

声明一个非虚函数的目的在于让派生类继承一个函数接口,并且有一个强制的实现,

你可以把Shape::objectID的声明想做是:

  • 每个Shape对象都有一个用来产生对象识别码的函数,此识别码总是采用相同计算方法,该方法有Shape::objectID的定义式决定,任何派生类都不应该尝试改变其行为

  • 由于非虚函数代表的意义是不变性凌驾特异性,所以它绝不该在派生类中被重新定义(这也是条款36所讨论的一个重点)

4、class设计者常犯的两个错误

  “纯虚函数、非纯虚的virtual虚函数、非虚函数”之间的差异,使得指定你想要派生类继承的东西:只继承接口,或是继承接口和一份缺省实现,或是继承接口和一份强制实现。针对于不同的函数,经验不足的class设计者最常犯的两个错误如:

4.1 第一个错误

  第一个错误是将所有函数声明为“non-virtual”,这使得派生类没有多余空间进行特化工作。

  non-virtual析构函数尤其会带来问题(见条款7)。

  当然,如果一个类不打算作为基类,那么将所有函数声明为“non-virtual”是可以的。但是如果该类会作为基类,那么可以适当的声明一些virtual函数(见条款7)。

  如果你当心virtual函数的成本,那么可以参阅80-20法则(也可参阅条款30):

  • 这个法则为:一个典型的程序有80%的执行时间花费在20%的代码身上。

  • 这个法则意味着,平均而言你的函数调用中可以有80%是virtual而不冲击程序的大体效率。所以当你担心virtual函数的成本之前,先将精力放在那举足轻重的20%代码上,它才是真正的关键。

4.2 第二个错误

  第二个错误是将所有成员函数声明为virtual。

  有时候这样做是正确的,例如条款31的Interface classes。然而某些函数就是不该在派生类中被重新定义,因此你应该将那些函数声明为non-virtual的。

5、牢记

  • 接口继承和实现继承不同。在public继承之下,derived classes总是继承base class的接口。

  • pure virtual函数只具体指定接口继承。

  • 简朴的(非纯)impure virtaul函数具体指定接口继承及缺省实现继承。

  • non-virtual函数具体指定接口继承以及强制性实现继承。

总结

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/108663.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2022 IoTDB Summit:中国核电刘旭嘉《工业时序数据库 Apache IoTDB 在核电的应用实践》...

12 月 3 日、4日,2022 Apache IoTDB 物联网生态大会在线上圆满落幕。大会上发布 Apache IoTDB 的分布式 1.0 版本,并分享 Apache IoTDB 实现的数据管理技术与物联网场景实践案例,深入探讨了 Apache IoTDB 与物联网企业如何共建活跃生态&#…

middlebury立体匹配评估使用方法总结(三)——线上版教程

系列文章目录 middlebury立体匹配评估使用方法总结(一)——网站说明 middlebury立体匹配评估使用方法总结(二)——python版离线教程 middlebury立体匹配评估使用方法总结(三)——线上版教程 文章目录系列文…

TableLayout布局

表格布局-TableLayout 1.TableLayout简介 1.简介 表格的形式,整齐可以嵌套继承于线性布局2.行数如何确定? tableRow,来指定行数列数由最多的那个决定layout_column来指定具体的列数,从0开始2.TableLayout的常见属性 所有的都是从0…

VMware ESxi 服务器迁移【手动版】

VMware ESxi 迁移【手动版】 应用场景 两个不同环境下的服务器进行迁移 因为不能直接对拷,需要在中间机上转一下 才有了这么一出 第一步 搭建NFS 在中间机上安装NFS(或者其他磁盘挂载方式) 目的呢是把源服务器上的系统拷贝到中间机上&#x…

android入门之broadcast

1. 前言 广播Broadcast是android四大组件之一。是用来互相通信(传递信息)的一种机制。 通信包括: a) 组件间(应用内)通信 b) 进程间通信 2. 广播Brocast的基本使用方式 广播发送者:Acvitity、Service等…

pdf文档页码怎么添加?分享这几个pdf加页码方法给你

不管是还在校园里的学生,还是已经步入职场的小伙伴,都会遇到要对一些文档进行编辑处理,例如有时需要将word、excel、ppt等格式的文档与pdf文件进行相互转换,有时又需要对pdf文件进行编辑文档增加页眉页脚、拆分合并、加密解密等操…

基于Python+Echarts+Pandas 搭建一套图书分析大屏展示系统(附源码)

今天给大家分享的是基于 Flask、Echarts、Pandas 等实现的图书分析大屏展示系统。 项目亮点 采用 pandas、numpy 进行数据分析 基于 snownlp、jieba 进行情感分析 后端接口选用 RESTful 风格,构建 Swagger 文档 基于 Flask、Echarts 构建 Web 服务,采…

2022年债券估值工具和方法

第一章 债券估值原理概述 债券估值是决定债券公平价格[1]的过程。债券公平价格是债券的预期现金流经过合适的折现率折现以后的现值,其原理是未来现金流流出折现到今日与今日现金流流出相等。因此,债券的估值模型可以表示为: 资料来源&#x…

新冠阳性的第四篇博客,SpringBoot 任务(异步、定时、邮件)

新冠阳性的第四篇博客,SpringBoot 任务(异步、定时、邮件)1.异步任务2.邮件任务3.定时任务1.异步任务 异步处理还是非常常用的,比如我们在网站上发送邮件,后台会去发送邮件,此时前台会造成响应不动&#x…

Python多元线性回归、机器学习、深度学习在近红外光谱分析中的应用

导师:郁磊副教授,主要从事MATLAB 编程、机器学习与数据挖掘、数据可视化和软件开发、人工智能近红外光谱分析、生物医学系统建模与仿真,具有丰富的实战应用经验,主编《MATLAB智能算法30个案例分析》、《MATLAB神经网络43个案例分析…

【Vue】二、 认识Vue.js的各种指令

后端程序员的vue学习之路1、创建第一个vue对象2、vue构造器3、Vue.js模板语法v-text至v-for练习v-on指令练习v-bind指令练习v-model指定练习v-pre指令v-slot指令v-cloak指令v-once指令1、创建第一个vue对象 引入了vue.js后,在页面就可以创建一个Vue对象&#xff0c…

JS圣诞树

✅作者简介:热爱国学的Java后端开发者,修心和技术同步精进。🍎个人主页:Java Fans的博客🍊个人信条:不迁怒,不贰过。小知识,大智慧。💞当前专栏:前端案例分享…

this指向问题,apply,call,bind用法及区别

1.谁调用我,我就指向谁。 在页面上直接打印一个consle.log(this),这个this会指向window对象。如果写一个函数:打印this,该this会指向window。因为这个函数是挂载在这个window对象上的。对象obj的this指向的是对象,因为…

[ 漏洞挖掘基础篇五 ] 漏洞挖掘之 XSS 注入挖掘

🍬 博主介绍 👨‍🎓 博主介绍:大家好,我是 _PowerShell ,很高兴认识大家~ ✨主攻领域:【渗透领域】【数据通信】 【通讯安全】 【web安全】【面试分析】 🎉点赞➕评论➕收藏 养成习…

创新指南|2023年企业战略制定应避免的5大误区

在迅速发展、不断变化的当下,尤其是疫情黑天鹅发生之后,许多企业面临着高度的不确定性,从而亟需进行企业战略的思考。在本文中,战略专家Stephen Bungay指出了五个战略误区,并解释了它们为什么听起来正确,以…

基于JAVA技术的《物联网技术》课程学习网站设计与实现

开发工具(eclipse/idea/vscode等): 数据库(sqlite/mysql/sqlserver等): 功能模块(请用文字描述,至少200字): 基于JAVA技术的《物联网技术》课程学习网站设计与实现 网站前台:关于我们、联系我们、公告信息、资料信息&a…

实现Kafka至少消费一次

实现Kafka至少消费一次默认的kafka消费者存在什么问题?实现至少消费一次加入重试队列再次消费使用seek方法再次消费在实际重要的场景中,常常需要实现消费者至少消费一次。因为使用默认的kafka消费者存在某些问题。 默认的kafka消费者存在什么问题&#x…

Django+DRF+Vue+Mysql+Redis OUC软件工程作业

交作业啦 前端:htmlcssjsVueElement-ui 后端:DjangoDRFceleryhaystackdjango_crontab 数据库:MysqlRedis 一些技术和功能: 为session、短信验证码、用户浏览记录、购物车、异步任务队列 创建缓存whoosh搜索引擎异步任务队列 用…

谷歌Recorder实现说话人自动标注,功能性与iOS语音备忘录再度拉大

在今年的 Made By Google 大会上,谷歌公布了 Recorder 应用的自动说话人标注功能。该功能将实时地为语音识别的文本加上匿名的说话人标签(例如 “说话人 1” 或“说话人 2”)。这项功能将极大地提升录音文本的可读性与实用性。 谷歌于 2019 …

Spring Cloud Alibaba Sentinel - - >流控规则初体验

源码地址:https://github.com/alibaba/Sentinel 新手指南:https://github.com/alibaba/Sentinel/wiki/新手指南#公网-demo 官方文档:https://sentinelguard.io/zh-cn/docs/introduction.html 注解支持文档:https://github.com/ali…