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→ 组合如果不确定 →选择组合
企业数字化 ERP 产品动态
相关推荐
C++ 线程与协程的选择指南:从场景到实践 C 线程与协程的选择指南:从场景到实践一、引言:并发编程的两种范式线程(Thread)和协程(Coroutine)是 C 中实现并发的两种核心机制。线程由操作系统调度,是 CPU 调度的最小单位;协程由程序自身调度,是用户态的“轻量级线… · 2026/11/15 9:53:48
Centos非LVM根分区容量不足后扩容,对调硬盘挂载/ Centos非LVM根分区容量不足后扩容,对调硬盘挂载/
引言:为什么需要扩容非LVM根分区?在服务器运维中,CentOS系统根分区(/)容量不足是常见问题。非LVM(Logical Volume Manager)分区由于… · 2026/10/28 15:22:42
吉林市高压电工证真证怎么查电子证书?过期别慌,教你几步搞定 吉林市高压电工证真证怎么查电子证书?过期别慌,教你几步搞定 证书过期了不知道怎么复审延期?别急,这事儿很多人都遇到过。尤其是在吉林市,高压电工证作为特种作业操作证的一种,是很多电工从业者必须持有的“上岗通行证”。如果你的证书已经过期,或者即… · 2026/11/15 9:53:21
浙江湖州报名考电工证实操考试心里没底?掌握这些轻松拿电子证书 浙江湖州报名考电工证实操考试心里没底?掌握这些轻松拿电子证书 实操考试心里没底怕挂科?特别是像你这样的漯河应届工程类毕业生,第一次接触特种作业考试,面对高压电工、低压电工这些证书,难免会感到焦虑。别担心,掌握好考试内容、复习方法和答题技巧,… · 2026/11/15 9:53:21
工地太忙根本没时间复习考试?粤省事查电工证通过率揭秘 工地太忙根本没时间复习考试?粤省事查电工证通过率揭秘 工地太忙根本没时间复习考试,你是不是也经常因为项目进度紧、任务重,导致特种作业证考试一拖再拖?特别是在南阳卧龙区这种施工项目集中的区域,项目现场管理员每天都被各种事务缠身,根本没时间准备… · 2026/11/15 9:53:21
若尔盖县电工证怎么考官方报名入口怎么找 若尔盖县电工证怎么考官方报名入口怎么找 之前考的证在外省能不能转过来?这是很多电工朋友最关心的问题之一。在若尔盖县,电工证的报考流程、报名入口、考试内容、有效期和年审要求都跟其他地方略有不同,但核心流程是相通的。 官方报名入口… · 2026/11/15 9:53:21
低压电工证的的级别全国通用吗?真实经历告诉你答案 低压电工证的的级别全国通用吗?真实经历告诉你答案 之前考的证在外省能不能转过来?我认识一个在河南鹤壁打工的建筑工人,他去年在老家考了低压电工证,结果到了河南项目上,甲方说证书不符合当地要求,差点耽误了工期。这事让他意识到,… · 2026/11/15 9:53:21
三门峡转岗从业者必看:消防证电工证查询系统多久拿证?别再等机会 三门峡转岗从业者必看:消防证电工证查询系统多久拿证?别再等机会 急需特种作业证才能上岗找工作,很多人在三门峡这类三四线城市,因为行业转型、岗位变动,不得不重新考取特种作业证。特别是像电工证这种,既是基础技能,又是很多岗位的硬性门槛。现在很多… · 2026/11/15 9:52:20
长春初级电工证怎么考?通过率揭秘全在这里 长春初级电工证怎么考?通过率揭秘全在这里 之前考的证在外省能不能转过来?这几乎是每个想从事电工工作的人都会问的问题。特别是那些在外地打拼多年、手里握着一张“外地电工证”的人,到了长春想继续干这一行,第一反应就是“我的证能用吗?”。今天我们就… · 2026/11/15 0:00:50
国内电工证报考条件与考试时间 低压电工证怎么考?学历不高也能报名,官方报名入口全在这 你是不是经常听说“电工证是入行门槛”,但又担心自己学历不够、经验不足,根本报不了名?别急,今天就带你从零了解【国内电工证】的报考流程、考试内容,还有怎么在 官方报名入口… · 2026/11/15 0:00:50
电工作业时如何断电?电子证书到底能不能全国通用? 电工作业时如何断电?电子证书到底能不能全国通用? 实操考试心里没底怕挂科,特别是电工作业时如何断电这一操作,不仅关系到你能不能拿到证书,更关系到以后能不能顺利上岗。很多人觉得这不过就是“断个电”,但其实这里面的门道可不少,稍有不慎就可能出错… · 2026/11/15 0:00:50
长春初级电工证怎么考?通过率揭秘全在这里 长春初级电工证怎么考?通过率揭秘全在这里 之前考的证在外省能不能转过来?这几乎是每个想从事电工工作的人都会问的问题。特别是那些在外地打拼多年、手里握着一张“外地电工证”的人,到了长春想继续干这一行,第一反应就是“我的证能用吗?”。今天我们就… · 2026/11/15 0:00:50
国内电工证报考条件与考试时间 低压电工证怎么考?学历不高也能报名,官方报名入口全在这 你是不是经常听说“电工证是入行门槛”,但又担心自己学历不够、经验不足,根本报不了名?别急,今天就带你从零了解【国内电工证】的报考流程、考试内容,还有怎么在 官方报名入口… · 2026/11/15 0:00:50
电工作业时如何断电?电子证书到底能不能全国通用? 电工作业时如何断电?电子证书到底能不能全国通用? 实操考试心里没底怕挂科,特别是电工作业时如何断电这一操作,不仅关系到你能不能拿到证书,更关系到以后能不能顺利上岗。很多人觉得这不过就是“断个电”,但其实这里面的门道可不少,稍有不慎就可能出错… · 2026/11/15 0:00:50
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/15 1:51:20
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/15 4:02:19
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/15 3:02:05