c++中复用重复逻辑的惯用解法是基类定义虚函数协议并封装公共流程,派生类仅重写变化点;编译期确定行为可用模板策略替代,但无运行时多态能力。

基类里放虚函数,派生类只管实现具体逻辑
重复的派生类逻辑,本质是“相同调用流程 + 不同执行细节”。C++ 的惯用解法不是靠继承代码,而是靠 virtual 函数定义协议,把公共流程收进基类,把变化点留给派生类去重写。
比如多个派生类都做「校验 → 处理 → 记日志」三步,但每步内容不同:
基类里写固定骨架:
class Base {
public:
void execute() {
if (!validate()) return;
do_work();
log_result();
}
protected:
virtual bool validate() = 0;
virtual void do_work() = 0;
virtual void log_result() { /* 默认实现,可选 */ }
};
派生类只覆盖需要变的部分,不碰流程:
class DerivedA : public Base {
protected:
bool validate() override { return input_ > 0; }
void do_work() override { result_ = input_ * 2; }
};
非虚函数+模板参数也能复用,但适用场景更窄
如果重复逻辑不涉及运行时多态(比如全是编译期确定的行为),用模板参数传策略比虚函数更轻量。适合工具类、算法包装等场景。
例如统一做资源清理的封装:
template<typename t typename deleter>
class ResourceManager {
T* ptr_;
Deleter deleter_;
public:
ResourceManager(T* p, Deleter d) : ptr_(p), deleter_(d) {}
~ResourceManager() { if (ptr_) deleter_(ptr_); }
};</typename>
使用时传入不同删除逻辑,不用写多个派生类:
auto res1 = ResourceManager<int void>(
new int(42), [](int* p) { delete p; });
auto res2 = ResourceManager<file void>(
fopen("a.txt", "r"), [](FILE* f) { fclose(f); });</file></int>
注意点:
- 模板方案无法在运行时切换行为,所有类型必须编译期已知
- 虚函数有 vtable 开销,但现代 CPU 上几乎可忽略;模板则可能膨胀代码体积
- 如果派生类之间共享数据成员(如缓存、状态标志),还是得放基类里,且需考虑初始化顺序
别把构造逻辑塞进基类构造函数
常见错误:在基类构造函数里调用虚函数,结果调不到派生类实现——因为此时派生类部分还没构造。
例如:
class Base {
public:
Base() { init(); } // 危险!此时派生类 ctor 还没跑
protected:
virtual void init() = 0;
};
这会导致未定义行为或调用纯虚函数崩溃。正确做法是:
- 用两阶段初始化:先构造,再显式调用
init() - 或用工厂函数返回智能指针,在对象完全构造后再执行逻辑
- 或把初始化逻辑拆成 protected 成员函数,由派生类 ctor 主动调用(但要确保基类成员已就绪)
多重继承时小心虚基类和函数遮蔽
如果提取的逻辑横跨多个维度(比如“可序列化”+“可撤销”),用多重继承比单继承更灵活,但要注意:
虚基类用于避免菱形继承中的重复子对象,但会引入额外指针开销和构造顺序复杂性。
函数名遮蔽更常见:派生类写了同名函数(哪怕签名不同),会直接隐藏基类所有重载版本。
- 用
using Base::func;显式引入基类重载 - 避免在派生类中仅靠参数类型差异重载,容易误遮蔽
- 虚函数重写必须严格匹配签名(包括 const、noexcept、引用限定符),否则就是新函数,不是重写
基类提取逻辑这事,最难的不是语法,而是判断哪些该抽象、哪些该保留具体。很多人过早泛化,结果基类堆满空虚函数和默认实现,反而让每个派生类都要重写一堆无意义的桩。真正值得上移到基类的,一定是高频出现、结构稳定、且变化点清晰隔离的模式。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











