读书笔记 effective c++ Item 39 明智而谨慎的使用private继承

1. private 继承介绍

Item 32表明C++把public继承当作”is-a”关系来对待。考虑一个继承体系,一个类Student public 继承自类Person,如果一个函数的成功调用需要从Student到Person的隐式转换,这时候“is-a”关系就出现了。对于一部分实例,使用private继承来代替public继承也是有价值的事情:

 1 class Person { ... };
 2 class Student: private Person { ... };     // inheritance is now private
 3 
 4 void eat(const Person& p);       // anyone can eat
 5 
 6 void study(const Student& s); // only students study
 7 
 8 Person p;                                  // p is a Person
 9 
10  
11 
12 Student s;       // s is a Student
13 
14 eat(p);    // fine, p is a Person
15 
16 eat(s);    // error! a Student isn’t a Person

 

很清楚的是,private继承并不意味着“is-a”关系。那它意味着什么呢?

 

在我们看到结果之前,先让我们看一下private继承的行为。Private继承的第一条规则也是刚才从实际中看到的:与public继承相反,如果类之间的继承关系是privte继承,编译器不会将派生类对象(Student)转换成为基类对象(Person)。这也是为什么为对象s调用eat会失败。第二条规则是即使在基类中的成员是protected或者public的,从此基类中private继承而来的成员会变成派生类中的private成员。

 

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

 

2. 如何在private继承和组合之间做出选择

Private继承意味着“is-implemented-in-terms-of”的事实会让你感觉有一些不安,因为Item 38中指出组合(composition)也同样意味着“is-implemented-in-terms-of”。怎么在它们之间做出选择?答案是简单的:在任何可能的时候使用组合(composition),在必须使用private继承的时候才去使用它。什么时候必须使用?主要是当protected成员或者(和)虚函数被牵扯进来的时候,也有处在边界的情况,因为空间原因而不能使用private继承。我们过后再去担心它,毕竟它是处在边界的情况。

 

2.1  一个不能简单的使用public继承的例子

假设我们正在一个涉及到Widgets类的应用上工作,我们想更好的理解Widgets是如何被使用的。例如,我们不但想知道Widget成员函数的调用有多频繁,我们同样想知道函数调用随时间变化的频率变化情况。程序在不同的执行阶段会有不同的行为轮廓。举个例子,编译器对函数的使用会大大的不同于优化和代码生成时对函数的使用。

 

我们决定修改Widget类来对每个成员函数的调用次数进行追踪。在运行时,我们会定期来检查这项信息,可能也会伴随着检查每个Widget对象值和其它一些我们认为有用的数据。为了达到这个目的,我们会创建一个定时器于是我们可以知道什么时候去收集这些统计信息。

 

我们更乐意去重用代码而不是实现新代码,我们翻阅了工具集,很高兴的找到了如下的类:

1 class Timer {
2 public:
3 explicit Timer(int tickFrequency);
4 
5 virtual void onTick() const;     // automatically called for each tick
6 
7 ...                                            
8 
9 };       

                                 

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

 

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

2.2 使用private继承

所以我们在这里使用private继承:

1 class Widget: private Timer {
2 private:
3 
4 virtual void onTick() const;    // look at Widget usage data, etc.
5 
6 ...                                           
7 
8 }     

                                

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

2.3 使用组合(compostion)以及两个优点

这是个很好的设计,但如果private继承不是必须的,它就没有任何价值。如果我们决定使用组合(compostion)来替代private继承。我们可以在Widget内部声明一个内嵌类,此类public继承Timer,在Timer中重新定义onTick,然后在Widget中声明一个此类型的对象。下面是这个方法的实现:

  

这个设计比private继承更加复杂,因为它同时涉及到(public)继承和组合(composition),同时引入了一个新类(WidgetTimer)。我用这个例子是提醒你如果有多种方法来处理一个设计问题,训练自己考虑多种方法是值得的(Item 35)。然而,我能想出两个原因来证明使用public继承加组合比private继承更好。

 

第一,  你可能想使用Widget作为其他类的基类,但是你可能想阻止派生类重新定义onTick。如果Widget继承自Timer,这是不可能的,即使继承是private继承。(回忆一下Item 35,即使虚函数是private的,派生类还是可能重新定义它)但是如果WidgetTImer在Widget中是private的,并且继承自Timer,Widget的派生类就没有对WidgetTimer的访问权,也就不能继承它或者重新定义它的虚函数。如果你使用java或者C#,并且发现C++没有阻止派生类重定义虚函数的能力(Java使用final methods,C#使用sealed),现在你有方法在C++中对此行为进行模拟了。

第二,  你可能想最小化Widget的编译依赖性。如果Widget继承自Timer,当Widget被编译的时候必须能够得到Timer的定义,所以定义Widget的文件必须#include Timer.h。从另外一个角度讲,如果将WidgetTimer移出Widget并且Widget只包含一个指向WidgetTimer的指针,在Widget中对WidgetTimer进行简单的声明就可以了,不需要#include与Timer相关的任何头文件。对于大型系统来说,这样的解耦是很重要的。(编译依赖的详细介绍看Item 31)

 

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

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

 

这种边缘情况确实靠边缘:它只应用在没有数据的类中。这种类没有非静态数据成员;没有虚函数(因为虚函数的存在会为每个对象添加一个vptr指针,见Item 7);没有虚基类(因为这样的基类同样会引入间接费用,见Item40)。从概念上来说,这样的空类对象应该不使用空间,因为对象中没有数据需要保存。然而由于技术的原因,C++使得独立对象必须占用空间,所以如果你写下下面的代码:

 

 1 class Empty {}; // has no data, so objects should
 2 // use no memory
 3 
 4 class HoldsAnInt {      // should need only space for an int
 5 
 6 
 7    private:
 8    int x;
 9 
10 Empty e;                    // should require no memory
11 
12 };                

 

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

 

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

  

1 class HoldsAnInt: private Empty {
2 private:
3 int x;
4 };

 

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

 

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

2.5 结论

让我们回到基本议题。因为大多数类不是空的,所以EBO不是使用private继承的合法理由。进一步来说,大多数继承对应着”is-a”,这也是public继承的工作而不是private继承。组合和private继承都意味着“is-implemented-in-terms-of“,但是组合更容易理解,所以你应该在任何可能的情况下使用它。

 

当你处理两个类时,它们不是“is-a“的关系,一个类要么需要访问另外一个类的protected成员要么需要重新定义一个或多个它的虚函数,这时候private继承在大多数情况下会是合法的设计策略。即使在这种情况中,我们看到public继承和包含(containment)的混合使用通常情况下能够产生我们需要的行为,虽然增加了设计复杂性。明智而谨慎的使用private继承就意味着,在你考虑过所有的替代方法之后,在你的软件中它是表示两个类关系的最好方法,在这种情况下才去使用它。

 

3. 总结 

  • Private继承意味着“is-implemented-in-terms-of “。它通常比组合的使用要低一个层次,但是当派生类需要访问protected基类成员或者需要重新定义继承而来的虚函数时使用Private继承有意义的。
  • 不像组合,private继承能够使用空基类优化。这对努力减少对象大小的库开发者来说很重要。


作者: HarlanC

博客地址: http://www.cnblogs.com/harlanc/
个人博客: http://www.harlancn.me/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出, 原文链接

如果觉的博主写的可以,收到您的赞会是很大的动力,如果您觉的不好,您可以投反对票,但麻烦您留言写下问题在哪里,这样才能共同进步。谢谢!

C# 与欧姆龙 PLC 实战:通过 Ethernet/IP 协议进行设备控制与监控 本文介绍了如何通过C#和Ethernet/IP协议实现与欧姆龙PLC的通信。Ethernet/IP是基于以太网的工业协议,适用于工业设备间的实时数据交换。文章详细讲解了使用EthernetIPSharp库建立连接、读写数据的步骤,并提供了示例代码。此外,还探讨了实时监控、报警系统等高级功能,为工业自动化控制系统开发提供了实用指导。该方法简化了PLC通信开发,提高了系统可靠性和智能化程度。 阅读详情

相关推荐

SCAN和JTAG分别是什么?它们之间有什么联系与区别?

SCAN(扫描)技术是集成电路测试领域的一种方法,主要用于边界扫描测试(Boundary Scan)。它是在集成电路设计中引入的一种测试结构,允许测试人员通过在芯片的输入/输出引脚上设置和读取特殊寄存器(边界扫描寄存器,BSR)的内容,来间接测试电路板上的互联和芯片内部功能块。这种方式可以在不接触其他内部节点的情况下测试整个电路板的连接性,极大地提高了测试覆盖率和可诊断性。JTAG(Joint Test Action Group,联合测试行动小组)则是开发并推广边界扫描测试标准的一个组织。

初心不忘产学研的博客 2542

继承 or 复合?

继承和复合的选择

我的天空 我的梦 641

cadence数模混合仿真反标sdf

数模混合IC设计时经常要调用一些Verilog模块(最简单的例如SPI接口),在前仿时一般直接用rtl代码来仿真,但是后仿时要关注因寄生带来的延迟,避免这些额外的延迟导致时序错误,因此不能简单地采用RTL来仿真。 一般有两种办法:1是采用由RTL综合得到的门级电路来仿真,这种方法比较简单,但是缺点是规模较大,而且只包括门延迟不包括因版图导致的线延迟;如果想要仿真更精细,就要对该这部分门电路提取寄生参数; 另一种方式是利用sdf文件(standard delay format)来进行仿真。后端工具在布局布

长河柱天的博客 7274

Effective C++ 读书笔记 Item39 明智而审慎地使用 private 继承

Item 32提出public继承表示"is-a"的关系,这是因为编译器会在需要的时候将子类对象隐式转换为父类对象。 然而private继承则不然: class Person { ... }; class Student: private Person { ... }; // inheritance is now private void eat(const Person& p...

Vermont_的博客 320

永磁同步电机矢量控制——电流内环前馈解耦策略

在采用永磁同步电机矢量控制时,ud和uq的指令值存在耦合项。需要对其进行解耦,目前,常用的解耦设计方法有前馈解耦、偏差解耦以及内模控制,本文主要分享电流内环前馈解耦控制策略。

a12312341ll的博客 8691

自动驾驶开发者必看:nuscenes数据集23类物体标注全解析(附避坑指南)

本文深入解析nuscenes数据集的23类物体标注系统,为自动驾驶开发者提供全面的标注规范解读和实战避坑指南。涵盖多模态数据组织、类别细分规则、属性系统应用技巧以及标注质量验证方法,帮助开发者高效利用这一标杆数据集提升模型性能。

weixin_29041767的博客 255

《Effective C++》读书笔记之item39:明智而审慎地使用private继承

1.private继承意味着“根据某物实现出”,只有实现部分被继承,接口部分应略去;它只在软件“实现”层面上有意义,在软件设计层面上没有意义。private继承而来的基类成员都会在派生类中成为private属性;如果类之间是private继承关系,编译器不会自动将一个派生类对象转换为基类对象。 2.private继承和复合都是“根据某物实现出”,在设计时应尽量使用复合,少使用private继承。

banguijun的专栏 644

实战拆解:RAG vs Agent在金融风控中的性能对比

2024年上半年我帮某华东地区城商行做零售交易反欺诈系统升级时,技术团队曾发生过一次激烈争论:一派认为应该优先上RAG(检索增强生成)方案,成本低、落地快、合规性好;另一派坚持要上Agent(智能体)方案,推理能力强、能应对复杂新型欺诈。最终我们做了完整的AB测试,结论是两者没有绝对的优劣,只有场景适配的差异:RAG适合高并发、低时延的实时风控场景,Agent适合低并发、高准确率要求的事后核查/大额审批场景,两者结合的 hybrid 方案能覆盖95%以上的风控需求。本文我会结合真实项目的测试数据,从核心概念

AI天才研究院 251

深挖C++:类和对象(中)

(先进后出)

xys_678的博客 237

VSCode+Typst零配置写作指南:5分钟搞定论文排版环境(含实时预览技巧)

本文介绍如何利用VSCode和Typst快速搭建零配置的论文排版环境,5分钟内实现高效写作与实时预览。Typst作为轻量级排版工具,结合LaTeX的精度与现代化语法,显著提升学术写作效率,特别适合需要频繁调整格式的研究人员。

weixin_30642561的博客 109

[xboard-real6410]39 real6410中的BL1和BL2实验及流程分析

文章目录uboot控制uboot 流程uboot加载kernel uboot控制 uboot 流程 uboot加载kernel

鹰之翔 298

23.IO库【由浅入深-C++】

int _month;int _day;return in;return out;operator<<返回ostream&operator>>返回istream&,以支持链式调用;输出通常接受const T&,输入接受T&,因为输入要修改对象;如果访问私有成员,可以声明为友元;重载后,都能工作。ios_base。

插花弄玉的博客 274

STM32F103(二十三)通用同步异步收发器(USART)

学习板:STM32F103ZET6 往期博客: STM32F103五分钟入门系列(一)跑马灯(库函数+寄存器)+加编程模板+GPIO总结 STM32F103五分钟入门系列(二)GPIO的七大寄存器+GPIOx_LCKR作用和配置 STM32F103五分钟入门系列(三)GPIO的常用库函数使用方法总结+一个网络上的误区 STM32F103五分钟入门系列(四)蜂鸣器实验(库函数+寄存器) STM32F103五分钟入门系列(五)按键实验(库函数+寄存器) STM32F103五分钟入门系列(六)时钟框图+相关寄存器

Curnane0_0的博客 1019

C++移动构造函数详解

移动构造函数是C++11引入的一种特殊构造函数,用于从临时对象(右值)"窃取"资源来初始化新对象,而不是进行昂贵的深拷贝。char* data;public:// 移动构造函数:参数是右值引用 &&// 关键:把源对象的资源置空。

拒绝AI文,努力在AI时代写出感动人心的好文章 389

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

WINCC7.3破解文件

WINCC7.3硬件够破解文件,有需要的下载!WINCC7.3硬件够破解文件,有需要的下载!

C++ 多态详解:作用、原理与实战用法

多态是C++面向对象的核心特性,通过虚函数实现运行期动态绑定,使同一接口在不同对象上表现出不同行为。其核心作用包括统一接口、提升可扩展性、降低耦合、支持运行时分派与批量处理。实现依赖虚函数表(vtable)和虚指针(vptr),需注意虚析构函数、对象切片等陷阱。相比编译期多态(模板),运行期多态灵活性高但有调用开销。合理结合两者,可兼顾代码的灵活性与性能,是构建可扩展、低耦合系统的关键。

2402_85743155的博客 377

C# WinForm 基于DirectShow.dll编写的程序,可边预览边保存视频,音频和图片

基于微软的DirectShow.dll的二次开发1.自动识别连接的摄像头跟麦克风且可以对其进行选择(默认选中第一个)2.可录制wmv和avi两种格式的视频并且都带有音频,avi可以录制的分辨率和帧数进行选择,wmv默认进行了淸晰度优化如不喜欢可去除代码中有相关提示3.可在预览点开之后进行拍照,也可边录制视频边拍照4.添加了视频和音频的压缩功能(酌情使用)这次二次开发发现C#对DirectShow的相关信息太少,这次做完也不敢藏私共享出来,还望大家指正

【C语言】字符与字符串函数

C语言中字符串为以\0结尾的字符数组,标准库函数基于此约定。本文解析字符串内存布局,详解ctype.h与string.h中字符分类、长度计算、拷贝、比较等函数的使用要点,强调长度判断与越界风险,最后通过命令行待办事项示例整合fgets、sscanf、strncpy、strcmp,展示实际应用中的关键细节。

记录在学习过程中学到的知识 343

IMSL CNL 7.0.0 (x86-64)Crack

IMSL CNL 7.0.0 (x86-64)Crack intel的算法库破解文件,这是C++版本的破解文件

C语言程序设计题|有序数组插入数字冒泡排序

题目要求将一个数插入有序数组末尾,并用冒泡排序实现整体升序。代码先将输入数存入数组最后位置,再通过双重循环进行冒泡排序,每轮将较大元素向后移动,最终输出11个升序元素。该方法虽时间复杂度为O(n²),但符合题目要求,逻辑清晰,适合初学者理解排序原理。

2301_81429079的博客 746

C++ 函数模版

模板参数:● template ,T 就是类型占位符,名字可以自定义,习惯用T● 占位符可以在函数返回值、形参、函数内部当作类型使用使用要点模板代码一般写在头文件:编译器需要看到完整模板源码,才能在每个编译单元完成实例化全局函数、类内成员函数都可以写成模板实例化发生在编译阶段,不是运行时隐式实例化:调用时编译器自动推导实参类型,生成对应版本函数。

2501_94041724的博客 303

【C++11】包装器实操指南:function + bind

C++11引入std::function和std::bind解决可调用对象类型不统一问题。std::function将函数、仿函数、lambda等包装为统一类型,便于存储与传递,常用于实现运算映射表;std::bind则支持参数绑定、顺序调整及成员函数对象绑定,提升灵活性。二者结合可实现灵活的函数适配与回调机制,显著增强代码可维护性与扩展性。

2301_82161965的博客 313

constexpr 完全指南:从 const 的两种含义,到编译期计算的四次演进

常量表达式:值不会改变、并且在编译过程中就能得到计算结果的表达式。字面值(423.14"abc"用常量表达式初始化的const对象constexpr变量│ constexpr 的三次认知升级 │││ │ │【第一层】 【第二层】 【第三层】const ≠ constexpr 函数有两种身份 它是工具不是目的│ │ │编译期已知 vs 不可改 传常量→编译期 配置校验、查表、│ 传变量→运行期 位掩码、编译期哈希│ │ │。

2601_94861874的博客 160

C++ 运行时多态与虚函数机制全面剖析【面试重点】

构成C++ 运行时多态必须是基类的指针或者引用调用虚函数。被调用的函数必须是虚函数,并且派生类必须对该虚函数完成重写/覆盖(Override)。只有基类的指针或引用才能既指向基类对象,又指向派生类对象(切片机制);而只有完成了虚函数重写,派生类与基类才具备不同的函数行为,达到“指向谁就调用谁”的效果。在类成员函数的前面加上virtual关键字修饰,该成员函数就成为了虚函数。public:// 成员函数前加 virtual 修饰,声明为虚函数。

Fuyo_0209的博客 436

PCL 点云例程(第一篇):点云容器的六个字段,和教程没讲的三个坑

没讲。

qq_20575585的博客 286

【C++ 基础入门】C++继承(下):问题、底层原理与组合

C++继承(菱形继承的问题、虚继承原理、继承与组合、构造顺序)

2601_96587588的博客 771

【C++ 基础入门】类和对象(上):封装、对象模型与 this 指针

C++类与对象知识(面向过程和面向对象的初步认识,类的初步认识,类的访问限定符及封装,类的作用域、实例化,类的对象模型,this指针)

2601_96587588的博客 876

C++算法 枚举

C++ 普通枚举算法题讲解

2503_94479566的博客 266

C++09_vector类复习

本文以10个核心问题为主线,系统解析C++ vector的底层机制:从基本概念、初始化方式、遍历方法,到size与capacity的区别、扩容策略(VS 1.5×,g++ 2×)、reserve与resize差异、中间增删的挪动原理、迭代器失效原因,直至二维vector的内存布局(含三指针结构)。通过“先会用、再懂扩容、最后看清对象结构”的递进逻辑,揭示vector作为动态数组的本质——连续内存、自动扩容、随机访问高效,但插入删除代价高,迭代器易失效。全文深入浅出,助读者摆脱“黑盒”认知,真正掌握其内存图景

lemo_n_的博客 337
上一篇: Vaadin发布Polyglot框架第8版
下一篇: springboot创建war包部署在外部tomcat上
weixin_33728708
博客等级 码龄11年 5472粉丝 1452原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值