抽象类可安全封装复用逻辑,普通函数调用纯虚函数是合法且常见的模板方法模式;但禁止在构造/析构函数中调用纯虚函数,虚析构函数必须声明,纯虚函数可有实现但应慎用。

抽象类里普通函数调用纯虚函数是常见且安全的模式
抽象类不是只用来“画接口”的空壳,它完全可以封装可复用逻辑,只要把变化点(比如具体算法、策略、数据生成方式)留给派生类通过纯虚函数实现。基类普通函数在内部调用 pure virtual function 是完全合法的,编译器允许,运行时也能正确分发到派生类实现。
典型场景是模板方法模式:基类定义流程骨架(如 execute()),其中调用若干 step1()、step2() 等纯虚函数,派生类只管填空,不用重复写流程控制逻辑。
- 普通函数可以有完整实现,用于统一预处理、日志、异常包装、资源管理等
- 纯虚函数必须由派生类提供,代表“必须定制的行为”,比如序列化格式、网络协议解析细节、图形渲染管线步骤
- 不要在构造函数或析构函数里直接调用纯虚函数——此时虚表尚未就绪或已销毁,会触发未定义行为,常见报错是
pure virtual function called
派生类重写纯虚函数后,普通函数自动获得新行为
一旦派生类实现了所有纯虚函数,它就不再是抽象类,能被实例化;更重要的是,基类中那些调用这些纯虚函数的普通函数,会立即使用派生类的新实现——无需修改基类代码,也不用加 virtual 修饰普通函数本身。
例如基类有个 run() 普通函数,内部调用了 validate() 和 process() 两个纯虚函数;Dog 类实现了这两个函数,那么 Dog{}.run() 就自然执行 Dog 版本的校验和处理逻辑。
- 普通函数不需声明为
virtual,它本身不参与多态分发,但它的行为依赖于所调用的虚/纯虚函数 - 如果普通函数也想被重写(比如想替换整个流程),那就得把它自己也声明为
virtual,但这和“配合使用”无关,属于另一层设计决策 - 注意:派生类实现纯虚函数时,建议加上
override,避免签名不一致导致意外继承基类默认实现(尤其参数 const/volatile 修饰差异)
别把普通函数塞进纯虚类里凑数
抽象类里混入大量带默认实现的普通函数,容易模糊契约边界。用户看到一个抽象类,预期是“这里定义了哪些能力我必须提供”,而不是“这里顺便帮你写了 70% 的逻辑,剩下 30% 你看着办”。过度复用反而削弱约束力。
- 如果某个功能对所有派生类都一样(比如通用日志前缀、错误码映射表),放普通函数合理
- 如果某个功能“多数情况一样,少数要改”,优先考虑用虚函数 + 默认实现,而不是纯虚函数;纯虚函数意味着“无例外,必须重写”
- 混合使用时,把普通函数设为
protected更安全——防止外部误调用未完成定制的中间状态 - 虚析构函数
virtual ~Base() = default;必须有,否则通过基类指针 delete 派生类对象会内存泄漏
纯虚函数其实可以有实现,但调用要谨慎
C++ 允许给纯虚函数提供定义(即函数体),语法上合法:virtual void f() = 0 { /* 实现 */ }。但这不是为了让你在基类里调用它,而是供派生类在需要时显式调用基类版本(Base::f())。绝大多数情况下没必要写。
- 写了实现也不影响该类仍是抽象类,仍不能实例化
- 派生类实现该函数后,若还想复用基类逻辑,必须显式写
Base::f(),不能靠隐式继承 - 除非有极强理由(比如所有派生类都要先做同一段初始化检查),否则别写;写了反而增加理解成本和误调风险
- 更推荐把共用逻辑抽成独立的
protected普通函数,由纯虚函数和派生类按需调用
关键点始终是:普通函数负责“不变的流程与结构”,纯虚函数负责“必须变的部分”。两者配合的本质,是把“做什么”和“怎么做”在类型系统里切开——编译器只管检查“做了没”,运行时才决定“怎么做的”。这层分离一旦松动,抽象类就退化成普通基类,契约就失效了。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











