Effective C++ 学习笔记 条款39 明智而审慎地使用private继承
条款32 展示了 C 将 public 继承视为 is-a 关系。它通过这样一个例子来说明当 Student 以 public 方式继承自 Person 时如果函数调用需要编译器会隐式地将 Student 转换为 Person。这里值得用 private 继承代替 public 继承再把那部分例子重复一遍显然private 继承并不意味着 is-a。那它意味着什么呢“等等”你说“在探讨含义之前先说说行为吧。private 继承的行为是怎样的”好的关于 private 继承的第一条规则你刚才已经见识过了与 public 继承相反如果类之间的继承关系是 private 的编译器通常不会将派生类对象如 Student转换为基类对象如 Person。这就是为什么对 s 调用 eat 会失败。第二条规则是从 private 基类继承来的成员在派生类中都会变成 private 成员即使它们在基类中是 protected 或 public 的。行为说完了。接下来就是含义。private 继承意味着 is-implemented-in-terms-of根据某物实现。如果你让类 D 私有继承自类 B你这样做是因为你感兴趣的是利用类 B 中已有的某些特性而不是因为 B 类型和 D 类型对象之间存在任何概念上的关系。因此private 继承纯粹是一种实现技术这就是为什么你从 private 基类继承的所有东西在类中都变成 private 的原因——这些都只是实现细节。使用条款34中引入的术语来说private 继承意味着只应继承实现接口应被忽略。如果 D 私有继承自 B意味着 D 对象是根据 B 对象来实现的仅此而已。private 继承在软件设计层面毫无意义只存在于软件实现层面。private 继承意味着 is-implemented-in-terms-of这个事实有点令人不安因为条款38指出组合也可以表达同样的意思。那该如何在它们之间做出选择呢答案很简单能用组合就用组合只有在必须的时候才用 private 继承。什么时候必须主要是在涉及 protected 成员和/或虚函数的时候不过还有一种边缘情况——空间方面的考量可能会让天平倾向 private 继承。边缘情况我们稍后再谈。毕竟它只是边缘情况。假设我们正在开发一个涉及 Widget 的应用程序并且决定需要更好地了解 Widget 的使用情况。例如我们不仅想知道 Widget 成员函数被调用的频率还想知道调用比率随时间如何变化。执行阶段不同的程序在不同阶段会有不同的行为特征。例如编译器在解析阶段使用的函数与优化和代码生成阶段使用的函数就有很大不同。我们决定修改 Widget 类让它能记录每个成员函数被调用的次数。在运行时我们会定期检查这些信息可能还会结合每个 Widget 的值以及我们认为有用的其他数据。为了实现这个功能我们需要某种定时器以便知道何时该收集使用统计数据。与其从头编写新代码我们更倾向于复用现有代码。于是我们在自己的工具包中翻找了一番欣喜地发现了下面这个类这正好是我们需要的。Timer 对象可以按我们需要的频率来滴答计时每次滴答时它都会调用一个虚函数。我们可以重新定义那个虚函数让它检查当前 Widget 世界的状态。完美为了让 Widget 能够重新定义 Timer 中的虚函数Widget 必须继承自 Timer。但在这种情况下public 继承并不合适。并不是说一个 Widget 是一个 Timer。Widget 的客户不应该能在 Widget 上调用 onTick因为这并不是 Widget 概念接口的一部分。允许这样的函数调用会让客户很容易错误地使用 Widget 接口这明显违反了条款18中“设计易于正确使用、难以错误使用的接口”的建议。因此public 继承在这里不是一个可行的选项。于是我们采用 private 继承通过 private 继承Timer 中的公有 onTick 函数在 Widget 中变成了私有成员而我们在重新声明它时也将其保持在私有区域。再次强调把 onTick 放在公有接口中会误导客户让他们以为自己可以调用它那就会违反条款18。这是一个不错的设计但值得指出的是private 继承并不是严格必要的。如果我们执意要用组合来代替也是可行的。我们只需在 Widget 内部声明一个私有的嵌套类让它公有继承 Timer在那里重新定义 onTick然后在 Widget 中放置一个该类型的对象即可。下面是这种方式的粗略框架这种设计比单纯使用 private 继承要复杂因为它同时涉及public继承和组合还引入了一个新类WidgetTimer。坦白说我展示这个例子主要是为了提醒你设计问题的解决方式往往不止一种训练自己考虑多种方案是很有价值的另见条款35。尽管如此我能想到两个理由让你可能更倾向于采用“public 继承加组合”的方式而非 private 继承。第一你可能想让 Widget 允许有派生类但同时又想阻止派生类重新定义 onTick。如果 Widget 直接继承自 Timer这一点就做不到——即使采用 private 继承也不行回想条款35派生类可以重新定义虚函数即使它们不被允许调用这些函数。但如果 WidgetTimer 是 Widget 中的私有成员且它继承自 Timer那么 Widget 的派生类就无法访问 WidgetTimer也就不能继承自它或重新定义它的虚函数。如果你用过 Java 或 C#并且怀念那种能够阻止派生类重新定义虚函数的能力即 Java 的 final 方法和 C# 的 sealed 方法那么现在你就有思路在 C 中近似实现那种行为了。第二你可能想最小化 Widget 的编译依赖。如果 Widget 继承自 Timer那么在编译 Widget 时 Timer 的定义必须可见所以定义 Widget 的文件可能必须 #include Timer.h。另一方面如果把 WidgetTimer 移出 Widget且 Widget 只包含一个指向 WidgetTimer 的指针那么 Widget 只需一个 WidgetTimer 类的前置声明就能应付过去无需 #include 任何与 Timer 相关的东西。对于大型系统这种解耦可能非常重要关于最小化编译依赖的细节请参见条款31。我之前说过private 继承主要在有派生类想要访问基类的 protected 成员或者想要重新定义基类的一个或多个虚函数时才有用但类之间的概念关系是 is-implemented-in-terms-of 而非 is-a。不过我也提到过还有涉及空间优化的边缘情况可能会让你倾向于 private 继承而非组合。这个边缘情况确实比较“边缘”它仅在你处理一个没有数据成员的类时才适用。这样的类没有非静态数据成员没有虚函数因为虚函数的存在会给每个对象增加一个虚表指针——参见条款7也没有虚基类因为虚基类也会带来大小开销——参见条款40。从概念上讲这种空类的对象应该不占用空间因为没有任何需要存储的、与对象相关的数据。然而由于技术上的原因C 规定独立对象freestanding objects的大小必须非零所以如果你这样做你会发现sizeof(HoldsAnInt) sizeof(int);一个 Empty 数据成员确实需要内存。在大多数编译器下sizeof(Empty) 是 1因为 C 对于零大小独立对象的禁令通常是通过在“空”对象中静默插入一个 char 来满足的。然而对齐要求参见条款50可能会让编译器给 HoldsAnInt 这样的类添加填充字节所以 HoldsAnInt 对象很可能不仅仅增加一个 char 的大小而是实际上扩大到足以容纳第二个 int在我测试过的所有编译器上情况确实如此。不过你可能已经注意到了我一直谨慎地说“独立”对象的大小不能为零。这个限制并不适用于派生类对象中的基类子对象因为它们不是独立的。如果你让 HoldsAnInt 继承自 Empty而不是包含一个该类型的对象你几乎可以肯定 sizeof(HoldsAnInt) sizeof(int)。这就是所谓的空基类优化empty base optimizationEBO在我测试过的所有编译器上都有实现。如果你是库开发者且客户关心空间EBO 值得了解。同样值得注意的是EBO 通常仅在单继承下可行因为 C 对象布局规则意味着 EBO 通常不能应用于有多个基类的派生类。在实际中“空”类并不真正是空的。虽然它们从不拥有非静态数据成员但它们通常包含 typedef、枚举、静态数据成员或非虚函数。STL 中有许多技术上为空的类却包含有用的成员通常是 typedef其中包括 unary_function 和 binary_function 这些基类——用户自定义的函数对象类通常从它们继承。得益于 EBO 的广泛实现这种继承很少会增加继承类的大小。不过还是让我们回到基本面上。大多数类都不是空的所以 EBO 很少能成为采用 private 继承的合理理由。而且大多数继承对应的是 is-a 关系那是 public 继承的职责而非 private 继承。组合和 private 继承都表示 is-implemented-in-terms-of但组合更容易理解所以只要能用组合你就应该用。private 继承最可能成为合理设计策略的场景是处理两个没有 is-a 关系的类且其中一个需要访问另一个的 protected 成员或者需要重新定义另一个的一个或多个虚函数。即便在这种情况下我们也看到 public 继承加包含containment的组合往往能实现你想要的行为尽管设计复杂度会更高。谨慎地使用 private 继承意味着在考虑过所有替代方案之后它仍然是在你的软件中表达两个类之间关系的最佳方式时才使用它。切记1.private 继承表示 is-implemented-in-terms-of。它通常不如组合但在派生类需要访问基类 protected 成员或需要重新定义继承来的虚函数时它是有意义的。2.与组合不同private 继承可以启用空基类优化。这对致力于最小化对象大小的库开发者来说可能很重要。

相关新闻

基于IEEE1588-PTP的高精度时间同步系统:Python与C混合编程实践

基于IEEE1588-PTP的高精度时间同步系统:Python与C混合编程实践

1. 项目概述与核心价值最近在做一个分布式数据采集的项目,遇到了一个挺头疼的问题:分布在几个机柜里的十几台设备,各自采集到的数据时间戳对不上,误差能到几十毫秒。这对于后续的数据融合和分析来说简直是灾难。为了解决这个问题&…

2026/7/21 5:56:17 阅读更多 →
HarmonyOS应用开发实战:小事记 - UIAbility 的冷启动/热启动/后台启动三种场景与 launchParam 解析

HarmonyOS应用开发实战:小事记 - UIAbility 的冷启动/热启动/后台启动三种场景与 launchParam 解析

前言 在 HarmonyOS 的 Stage 模型中,UIAbility 的启动方式决定了其生命周期回调的顺序和参数。理解冷启动(Cold Start)、热启动(Hot Start)和后台启动(Background Launch)三种场景的区别&#…

2026/7/21 5:55:17 阅读更多 →
从零实现C++ Json-RPC框架:深入理解网络编程与协议设计

从零实现C++ Json-RPC框架:深入理解网络编程与协议设计

1. 项目概述:为什么我们需要自己造一个Json-Rpc轮子?在C的后端服务开发里,远程过程调用(RPC)是个绕不开的话题。你可能用过gRPC,或者Thrift,它们功能强大,生态成熟。但有时候&#x…

2026/7/21 5:55:17 阅读更多 →

最新新闻

UE5碰撞系统核心:UpdateOverlaps源码解析与实战优化

UE5碰撞系统核心:UpdateOverlaps源码解析与实战优化

1. 项目概述:为什么我们需要深入理解UpdateOverlaps? 在虚幻引擎5(UE5)里做开发,无论是做角色拾取物品、子弹击中判定,还是简单的触发器门,都绕不开一个核心机制—— 重叠事件 。蓝图里拖个 …

2026/7/21 15:23:26 阅读更多 →
Mindustry终极指南:5步掌握自动化塔防策略游戏

Mindustry终极指南:5步掌握自动化塔防策略游戏

Mindustry终极指南:5步掌握自动化塔防策略游戏 【免费下载链接】Mindustry The automation tower defense RTS 项目地址: https://gitcode.com/GitHub_Trending/min/Mindustry 想不想体验一款融合了塔防、自动化和即时战略的科幻游戏?今天我就来给…

2026/7/21 15:23:26 阅读更多 →
揭秘scGPT:单细胞数据分析的AI革命,如何在1秒内处理千万级细胞数据?

揭秘scGPT:单细胞数据分析的AI革命,如何在1秒内处理千万级细胞数据?

揭秘scGPT:单细胞数据分析的AI革命,如何在1秒内处理千万级细胞数据? 【免费下载链接】scGPT 项目地址: https://gitcode.com/gh_mirrors/sc/scGPT 你是否曾为单细胞数据分析的复杂性而头疼?面对海量的基因表达数据&#x…

2026/7/21 15:23:26 阅读更多 →
Java代码审计:常见漏洞与安全开发实践

Java代码审计:常见漏洞与安全开发实践

1. Java代码审计概述:为什么需要关注不安全代码? 在Java开发领域,代码审计正逐渐成为保障应用安全的关键环节。去年某知名电商平台的数据泄露事件,根源就是一段未经验证的SQL拼接代码。这类安全问题往往隐藏在看似正常的业务逻辑中…

2026/7/21 15:23:26 阅读更多 →
深入解析TI C2000 eHRPWM模块:从架构原理到多模块同步实战

深入解析TI C2000 eHRPWM模块:从架构原理到多模块同步实战

1. 项目概述与核心价值 如果你正在开发电机驱动、数字电源或者任何需要精确控制功率开关的嵌入式系统,那么德州仪器(TI)C2000系列微控制器里的增强型高分辨率脉宽调制器(eHRPWM)模块,绝对是你绕不开的核心外…

2026/7/21 15:23:26 阅读更多 →
PyWxDump项目终结:从技术探索到合规反思的完整指南

PyWxDump项目终结:从技术探索到合规反思的完整指南

PyWxDump项目终结:从技术探索到合规反思的完整指南 【免费下载链接】PyWxDump 删库 项目地址: https://gitcode.com/GitHub_Trending/py/PyWxDump 还在寻找微信数据恢复工具却遭遇项目下架?想知道开源项目如何平衡技术创新与法律合规?…

2026/7/21 15:22:25 阅读更多 →

日新闻

Octane Render与C4D汉化版安装与优化指南

Octane Render与C4D汉化版安装与优化指南

1. Octane Render与C4D的黄金组合:为什么选择这个方案?在三维创作领域,渲染器的选择往往决定了作品的最终呈现质量和工作效率。作为Cinema 4D(C4D)用户,Octane Render的GPU加速特性与实时预览功能&#xff…

2026/7/21 0:00:19 阅读更多 →
GPMC接口设计:异步/同步模式与多路复用配置实战

GPMC接口设计:异步/同步模式与多路复用配置实战

1. GPMC接口设计:从硬件连接到软件配置的全局视角在嵌入式系统开发中,尤其是基于TI Sitara系列如AM263x这类高性能微控制器的项目里,外部存储器的扩展几乎是绕不开的一环。无论是存放大量非易失性代码的NOR Flash,还是作为高速数据…

2026/7/21 0:00:19 阅读更多 →
UE5 GAS框架下RPG被动技能系统:从核心原理到实战实现

UE5 GAS框架下RPG被动技能系统:从核心原理到实战实现

1. 项目概述:UE5 GAS RPG被动技能的核心价值在UE5里用GAS(Gameplay Ability System)做RPG游戏,主动技能像是你手里的武器,按一下打一下,逻辑直接,反馈也快。但被动技能,它更像是你身…

2026/7/21 0:00:19 阅读更多 →

周新闻

Go语言静态资源打包方案对比与实践指南

Go语言静态资源打包方案对比与实践指南

1. 项目背景与核心需求在Go语言开发中,我们经常需要处理静态资源文件的打包问题。无论是Web应用的模板文件、前端资源,还是配置文件、证书等,都需要随程序一起分发。传统做法是将这些文件与编译后的二进制文件放在同一目录下,但这…

2026/7/21 8:48:31 阅读更多 →
Go语言实现高性能LDAP认证服务的架构与实践

Go语言实现高性能LDAP认证服务的架构与实践

1. 项目背景与核心价值LDAP(轻量级目录访问协议)作为企业级身份认证的黄金标准,已经服务了超过80%的财富500强公司。我在金融科技领域实施统一认证体系时,发现传统Java方案存在启动慢、内存占用高等痛点。而Go语言凭借其协程并发模…

2026/7/21 5:34:47 阅读更多 →
【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

更多请点击: https://intelliparadigm.com 第一章:AI面试官实战指南的核心价值与适用场景 AI面试官并非替代人类HR的“黑箱工具”,而是以可解释、可审计、可迭代的方式,赋能招聘全链路的关键基础设施。其核心价值在于将主观经验沉…

2026/7/21 8:25:39 阅读更多 →

月新闻