28 / 80 · C++14 · 约 9 分钟
基类析构:public virtual 或 protected nonvirtual
如果允许通过基类指针拥有并删除派生对象,基类应提供 public virtual 析构;如果基类只是不可独立销毁的接口视图,可用 protected nonvirtual 析构阻止这种删除。选择依据是销毁契约,不是见到继承就一律加 virtual。
先问谁拥有并删除对象
普通单对象 delete 经基类指针销毁动态类型不同的派生对象时,基类需要虚析构才能让整个派生对象正确销毁。这里讨论常规删除,不采用 C++20 专门的 destroying delete 机制。其他成员是否虚,与析构是否虚是独立问题;有虚函数不自动使析构成为虚函数。
公开虚析构表示调用者可通过该接口完成拥有对象的销毁。示例的 unique_ptr<Owned> 正是这种契约:它保存基类指针,离开作用域时应先执行 Impl 析构,再执行 Owned 析构。默认智能指针删除器不会替非虚基类自动补出动态删除能力。
不允许基类删除就明确禁止
若基类只供借用和行为访问,而所有对象始终按具体派生类型管理,可把基类析构设为 protected 且非虚。外部 delete Base* 因访问权限失败,派生类析构仍能调用基类析构,具体对象依然可正常放在栈上或由 unique_ptr<Derived> 管理。
这比公开非虚析构更明确:危险用法在编译阶段被拒绝,而不是由调用者记住隐藏约定。protected virtual 也可能出现在特殊框架中,但不是此处两个常见契约的默认选择;关键是同时设计访问权限与动态销毁方式。
销毁链不依赖派生层永久存在
一旦基类析构是虚函数,派生析构也自动具有虚性,可加 override 检查意图。实际顺序是派生析构函数体、派生成员、基类析构;基类析构中再调用虚函数时,不会返回到已经结束的派生层实现。
示例用固定数组记录销毁顺序,再给出 protected 非虚析构的借用接口。两种选择都没有假定对象大小。若接口跨动态库边界,还需确保分配与释放兼容、实现模块活得足够久;virtual 只解决正确的动态析构入口,不解决所有部署层面的生命周期问题。
容易答错的地方
- unique_ptr<Derived> 转成 unique_ptr<Base> 后,默认删除器通常按 Base* 删除;Base 的非虚公开析构因此仍然危险。
- 不要经 Base* 对派生数组执行 delete[];虚析构不能让基类指针正确代表派生数组布局。
运行一个例子
最低标准 C++14 · 完整程序 · 下载 .cpp
#include <array>
#include <cassert>
#include <cstddef>
#include <memory>
#include <type_traits>
struct Log {
std::array<int, 2> entries{};
std::size_t used = 0;
void add(int n) noexcept { entries[used++] = n; }
};
struct Owned {
Log& log;
explicit Owned(Log& l) : log(l) {}
virtual ~Owned() { log.add(2); }
};
struct Impl final : Owned {
explicit Impl(Log& l) : Owned(l) {}
~Impl() override { log.add(1); }
};
class Borrowed {
public:
virtual int value() const = 0;
protected:
~Borrowed() = default;
};
struct Concrete final : Borrowed {
int value() const override { return 7; }
};
int main() {
Log log;
{ std::unique_ptr<Owned> object = std::make_unique<Impl>(log); }
const std::array<int, 2> expected{{1, 2}};
assert(log.entries == expected && log.used == 2);
static_assert(!std::is_destructible<Borrowed>::value, "external deletion forbidden");
Concrete concrete;
const Borrowed& view = concrete;
assert(view.value() == 7);
}
在本地编译
g++ -std=c++14 -Wall -Wextra -Wpedantic -pthread objects-virtual-destructor.cpp -o example && ./example预期结果
预期:正常退出、无输出;所有 assert 通过。
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
把 Borrowed 析构改成 public 但保留非虚,示例中的局部 Concrete 会立刻出错吗?为什么仍不推荐?
查看参考答案
局部 Concrete 仍按具体类型销毁,不会因此出错。但新接口允许外部写出 delete Borrowed*,并在指针实际指向 Concrete 时进入不安全的普通删除路径。protected 非虚方案的价值是把不支持的销毁方式从可编译接口中排除,而不是修复当前这一处局部对象。
继续查证
标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。