当前位置: 首页 > news >正文

Item39--明智而审慎地使用 private 继承

人话版本

1. 正常人的做法:组合 (Composition)

想象你要造一辆 “汽车”。 你也知道,汽车需要一个 “引擎” 才能跑。

最自然的做法是什么? 你在车身里留个位置,塞一个引擎进去。

  • 这就叫 “组合”(Item 38 讲的)。
  • 关系是:汽车“有一个”引擎 (Car has-a Engine)。
  • 这很合理,对吧?

2. 怪人的做法:私有继承 (Private Inheritance)

Item 39 讲的是一种很怪的做法: 你造汽车的时候,让汽车继承引擎,但是设为 Private(私有)

这是什么意思呢?

  • Public 继承(公开):你说“汽车就是一种引擎”。(这显然是胡说八道,车是车,引擎是引擎)。
  • Private 继承(私有):你偷偷地把引擎的所有功能“吃”进汽车的身体里。
    • 在汽车内部,你可以像使用自己的手脚一样使用引擎的功能。
    • 但在外人看来,绝不承认这辆车跟引擎有任何亲戚关系。外人把这辆车仅仅看作车。

简单说:私有继承 = “我偷偷把你的本事学过来自己用,但我对外不承认你是我的爸爸”。


3. 既然“组合”那么好,为什么还要学这个“怪招”?

既然直接把引擎塞进去(组合)就能解决问题,为什么非要搞这种偷偷摸摸的继承?

只有两种极端的特殊情况,你才会被迫使用这招:

情况一:你想篡改它的内部逻辑(重写虚函数)

假设那个 “引擎” 有个功能叫 发出声音()

  • 如果是组合(塞进去): 你只能用它。引擎怎么叫,车就怎么叫。你控制不了它。
  • 如果是私有继承(吃进去): 因为你实际上是它的子类(虽然是地下的),你有特权!你可以修改(Override)发出声音() 这个功能。

小白结论 1: 如果你只想它的功能,用“组合”。 如果你想修改它的内部功能(特别是虚函数),被迫用“私有继承”。

情况二:为了省下极其微小的空间(空基类优化)

假设你要给汽车贴一个 “环保标签”。 这个“标签”类很神奇,它里面是空的,没有数据,占用的内存理论上是 0。

  • 如果是组合(塞进去): C++ 很死板。哪怕是 0 大小的东西,如果你把它作为成员变量塞进对象里,编译器为了安全,强行会给它分配 1 个字节(甚至为了对齐变成 4 或 8 个字节)。这就好比你为了装一张纸,不得不背了一个书包。浪费了空间。
  • 如果是私有继承(吃进去): C++ 有个特殊规则:如果继承一个空的东西,编译器可以把它“融合”掉,不占额外空间。就好比你把那张纸直接贴在脑门上,不占地方。

小白结论 2: 如果你要包含的东西是的(没数据),而且你极度在乎内存(哪怕几字节),被迫用“私有继承”。


最终总结(人话版)

  1. 绝大多数时候(99%): 忘掉 Item 39。当你想用别人的代码时,直接把它作为成员变量放在你的类里(用组合)。这最简单,也最正常。

  2. 只有当你遇到这两种倒霉情况(1%)

    • 你想改写那个类的虚函数。
    • 那个类是空的,而你在这个项目里非常穷(内存极其紧张)。

    这时,你才把 Item 39 拿出来用一下:使用私有继承。

非人话版本

1.私有继承的本质

首先,我们需要明确私有继承(Private Inheritance)的两个核心特性:

  1. 编译器不会自动转换指针: 如果你使用私有继承(class D : private B),编译器不会D* 转换为 B*。这意味着在外部看来,D 并不是 一个 B。它没有 "Is-a" 关系。
  2. 继承下来的成员全变私有: 基类 Bpublicprotected 成员,到了 D 里面全变成了 private。这意味着这些功能是给 D 自己用的,不是给 D 的用户用的。

结论: 私有继承纯粹是为了实现细节。它的含义和组合(Composition)完全一样:"Is-implemented-in-terms-of"(根据某物实现出)。


2. 既然含义一样,为什么首选“组合”?

在 Item 38 中我们讲过,尽可能用组合。因为组合有明显的优势:

  • 解耦(Decoupling): 可以在不重新编译包含类的情况下修改被包含类的定义(如果使用指针指向实现)。
  • 逻辑清晰: 明确的“拥有”关系。
  • 防止命名空间污染: 派生类不会意外地继承基类的变量名或函数名。

3. 必须使用“私有继承”的两个特殊场景

既然组合更好,那只有在组合做不到,或者代价太大时,我们才考虑私有继承。只要满足以下两点中的一点,你就需要它:

场景 A:你需要访问基类的 protected 成员,或者重写 virtual 函数

假设有一个 Timer 类,它可以在设定的时间触发 onTick 函数:

class Timer {
public:explicit Timer(int tickFrequency);virtual void onTick() const; // 自动被调用,默认做一些记录
};

现在你想设计一个 Widget 类,你需要它每秒钟记录一次数据。 需求: Widget 需要利用 Timer 的计时功能,并且需要重写 onTick 来做特定的记录。

方案 1:使用组合(如果不重写虚函数) 如果是简单的调用,组合很好。但 Timer::onTick 是虚函数,必须通过继承才能重写它。

方案 2:使用私有继承(可行)

class Widget : private Timer {
private:// 私有继承允许我们重写虚函数virtual void onTick() const override {// Widget 特定的动作...}
};

这样 Widget 就能利用 Timer 的机制,同时向外界隐藏了“我其实是个 Timer”的事实(因为是 private 继承,Widget* 不能转为 Timer*)。

方案 3:组合 + 嵌套类(比私有继承更优雅,但代码多一点) 如果你极度不想用私有继承,可以在 Widget 内部搞一个私有的嵌套类去公有继承 Timer

class Widget {
private:// 内部类,专门用来继承 Timerclass WidgetTimer : public Timer {public:virtual void onTick() const override {// 回调 Widget 的逻辑}};WidgetTimer timer; // 组合
};

Scott Meyers 认为方案 3 通常比私有继承更好,因为它把 WidgetTimer 的耦合度降到了最低(例如 Widget 的派生类无法访问 Timer 的成员)。但如果嫌麻烦,私有继承是这种场景下的直接解决方案。


场景 B:空间最优化 (EBO - Empty Base Optimization)

这是私有继承最“硬核”的理由。

在 C++ 中,空类(Empty Class) 并不是没有大小的。

class Empty {}; // 没有数据成员,没有虚函数sizeof(Empty); // 结果通常是 1,而不是 0

为什么? 因为 C++ 规定每个对象必须有独立的内存地址。如果是 0,那数组 Empty e[10] 怎么寻址?

如果我们用组合

class HoldsAnInt {
private:int x;Empty e; // 组合一个空类
};

由于内存对齐(Alignment),sizeof(HoldsAnInt) 可能会变成 8(4字节 int + 1字节 Empty + 3字节填充)。这就造成了空间浪费。

但是! 如果我们使用私有继承

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

C++ 有一个特殊的规则叫 EBO (Empty Base Optimization,空基类优化)。如果基类是空的,编译器允许派生类的起始地址和基类部分重叠,从而不需要额外的空间。 此时,sizeof(HoldsAnInt) 就是 4

应用场景: 这听起来像是在扣毫厘,但在库开发(Library Development)中极其重要。比如 STL 中的很多对象(如 unary_function, 内存分配器 allocator 等)往往是空类。如果你在开发一个会被成千上万个对象实例化的类,节省这几字节的 padding 是非常有意义的。


4. 总结

Item 39 的核心决策逻辑:

  1. 默认选择:总是优先使用 组合 (Composition) 来表示 "Is-implemented-in-terms-of"。
  2. 例外情况:只有在以下两种情况下,才考虑使用 私有继承 (Private Inheritance)
    • 需要重写虚函数:你需要 override 基类的 virtual 函数,或者你需要访问基类的 protected 成员。
    • 极致的空间优化:你需要利用 EBO (空基类优化) 来节省内存(当被包含的类是空类时)。

一句话总结: "私有继承"意味着“利用它实现(Implementation Only)”,这通常用“组合”就能做到而且做得更好;除非你需要触碰“受保护的成员/虚函数”或者“为了省那几字节的内存”。

http://www.zskr.cn/news/117181.html

相关文章:

  • 2025年国内正规的工业冷却塔实力厂家哪家靠谱,冷却塔填料/方形横流冷却塔/工业冷却塔/圆形逆流冷却塔/工业冷却塔定做厂家哪家权威 - 品牌推荐师
  • 高危漏洞CVE-2025-54004:WooCommerce前台管理插件权限绕过漏洞剖析
  • AutoHotkey v2 (AHK) windows自动化使用
  • 想做安全副业却纠结方向?漏洞挖掘、技术博客、竞赛奖金实战哪个更适合你?
  • ConvLSTM实战:构建交通流量预测系统
  • 一文帮你总结2025年最新获客系统品牌有哪些,必看! - 品牌策略主理人
  • Conda环境管理:比传统pip快3倍的依赖解决方案
  • Redmi Note 12 Pro Speed-开启开发者选项
  • 利用wan2.1协议快速构建网络通信原型
  • TestDisk数据恢复实战:从分区丢失到文件找回的完整指南
  • 盲盒抽赏小程序开发运营指南:从合规架构到百万盈利的全链路拆解
  • 使用 C# 将 DataTable 和 Excel 数据互转
  • 敏捷协作中的心理测试:消除团队沟通的隐藏BUG
  • 2025搭子经济风口:组局小程序开发落地、盈利模式与风险防控手册
  • 源网荷储充一体化平台:安科瑞EMS微电网能源管理系统介绍
  • 磁链观测器的探索之旅:从仿真到闭环代码实现
  • 33、Linux线程同步与互斥
  • 终极指南:macOS iSCSI启动器完整配置与使用详解
  • Taiga开源项目管理工具:2025年敏捷开发终极指南
  • SimpRead浏览器扩展图标终极适配指南:从16px到128px的完整解析
  • 【AI】免费的代价?Google AI Studio 使用指南与 Cherry Studio + MCP 实战教程
  • MCP概念和实践
  • 【tRPC-Go 框架】深度解析:特性、架构及与主流RPC框架对比
  • 【Go 语言】核心特性、基础语法及面试题
  • 线性自抗扰控制:包含线性跟踪微分器、扩张状态观测器及控制律的STM32F1 C代码与实践
  • 能控制计算机桌面的多模态AI agent框架
  • 3分钟免费拥有macOS精致鼠标指针:Windows和Linux完美适配指南
  • Matlab Simulink 基于自适应的永磁同步电机无位置传感器控制系统 以PMSM做为控制对像
  • 国自然科学基金本子拟解决关键问题与创新点,如何利用AI分别进行辅助?
  • NocoDB容器化部署架构深度解析:从单机到云原生演进路径