C++ 继承与组合的选择指南:从设计原则到实践
C 继承与组合的选择指南从设计原则到实践一、引言面向对象设计的核心抉择在面向对象设计中代码复用的两种主要方式是继承(Inheritance)和组合(Composition)。继承表达“is-a”关系组合表达“has-a”关系。选择哪种方式直接影响代码的灵活性、可维护性和可扩展性。C 同时支持这两种机制但现代设计理念如 GoF 设计模式明确建议优先使用组合而非继承。理解这一原则的深层原因以及何时继承仍然是正确的选择是成熟 C 开发者的标志。二、核心概念速览| 维度 | 继承 (Inheritance) | 组合 (Composition) ||------|-------------------|-------------------|| 关系语义 |Is-A是一个 |Has-A有一个 || 耦合度 |高白盒复用依赖父类实现 |低黑盒复用依赖接口 || 运行时灵活性 | 差编译期固定 |好可动态替换 || 访问控制 | 派生类可访问 protected 成员 | 完全封装通过接口访问 || 代码复用 | 自动获得父类接口和实现 | 需要手动转发可封装 || 破坏封装 |容易暴露父类细节 |不容易|三、使用继承的场景3.1 真正的 Is-A 关系// ✓ 正确的继承Cat is-a Animal class Animal { public: virtual ~Animal() default; virtual void makeSound() const 0; virtual void move() 0; protected: std::string name_; int age_; }; class Cat : public Animal { public: void makeSound() const override { std::cout Meow std::endl; } void move() override { /* 猫的移动方式 */ } }; class Dog : public Animal { public: void makeSound() const override { std::cout Woof std::endl; } void move() override { /* 狗的移动方式 */ } };3.2 多态替换——里氏替换原则// ✓ 继承用于多态通过基类接口操作不同的派生类对象 void processAnimals(const std::vectorstd::unique_ptrAnimal animals) { for (const auto animal : animals) { animal-makeSound(); // 多态调用 animal-move(); // 不同动物有不同行为 } } int main() { std::vectorstd::unique_ptrAnimal zoo; zoo.push_back(std::make_uniqueCat()); zoo.push_back(std::make_uniqueDog()); processAnimals(zoo); // 通过 Animal 接口操作 }3.3 模板方法模式// ✓ 继承实现模板方法基类定义算法骨架派生类实现细节 class DataProcessor { public: virtual ~DataProcessor() default; // 模板方法定义算法骨架 void process() { loadData(); transformData(); validateData(); saveData(); } protected: virtual void loadData() 0; virtual void transformData() 0; virtual void validateData() { /* 默认实现 */ } virtual void saveData() 0; }; class CSVProcessor : public DataProcessor { protected: void loadData() override { /* 读取 CSV */ } void transformData() override { /* CSV 特有转换 */ } void saveData() override { /* 保存为 CSV */ } };3.4 基于类型的特殊处理// ✓ 继承用于类型区分和处理 class Message { public: virtual ~Message() default; virtual MessageType type() const 0; }; class TextMessage : public Message { public: MessageType type() const override { return MessageType::TEXT; } std::string content_; }; class ImageMessage : public Message { public: MessageType type() const override { return MessageType::IMAGE; } std::vectoruint8_t imageData_; }; void dispatchMessage(const std::unique_ptrMessage msg) { switch (msg-type()) { case MessageType::TEXT: /* 处理文本 */ break; case MessageType::IMAGE: /* 处理图片 */ break; } }四、使用组合的场景4.1 Has-A 关系// ✓ 组合Car has-a Engine class Engine { public: void start() { std::cout Engine started std::endl; } void stop() { std::cout Engine stopped std::endl; } int getHorsePower() const { return 200; } }; class Car { Engine engine_; // 组合Car 拥有 Engine std::vectorWheel wheels_; // 组合Car 拥有 4 个 Wheel public: void start() { engine_.start(); } void stop() { engine_.stop(); } }; // Car 不是 Engine——它是通过 Engine 实现功能4.2 多重“继承”效果的替代// ❌ 继承方式C 虽支持多继承但复杂且有问题 class Bird : public Flyable, public Swimmable { }; // 菱形问题 // ✓ 组合方式更清晰、更灵活 class Bird { FlightBehavior flightBehavior_; // 飞行能力 SwimBehavior swimBehavior_; // 游泳能力 SingBehavior singBehavior_; // 鸣叫能力 public: void fly() { flightBehavior_.fly(); } void swim() { swimBehavior_.swim(); } void sing() { singBehavior_.sing(); } void setFlightBehavior(FlightBehavior fb) { flightBehavior_ std::move(fb); } }; // 可以动态改变行为继承做不到4.3 运行时替换实现策略模式// ✓ 组合实现策略模式运行时切换算法 class SortStrategy { public: virtual ~SortStrategy() default; virtual void sort(std::vectorint data) const 0; }; class QuickSort : public SortStrategy { public: void sort(std::vectorint data) const override { /* 快速排序 */ } }; class MergeSort : public SortStrategy { public: void sort(std::vectorint data) const override { /* 归并排序 */ } }; class DataSorter { std::unique_ptrSortStrategy strategy_; public: void setStrategy(std::unique_ptrSortStrategy s) { strategy_ std::move(s); } void process(std::vectorint data) { strategy_-sort(data); // 委托给组合对象 } }; // 可以随时切换策略继承无法做到五、什么时候不应该用继承5.1 仅仅为了代码复用// ❌ 错误仅为了复用代码而继承 class FileReader { public: std::string readAll(const std::string path) { /* 读取文件 */ } }; // NetworkReader 不是 FileReader class NetworkReader : public FileReader { // ❌ 错误的继承 // 继承了 readAll()但网络读取逻辑完全不同 }; // ✓ 正确提取公共接口各自实现 class DataReader { public: virtual ~DataReader() default; virtual std::string readAll(const std::string source) 0; }; class FileReader : public DataReader { public: std::string readAll(const std::string path) override { /* 读取文件 */ } }; class NetworkReader : public DataReader { public: std::string readAll(const std::string url) override { /* 网络读取 */ } };5.2 破坏封装的继承// ❌ 错误派生类依赖基类实现细节 class Stack : public std::vectorint { // Stack 不是 Vector public: void push(int value) { push_back(value); } int pop() { int v back(); pop_back(); return v; } // 问题用户可以直接调用 vector 的方法破坏栈语义 }; // ✓ 正确使用组合 class Stack { std::vectorint data_; // 私有组合 public: void push(int value) { data_.push_back(value); } int pop() { int v data_.back(); data_.pop_back(); return v; } // vector 的接口不会泄露 };六、选择决策流程是是是否是否否是是否否:Has-A仅复用代码无明确关系需要代码复用或类型关系关系是 Is-A?需要多态?基类有纯虚接口?使用继承接口继承基类实现稳定?可以考虑继承改用组合需要复用基类实现?会导致紧耦合?使用组合可以考虑继承使用组合使用组合注意虚析构函数里氏替换原则不破坏封装注意转发接口生命周期管理不暴露内部细节七、继承与组合的代码对比| 场景 | 继承实现 | 组合实现 ||------|----------|----------|| 代码量 | 少自动继承接口 | 多需手动转发 || 耦合度 | 高依赖基类 | 低依赖接口 || 运行时灵活性 | 差类型固定 | 好可替换组件 || 封装保护 | 弱protected 暴露细节 | 强完全隐藏 || 适合 | 稳定的 Is-A 关系 | Has-A 关系、需要灵活性 |八、总结继承和组合的选择可以归纳为以下核心原则优先使用组合除非能明确论证继承的合理性否则应该使用组合。这是 GoF 设计模式的核心建议也是现代 C 的最佳实践。组合降低了耦合度提高了灵活性且更容易维护。继承用于真正的 Is-A 关系继承应该严格用于表达类型层次——派生类必须是基类的一种。如果关系不明确用组合。关键测试派生类对象是否可以安全地在任何期望基类对象的场合使用里氏替换原则。组合用于 Has-A 关系和策略模式当一个对象包含另一个对象作为其一部分时如 Car 有 Engine使用组合。当需要在运行时动态替换行为策略模式时组合是唯一的选项。避免仅为了代码复用而继承代码复用是继承的副产品不应该是继承的主要目的。为了复用代码而继承通常会导致脆弱的设计。接口继承优于实现继承优先定义纯虚接口抽象基类让派生类实现而不是继承一个有状态和实现的基类。这能最大程度降低耦合。简洁的选择准则如果 B 是 AB is-a A→ 继承如果 B 有 AB has-a A→ 组合如果不确定 →选择组合

相关新闻

C++ 线程与协程的选择指南:从场景到实践

C++ 线程与协程的选择指南:从场景到实践

C 线程与协程的选择指南:从场景到实践一、引言:并发编程的两种范式线程(Thread)和协程(Coroutine)是 C 中实现并发的两种核心机制。线程由操作系统调度,是 CPU 调度的最小单位;协程由程序自身调度,是用户态的“轻量级线…

2026/10/12 16:28:48 阅读更多 →
【AI多模态学习终极指南】:20年实战专家拆解5大核心概念、3大认知误区与落地避坑清单

【AI多模态学习终极指南】:20年实战专家拆解5大核心概念、3大认知误区与落地避坑清单

更多请点击: https://kaifayun.com 第一章:AI多模态学习的定义与演进脉络 AI多模态学习是指模型能够协同理解、对齐并推理来自多种感知模态(如视觉、语言、语音、文本、时序信号等)的信息,从而实现超越单模态能力的泛…

2026/10/14 11:46:02 阅读更多 →
P10386 [蓝桥杯 2024 省 A] 五子棋对弈题解复盘

P10386 [蓝桥杯 2024 省 A] 五子棋对弈题解复盘

五子棋平局(蓝桥杯填空题)题解复盘 基本信息项目内容题目编号、来源蓝桥杯 五子棋平局(结果填空题)训练层级B DFS 回溯知识版块DFS、回溯、棋盘枚举、状态压缩 解题前・关键信号识别维度分析目标、约束、底层结构目标&#xff1a…

2026/10/12 20:28:39 阅读更多 →

最新新闻

STM32MP157参考手册高效阅读指南:从寄存器配置到启动流程

STM32MP157参考手册高效阅读指南:从寄存器配置到启动流程

简介:本手册是ST官方发布的STM32MP157微处理器参考手册,覆盖双核架构、存储管理、总线和各类外设的完整说明,适用于嵌入式软硬件开发、驱动移植与工业控制等应用场景,既可帮助初学者建立芯片整体认知,也可供有经验的工…

2026/10/15 4:36:52 阅读更多 →
小团队本地生活破局:高杠杆轻启动实战指南

小团队本地生活破局:高杠杆轻启动实战指南

这两年聊本地生活的人特别多,身边做运营、做策划的朋友,不少都蠢蠢欲动想进场。可真正下场试过的人大概率有个共同感受:这不是一个缺机会的行业,缺的是把机会变成现金流的那套执行。我一直觉得,本地生活最大的迷人之处…

2026/10/15 4:36:52 阅读更多 →
C#开发SMT贴片MAPPING图软件:XML坐标可视化与蛇形走位路径规划实现

C#开发SMT贴片MAPPING图软件:XML坐标可视化与蛇形走位路径规划实现

做贴片产线或者电子组装这一行的朋友,应该都体会过拿到一坨坐标数据却要对着Excel表格脑补位置的痛苦。生产线上的物料、元件、站位,如果只有一个XML文件摆在那儿,人是没法直观地看到每一颗料该放在哪、一共有多少、用什么顺序去拿的。我最近…

2026/10/15 4:36:52 阅读更多 →
工业标识解决方案详解:从打标选型到产线追溯落地

工业标识解决方案详解:从打标选型到产线追溯落地

1. 一条产线上的追溯难题:能打码的设备买了,扫码率却只有85%先讲一个我参与过的现场。某电子配件厂,产线刚上了一台打标设备,花了几十万,设备商调试完就走了。结果不到一个月,客户投诉批次混料,…

2026/10/15 4:36:52 阅读更多 →
树的三种存储结构详解:双亲、孩子、孩子兄弟表示法对比

树的三种存储结构详解:双亲、孩子、孩子兄弟表示法对比

树的存储结构一直是一个让初学者挠头的话题。我记得第一次接触二叉树时,满脑子都是“这玩意到底在内存里长什么样”的疑惑,链表、数组这些线性结构闭着眼睛都能画出来,但树一旦分支多起来,内存布局就成了抽象概念。后来在实际项目…

2026/10/15 4:36:52 阅读更多 →
ABAC权限管理实战:从属性建模到轻量级Java策略引擎落地

ABAC权限管理实战:从属性建模到轻量级Java策略引擎落地

1. 为什么需要ABAC:权限管理走到哪一步了做过几年Java后端,权限模块是每个系统都躲不开的“硬骨头”。刚开始用角色(RBAC),后来发现业务一复杂,角色就开始爆炸:一个角色对应几十种权限组合&…

2026/10/15 4:35:51 阅读更多 →

日新闻

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

1. 为什么选择容器化方式部署 LoRa 网络服务LoRa 物联网项目落地时,网络服务器是整个链路的核心枢纽。它负责接收网关上报的数据包、完成去重与解密、再把有效载荷分发给上层应用。早期大家习惯把网络服务器直接装在物理机上,依赖一堆系统库和特定版本的…

2026/10/15 0:00:46 阅读更多 →
共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

1. 共享推理服务里,扩缩容到底难在哪做过大模型推理服务部署的人,大概都经历过这样的场景:一个团队共用一套推理集群,A 业务跑的是 7B 的小模型,B 业务跑的是 72B 的大模型,C 业务白天流量高、晚上几乎归零…

2026/10/15 0:00:46 阅读更多 →
Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

1. 引子:一次内核恐慌引发的追查先从一个真实场景说起。某次我给一台老旧的嵌入式设备折腾系统裁减,为了把启动时间压进两秒以内,我手贱地把一串自认为“没用”的 systemd 服务给 disable 了,其中就包括systemd-remount-fs.servic…

2026/10/15 0:00:46 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/14 16:30:29 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 7:53:34 阅读更多 →