c++的虚析构函数(virtual destructor)为什么如此重要? (防止资源泄漏)

虚析构函数必须声明为virtual,否则通过基类指针delete子类对象时子类析构函数不会被调用,导致资源泄漏;只要存在多态删除场景(如工厂返回指针、容器存储基类指针等),就必须加;基类声明virtual析构后,派生类析构自动为虚,建议显式加override。

虚析构函数不加,delete 父类指针时子类析构函数根本不会调用

这是最直接的后果。当用 Base* 指向一个 Derived 对象,且 Base::~Base() 不是 virtual 时,delete ptr 只会调用 Base::~Base()Derived::~Derived() 完全被跳过。如果子类在析构里释放文件句柄、内存、网络连接或调用 close()/delete,这些操作就彻底漏掉了。

常见错误现象:

  • 程序运行时不报错,但反复创建/销毁对象后出现句

    柄耗尽(Too many open files
  • Valgrind 报告“still reachable”内存块,实际是子类分配的堆内存没被 delete
  • 资源泄漏只在子类有非 trivial 析构逻辑时才暴露,基类空析构会让问题潜伏很久

什么时候必须声明为 virtual?看多态删除是否可能发生

不是“只要用了继承就要加”,而是“只要存在通过基类指针/引用创建子类对象,并可能用基类指针 delete 它”的场景,就必须加。典型包括:

  • 工厂函数返回 std::unique_ptr 或裸指针
  • 容器存的是 std::vector<:unique_ptr>>
  • 回调系统中注册了子类对象,由框架统一销毁

反例:子类只用于栈上对象(Derived d;),或完全不通过基类指针销毁,则虚析构不是必需的——但加了也没坏处,且容易误判使用方式,所以只要类设计为被继承,就应默认加 virtual

virtual 析构函数对性能和 ABI 的影响其实极小

有人担心虚函数表开销或调用成本,但现实是:

  • 析构函数本身只调用一次,且通常在对象生命周期末尾,性能敏感度远低于频繁调用的成员函数
  • 编译器能优化掉部分虚调用(如静态类型已知时),但无法保证所有场景
  • ABI 影响仅限于增加一个虚函数表项,不影响对象布局(析构函数不改变 sizeof
  • 现代 C++ 中,若基类已有其他虚函数(比如 virtual void foo() = 0;),加 virtual 析构几乎零成本

真正代价是没加带来的不确定性——你永远不知道下游用户会不会拿你的类做多态删除。

正确写法:基类析构声明为 virtual,子类无需显式加 virtual

只要基类析构是 virtual,派生类的析构自动成为虚函数,即使不写 virtual 关键字。但建议显式写出,提高可读性:

class Base {
public:
    virtual ~Base() = default; // ✅ 推荐:= default 更清晰
};

class Derived : public Base { public: virtual ~Derived() override { / 清理子类资源 / } // ✅ 显式 + override };

注意:= default{} 都可以,但避免写成 ~Base() {} —— 这会抑制编译器生成移动操作,且语义不如 = default 明确。

最容易被忽略的一点:纯虚析构函数必须提供定义,哪怕为空:

class Interface {
public:
    virtual ~Interface() = 0; // 声明纯虚
};
Interface::~Interface() = default; // ✅ 必须定义,否则链接失败

虚析构函数不是“最佳实践”的点缀,而是多态对象生命周期管理的契约底线——漏掉它,等于把资源释放的控制权交给了未定义行为。