纯虚函数 != 没有实现
C++ 允许为纯虚函数编写函数体
核心矛盾:强制性 vs. 便利性
在普通的虚函数中,如果你提供了实现,派生类可以选择性地重写。如果你忘了重写,程序会默认运行基类的版本。
而在某些设计场景下,你希望达成以下两个看似矛盾的目标:
- 强制性:我要求每一个派生类必须显式地重写这个函数,不能偷懒直接用基类的(防止意外的默认行为)。
- 复用性:虽然每个子类都要重写,但它们重写时的“核心逻辑”其实是一样的。我想把这部分公共代码写在基类里供它们调用。
这就是纯虚函数实现的用武之地。
实例
// IRenderer.h
class IRenderer {
public:
// 纯虚函数:强制派生类必须重写
virtual void SetViewportSize(int w, int h) = 0;
virtual ~IRenderer() {}
};
// IRenderer.cpp
// 居然可以有实现!
void IRenderer::SetViewportSize(int w, int h) {
std::cout << "Updating internal state: " << w << "x" << h << std::endl;
// 这里存放所有渲染器通用的逻辑(如更新成员变量、日志记录等)
}
// ---------------------------------------------------------
// OpenGLRenderer.cpp
class OpenGLRenderer : public IRenderer {
public:
void SetViewportSize(int w, int h) override {
// 1. 调用基类的纯虚实现(复用通用逻辑)
IRenderer::SetViewportSize(int w, int h);
// 2. 实现 OpenGL 特有的逻辑
// glViewport(0, 0, w, h);
}
};意义
A. 消除“默认行为”的危险
如果 SetViewportSize 只是普通的虚函数,开发者在写 VulkanRenderer 时可能忘了重写它。编译器不会报错,程序会运行基类的代码。如果基类代码对 Vulkan 来说是不完整的,就会产生难以排查的 Bug。
- 纯虚函数强制开发者必须写下这个函数名,作为一种“签到”机制。
B. 提供“积木式”的基类行为
派生类在重写时,不需要从零开始。它可以通过 Base::Method() 调用基类的实现。基类就像提供了一套“标准组件”,子类在自己的函数体内决定何时以及如何组装这些组件。
C. 纯虚析构函数 (The Special Case)
这是纯虚函数实现最常见的用途。如果你想让一个类变成抽象基类,但它又没有任何合适的成员函数可以作为纯虚函数,你可以把析构函数设为纯虚:
class AbstractBase {
public:
virtual ~AbstractBase() = 0; // 纯虚析构
};
// 必须提供实现,因为派生类析构时会向上调用基类析构
AbstractBase::~AbstractBase() {} 接口 (Interface) vs. 抽象基类 (ABC)
- 纯接口 (Interface):不包含任何实现,只定义行为协议。
- 抽象基类 (ABC):包含部分实现(即便这些实现是在纯虚函数里)。
当你为纯虚函数提供实现时,这个类在语义上就从“纯协议”变成了“带有部分预设逻辑的骨架”。
总结
为纯虚函数提供实现,是为了实现一种**“有条件的强制重写”**:
- 编译器确保你不会忘记重写(强制多态)。
- 基类确保你不必重复编写通用逻辑(代码复用)。