C++ / a working model

28 / 80   ·   C++14   ·   约 9 分钟

基类析构:public virtual 或 protected nonvirtual

先记住这句话

如果允许通过基类指针拥有并删除派生对象,基类应提供 public virtual 析构;如果基类只是不可独立销毁的接口视图,可用 protected nonvirtual 析构阻止这种删除。选择依据是销毁契约,不是见到继承就一律加 virtual。

本篇内容
  1. 先问谁拥有并删除对象
  2. 不允许基类删除就明确禁止
  3. 销毁链不依赖派生层永久存在
  4. 运行示例
  5. 动手练习

先问谁拥有并删除对象

普通单对象 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 非虚方案的价值是把不支持的销毁方式从可编译接口中排除,而不是修复当前这一处局部对象。

继续查证

标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。

回到目录