Effective C++条款39:明智而审慎地使用private继承(Use private inheritance judiciously)

news2024/11/18 2:47:27

Effective C++条款39:明智而审慎地使用private继承(Use private inheritance judiciously)

  • 条款39:明智而审慎地使用private继承
    • 1、private 继承
    • 2、在private继承和复合之间做出正确选择
    • 3、使用private继承比组合更加合理的例子
    • 4、牢记
  • 总结


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

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

在这里插入图片描述


条款39:明智而审慎地使用private继承

1、private 继承

  条款32论证过C++如何把public继承视为”is-a”关系来。考虑一个继承体系,其中类Student public 继承自类Person,于是编译器为了让函数成功调用需要将Student隐式转换为Person,这时候“is-a”关系就出现了。现在重复一部分该例,并以private继承替换public继承:

class Person { ... };
class Student: private Person { ... };     // 改用private继承
	void eat(const Person& p);       // 任何人都可以吃
	void study(const Student& s); // 只有学生才在校学习
	Person p;                                  // p是人
	Student s;       // s 是学生
	eat(p);    // 没问题,p是人
	eat(s);    // error! 

  显然,private继承并不意味着“is-a”关系。那它意味着什么呢?

  我们观察一下private继承的行为。private继承的规则:与public继承相反,如果类之间的继承关系是private,编译器不会将派生类对象(Student)转换成为基类对象(Person)。这和public继承的情况不同。这也是为什么为对象s调用eat会失败。第二条规则是,由private基类继承而来的所有成员,在派生类中都会变成private属性,即使在基类中的成员是protected或者public的,从此基类中private继承而来的成员会变成派生类中的private成员。

  private继承意味着“is-implemented-in-terms-of”。如果你让类D private继承自类B,你的用意是因为你想利用类B中的一些让你感兴趣的性质,而不是因为在类型B和类型D之前有任何概念上的关系。private继承纯粹只是一种实现技术。(这也是为什么你从private基类中继承而来的任何东西在你的类中都变为了private的:所有的都只是实现上的细节。)借用条款34中引入的术语,private继承意味着只有实现部分被继承;而接口应该被忽略掉。如果类D private继承自类B,就意味着D对象的实现依赖于类B对象,没有别的含义了。private继承在软件实现层名才有意义,在软件设计层面是没有意义的。

2、在private继承和复合之间做出正确选择

  private继承意味着“is-implemented-in-terms-of”的事实会让你感觉有一些不安,因为条款38中指出复合(composition)也同样意味着“is-implemented-in-terms-of”。

  你应该怎么在它们之间做出取舍?答案是简单的:尽量使用复合(composition),在必须使用private继承的时候才去使用它。何时是必须使用?主要是当protected成员或者(和)虚函数被牵扯进来的时候,还有一种情况是,因为空间原因而不能使用private继承。

演示案例①:以public方式继承(错误做法)

  假设我们正在一个涉及到Widgets类的应用上工作,我们决定应该较好的了解如何使用Widgets。例如,我们不只想知道Widget成员函数的调用有多频繁,也想知道经过一段时间后调用比例如何变化。

  我们决定修改Widget类,让它记录每个成员函数的调用次数。在运行时,我们周期性地来审查这项信息,为了达到这个目的,我们会创建一个定时器于是我们可以知道什么时候去收集这些统计信息。

  我们更乐意去重用代码,尽量少写新代码,例如下面这个类:

class Timer {
public:
	explicit Timer(int tickFrequency);
	virtual void onTick() const;    // 定时器每滴答一次
	...                            //此函数就自动调用一次
};      

  这就是我们要找的。我们可以为这个Timer对象配置任意的tick频率,在每个tick发生的时候,它会调用一个虚函数。我们可以重定义这个虚函数来检查Widget世界的当前状态。完美!

  为了让Widget重定义Timer内的虚函数,Widget必须继承自Timer。但public继承是不合适的。因为Widget不是一个Timer。Widget客户不应该在一个Widget对象上调用onTick,因为onTick不是Widget的接口。并且允许这样的函数调用会使得客户很容易出现对Widget接口的误用,这很明显的违反了条款18的忠告:使接口容易被正确使用不容易被误用。Public继承在这里不是有效选择。

演示案例②:使用private继承
  所以我们在这里使用private继承:

class Widget: private Timer {
private:
	virtual void onTick() const; 
	...                                           
}   

  借由private继承的力量,Timer的public onTick函数在Widget中变为了private,我们将其放在private关键字下并对其进行了重新声明。

演示案例③:以复合的形式实现

  这是个很好的设计,因为private继承不是绝对必须的,我们决定使用组合(compostion)来替代private继承,是可以的。只要在Widget内部声明一个内嵌私有类,此类public继承Timer,在Timer中重新定义onTick,然后在Widget中声明一个此类型的对象。下面是这个方法的实现:

class Timer {
public:
    explicit Timer(int tickFrequency);
    virtual void onTick()const; //定时器每滴答一次,此函数就被自动调用一次
};
 
class Widget{
private:
    class WidgetTimer :public Timer {
    public:
        virtual void onTick()const;
        ...
    };
    WidgetTimer timer;
    ...
};

在这里插入图片描述

  我们派生了一个Timer的派生类WidgetTimer,并重写onTick()函数,然后定义一个WidgetTimer类对象定义于Widget
类中。

  相同的问题,建议使用复合模式,而不建议使用private继承,原因有两点:

① 防止Widget的派生类重写onTick()函数:

  • 在继承方式下:如果Wiget又定义了派生类,你不希望派生类去重写onTick()函数,但是这种情况可能会无法阻止。

  • 在复合模式下:Widget的派生类就不可能有机会去重写onTick()函数了,因为WidgetTimer类是Widget内部的一个private成员,派生类永远无法访问。

② 可以将Widget的编译依存性降至最低:

  • 在继承方式下:如果Widget继承与Timer,那么当Widget被编译时需要知道Timer的定义(不仅仅是声明),因此你可能会在Widget的头文件中包含#include"Timer.h"这样的东西。

  • 在复合模式下:假设我们修改上面的复合模式,将WidgetTimer定义在Widget之外,然后在Widget内定义一个WidgetTimer的指针,此时Widget可以只带着WidgetTimer的声明式,那么当Widget编译时就不需要任何与Timer的任何东西。对大型系统而言,这是很重要的措施。

3、使用private继承比组合更加合理的例子

  在派生类想要访问基类的protected部分或者想去重定义基类的虚函数的时候private继承才是有用的,但是类之间的关系是”is-implemented-in-terms-of”而不是“is-a”。然而,我同时指出有一种涉及到空间优化的边缘情况可以促使你更加喜欢private继承而不是composition(复合)。

  这种情况比较激进:它只适用在没有数据的类中。这种类没有非静态数据成员;没有虚函数(因为虚函数的存在会为每个对象添加一个vptr指针,见条款7);没有虚基类(因为这样的基类同样会引入额外开销,见条款40)。从概念上来说,这样的空类对象应该不使用空间,因为对象中没有数据需要保存。然而由于技术的原因,C++使得独立对象必须占用空间。

class Empty {};
 
class HoldsAnint :private Empty {
private:
    int x;
};
 
sizeof(HoldsAnint); //4 

  你会发现sizeof(HoldsAnInt)>sizeof(int):一个Empty数据成员也会占用空间。对于大多数编译器来说,sizeof(Empty)为1,因为C++法则处理大小为0的独立对象时会默认向” empty ”对象中插入一个char。然而,内存对齐的需求(见条款50)可能导致编译器向HoldsASnInt这样的类中添加填充物,所以HoldsAnInt对象不会只多出来一个char的大小,实际上会增加足够的空间来容纳第二个int。(在我测试过的所有编译器中,上面描述的填充也确实发生了。)

  但是可能你注意到了我非常小心的说明是“独立”(freestanding)对象占用的空间必须不能为0。这个限制不能被应用在派生类对象的基类部分中,因为他们不是“独立“的。如果你继承自Empty类而不是包含一个Empty类型的对象,

class HoldsAnInt: private Empty {
private:
	int x;
};

  几乎可以确定sizeof(HoldsAnInt)==sizeof(int)。这被称作EBO(empty base optimization;空白基类最优化),并且我测试过的编译器都通过了这个测试。如果你是一个库开发人员,如果其客户对空间十分关心,那么了解一下EBO是很值得的。并且你需要知道EBO一般只在单继承下才是可行的。管理C++对象布局的规则通常意味着EBO不能被应用在有多个基类的派生类中。

  事实上,“empty“类不是真的empty。虽然它们永远不会拥有非静态数据成员,它们通常会包含typedefs,enums,静态数据成员或者非虚函数。STL在技术上有很多包含有用成员(通常为typedefs)的空类,包括基类unary_function和binary_function,用户定义的函数对象会继承这些类。多亏了EBO的广泛使用,使得这些继承很少会增加派生类的大小。

4、牢记

  • private继承意为“is-implemented-in-terms-of(根据某物实现出)”。它通常比复合(composition)的级别低。但是当derived class需要访问protected base class的成员,或需要重新定义继承而来的virtual函数时,这么设计时合理的。

  • 和复合(composition)不同,private继承可以造成empty base最优化。这对致力于“对象尺寸最小化”的程序库开发者而言,可能很重要。

总结

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

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

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

相关文章

wsl安装CUDA

NVCC 昨天已经安装好了gpu版的pytorch,对于一般的代码应该就可以运行了。但有些代码中需要用到cuda算子,需要配置nvcc环境。对于这个我也没能搞太清楚,网上的说法不一,我使用conda安装pytorch时也安装了cudatoolkit,按…

c++11 标准模板(STL)(std::forward_list)(八)

定义于头文件 <forward_list> template< class T, class Allocator std::allocator<T> > class forward_list;(1)(C11 起)namespace pmr { template <class T> using forward_list std::forward_list<T, std::pmr::polymorphic_…

当面试官问:“你还有什么要问我”,怎样回答才最加分?

面试到最后&#xff0c;面试官常常会问求职者&#xff1a;“你还有什么要问我&#xff1f;”许多人面对这个问题&#xff0c;不知该怎样回答&#xff0c;怕回答不好影响自己的面试结果&#xff0c;那么怎么回答才最加分呢&#xff1f;有人说&#xff0c;可以问问这个职位应该具…

springboot整合gateway网关

2.3 搭建Gateway 本项目使用Spring Cloud Gateway作为网关&#xff0c;下边创建网关工程。 新建一个网关工程。 工程结构 添加依赖&#xff1a; XML org.springframework.cloud spring-cloud-starter-gateway com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discove…

TryHackMe-Blog

Blog 比利乔尔&#xff08;Billy Joel&#xff09;在他的家用电脑上写了一个博客&#xff0c;并开始工作。这将是非常棒的&#xff01; 枚举此框并找到隐藏在其上的 2 个标志&#xff01;比利的笔记本电脑上有一些奇怪的事情。你能四处走动并得到你需要的东西吗&#xff1f;还…

蓝队常用的攻击手段

目录 一&#xff0c; 漏洞利用 1.1 SQL 注入漏洞 1.2 跨站漏洞 1.3 文件上传或下载漏洞 1.4 命令执行漏洞 1.5 敏感信息泄露漏洞 在实战过程中&#xff0c;蓝队专家根据实战攻防演练的任务特点逐渐总结出一套成熟的做法:外网纵向突破重点寻找薄弱点&#xff0c;围绕薄弱点…

2022 VeLO: Training Versatile Learned Optimizers by Scaling Up

VeLO: Training Versatile Learned Optimizers by Scaling Up 通过扩展模型的规模来训练一个通用的优化器。 设计上&#xff0c;优化器的原理基于元学习的思路&#xff0c;即从相关任务上学习经验&#xff0c;来帮助学习目标任务。 相比迁移学习&#xff0c;元学习更强调获取元…

2023年批量下载和改名音频专辑(单页列表)

一、下载原理 1&#xff09;找到目标音频的专辑网页&#xff0c;这里以 kite runner mp3为例。&#xff08;需要自己找&#xff09; https://www.xi___ma___la_____ya.com/album/71718770 2&#xff09;进入详细页&#xff08;称为一次请求URL&#xff09;&#xff08;不需要…

JUC(java.util.concurrent)的常见类

文章目录一、JUC常见类Callable 接口ReentrantLockSemaphore(信号量)CountDownLatch一、JUC常见类 concurrent代表了并发&#xff0c;这个包下为我们提供了并发编程(多线程)相关的组件. Callable 接口 我们的Callable接口和Runnable是一样的&#xff0c;但也有一些区别: Run…

C/C++实现跨年表白烟花

跨年表白烟花使用c/c实现烟花效果&#xff08;小白进&#xff09;分析诉求&#xff0c;拆分问题头文件贯穿全文的媒体部分文字部分&#xff1a;进入烟花弹部分烟花弹的属性初始化烟花弹让烟花弹飞起来烟花爆炸烟花弹的属性初始化烟花让烟花炸起来完成代码&#xff1a;使用c/c实…

840个最优的机器学习python开源项目整理分享

本资源包含了840个很棒的机器学习开源项目&#xff0c;总共270万颗星分为32个类别。所有项目均按项目质量得分排名&#xff0c;该得分是根据从GitHub和不同程序包管理器自动收集的各种指标计算得出的。资源整理自网络&#xff0c;资源获取见源地址&#xff1a;https://github.c…

三星手机提取微信聊天数据

三星手机提取微信聊天数据的方法&#xff0c;无需root。 注意&#xff0c;暴力破解密码需要英伟达显卡&#xff0c;一小时内破解&#xff0c;无显卡可能要两天。 1. 安装USB驱动&#xff0c;通过S换机助手&#xff0c;备份微信软件至电脑。注意&#xff0c;选择不加密。 三星…

[硬核] Bootstrap Blazor Table 综合演示例子

知识点: 1.导入导出 2.分页功能 3.增删改查 4.批量删除 5.批量编辑(审核) 6.列排序与列搜索 7.顶部搜索实现所有列搜索 8.高级搜索实现多条件搜索 9.顶部与刷新与视图列 10.实现文本类型明细行 11.列的统计 12.隐藏列,时间日期列格式化 13.新窗口打开 14.随机数据 15.自由编辑…

DVWA靶机CSRF全难度(未完)

目录 Low难度 medium难度 Cross Site Request Forgery跨站的请求伪造 原理&#xff1a;利用受害者尚未失效的身份认证信息、会话&#xff1b;诱骗其访问黑客设计号的页面&#xff0c;在受害人不知情的情况下以受害人的身份向服务器发送请求完成非法操作 Low难度 源代码 &l…

十二、RabbitMQ 报错汇总

&#x1f33b;&#x1f33b; 目录一、报版本过低问题一、报版本过低问题 问题&#xff1a; error: Failed dependencies: libcrypto.so.1.1()(64bit) is needed by erlang-25.1.2-1.el8.x86_64 libcrypto.so.1.1(OPENSSL_1_1_0)(64bit) is needed by erlang-25.1.2-1.el8.x86_…

基于模糊控制的自平衡小车的研究

1、内容简介略635-可以交流、咨询、答疑2、内容说明随着人类文明的发展&#xff0c;传感器技术、计算机应用技术、机械学、微电子技术、通讯技术以及人工智能技术也得到了飞速的发展。进入21世纪后&#xff0c;在机器人学和机器人技术领域&#xff0c;自平衡小车已成为其中的重…

LeetCode 2293. 极大极小游戏

【LetMeFly】2293.极大极小游戏 力扣题目链接&#xff1a;https://leetcode.cn/problems/min-max-game/ 给你一个下标从 0 开始的整数数组 nums &#xff0c;其长度是 2 的幂。 对 nums 执行下述算法&#xff1a; 设 n 等于 nums 的长度&#xff0c;如果 n 1 &#xff0c;…

【OpenCV】形态学操作 | 图像平滑 | 边缘检测 | Laplacian算子

Ⅰ. 形态学操作 0x00 腐蚀和膨胀 腐蚀和膨胀是最基本的形态学操作&#xff0c;腐蚀和膨胀都是针对白色部分&#xff08;高亮部分&#xff09;而言的。 膨胀就是使图像中的高亮部分扩张&#xff0c;效果图拥有比原图更大的高亮区域&#xff1b;腐蚀是原图中的高亮区域被蚕食&…

C语言文件补充笔记2:VS查看定义、文件章节涉及到的函数

1 VS查看函数的定义与库的原码 &#xff08;1&#xff09;查看库函数的定义 右击要查看的函数&#xff0c;然后“转到定义” 这里就跳转到了定义的所在文件 在右上角关闭相关文件 &#xff08;2&#xff09; 查看库原码 将鼠标放到导入的库中&#xff0c;然后右击&#…

给数组创建复制(深拷贝)给数组创建复制(深拷贝)

【小白从小学Python、C、Java】 【计算机等级考试500强双证书】 【Python-数据分析】 数组的深拷贝、浅拷贝、引用拷贝 修改原数组不会影响复制后的数组 numpy.copy() [太阳]选择题 对于以下python代码最后输出的结果是? import numpy as np print("【执行】a np.arang…