当继承体系出现多继承无逻辑层级、大量using或重复重写虚函数、protected成员滥用、单元测试依赖整个继承树时,应停止继承,改用组合——将行为维度抽为独立接口及其实现类,主类仅持接口指针并注入依赖。

直接说结论:当继承体系开始出现“改一个地方要动十几个类”“新加一个功能得复制粘贴五份相似代码”“子类意外暴露基类内部字段”时,就该停手,用组合重写——不是全盘推翻,而是把“行为维度”抽成独立对象,让主类只持有一个接口指针或引用。
怎么识别该拆的继承链
别等类爆炸才动手。以下现象出现任意一条,就是信号:
-
class A : public B, public C, public D这种多继承,尤其当B、C、D之间没逻辑层级关系时(比如NetworkClient同时继承Logger和ConfigReader) - 派生类里大量
using Base::xxx;或反复重写同一组虚函数(如serialize()、validate()、to_json()),但每份实现只是改了序列化格式 - 基类里塞了太多
protected成员,子类靠直接读写base_data_工作,而不是调用接口 - 单元测试必须 mock 整个继承树才能测一个子类——说明依赖太深,无法隔离
用组合替换继承的关键三步
核心不是删掉 :,而是把“变的部分”拎出来,变成可插拔的部件。以一个老项目里的 ReportGenerator 类为例,它原本继承自 PDFExporter、CSVExporter、HTMLRenderer,还混着 PermissionChecker 的逻辑:
第一步:定义行为接口,不带数据
struct Exporter {
virtual ~Exporter() = default;
virtual std::string export_data(const ReportData& data) = 0;
};
struct PermissionChecker {
virtual ~PermissionChecker() = default;
virtual bool can_access(const User& user) const = 0;
};
第二步:把原继承来的功能转为具体实现类,生命周期由使用者控制
- 不再让
ReportGenerator继承PDFExporter,而是写class PDFExporterImpl final : public Exporter - 原来硬编码在基类里的权限检查逻辑,抽成
class RoleBasedChecker final : public PermissionChecker - 所有实现类都用
final标记,禁止进一步继承——组合链到此为止
第三步:主类只持接口指针,构造时注入
class ReportGenerator {
std::unique_ptr<exporter> exporter_;
std::unique_ptr<permissionchecker> checker_;
public:
ReportGenerator(std::unique_ptr<exporter> e, std::unique_ptr<permissionchecker> c)
: exporter_(std::move(e)), checker_(std::move(c)) {}
<pre class="brush:php;toolbar:false;">std::string generate(const ReportData& data, const User& user) {
if (!checker_->can_access(user)) throw std::runtime_error("no permission");
return exporter_->export_data(data);
}};
容易踩的坑:生命周期和虚析构
组合不是加个成员变量就完事。下面这些点不处理,运行时会崩:
- 所有接口类的析构函数必须是
virtual或= default,否则std::unique_ptr<exporter></exporter>删除派生对象时会漏掉派生部分析构——这是最常被忽略的崩溃源 - 别用裸指针管理组合对象:
Exporter* exporter_看似简单,但谁负责delete?换成std::unique_ptr或std::shared_ptr,明确所有权 - 如果部分类需要访问主类状态(比如
Exporter需要读取ReportGenerator的缓存),不要反向传this指针——改用回调函数对象(std::function<const cache></const>)或接口抽象,避免循环依赖 - 模板参数不能替代接口抽象:有人用
template<typename e> class ReportGenerator</typename>替代std::unique_ptr<exporter></exporter>,这会导致编译期膨胀、无法运行时切换策略,违背解耦初衷
真正难的不是语法,是判断哪些东西该放进组合部件、哪些必须留在主类里。一个经验法则是:只要两个功能可以独立变化、独立测试、甚至由不同团队维护,它们就不该挤在一个继承链里。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











