C++中为什么析构函数必须是虚函数? (多态下的内存泄漏)

2次阅读

基类析构函数必须声明为virtual,否则通过基类指针delete派生类对象时仅调用基类析构,导致派生类资源泄漏;纯虚析构也需提供定义,智能指针中unique_ptr仍依赖虚析构,shared_ptr是特例但不可依赖。

C++中为什么析构函数必须是虚函数? (多态下的内存泄漏)

基类指针 delete 派生类对象时,不加 virtual 会跳过派生类析构

核心问题不是“析构函数该不该虚”,而是:当用 Base* 指向 Derived 对象并调用 delete 时,若 Base::~Base() 不是虚函数,编译器只调用 Base 的析构函数,完全跳过 Derived::~Derived() —— 这意味着派生类中所有资源(如 new 出的内存、打开的文件句柄、锁等)都不会被释放。

常见错误现象:
– 程序运行无 crash,但 Valgrind 报告“definitely lost”内存块
– 对象里有 std::vectorstd::String 成员?它们的内部缓冲区可能没被回收(虽然标准库实现通常有保护,但自定义资源一定出问题)
线程下出现诡异的 double-free 或 use-after-free,其实是上层逻辑误以为资源已清理

  • 只有通过基类指针/引用 delete 对象时才触发这个问题;直接 Derived d; delete &d;对象自动析构不受影响
  • 只要类设计为“可能被继承 + 可能被多态删除”,就必须把析构函数声明为 virtual
  • 虚析构函数本身不增加运行时开销,但会让类失去 triviality(影响 std::is_trivially_destructible 判断)

纯虚析构函数也能防止内存泄漏,但必须提供定义

如果基类本就不该被实例化(比如抽象接口类),可以写成纯虚析构:virtual ~Base() = 0;。但这只是语法上禁止构造,不代表能省略实现——链接器会报错 undefined reference to 'Base::~Base()',因为即使纯虚,析构函数仍需在派生类析构链中被调用。

  • 必须在类外提供定义:Base::~Base() {}(哪怕空实现)
  • 纯虚析构和普通虚析构在多态 delete 行为上完全一致,区别仅在于是否强制派生
  • 不要为了“看起来更抽象”而滥用纯虚析构;若基类允许实例化,就用普通虚析构

现代 c++ 中,智能指针不会绕过虚析构要求

有人误以为用 std::unique_ptr 就安全了,其实不然。智能指针的默认删除器仍是基于静态类型调用析构函数。除非显式传入自定义删除器,否则它和裸指针 delete 面临完全相同的问题。

示例:

立即学习C++免费学习笔记(深入)”;

std::unique_ptr ptr = std::make_unique(); // 若 Base::~Base() 不是 virtual,这里仍只调用 Base::~Base()
  • std::shared_ptr 是例外:它在构造时捕获了实际类型信息,所以即使基类析构非虚,Derived 析构也会被正确调用(但这是靠控制块实现的特例,不是语言规则)
  • 别依赖 shared_ptr 的这个行为来规避虚析构;它不改变裸指针或 unique_ptr 的语义,也不解决其他 RAII 类型(如自定义 handle 类)的资源泄漏
  • 虚析构是面向多态删除的通用契约,和用什么指针管理无关

什么时候可以不写虚析构?

只有两种情况真正安全:
– 类明确设计为“不可继承”(加 final 关键字)
– 类绝不会被多态删除(即:永远不会用基类指针指向派生对象后调用 delete

注意:
final 是编译期保证,比文档注释可靠得多
– “绝不会”这种判断容易出错,尤其在大型项目或 API 设计中;宁可多加 virtual,也不要赌使用者不会误用
– 模板基类(如 template class Base { ... };)通常不需要虚析构,因为模板实例化后一般不用于多态场景

最容易被忽略的一点:析构函数的访问控制(public/protected)和虚性无关,但若设为 private,连派生类都无法调用,直接编译失败 —— 虚只是解决“调哪个”,不是解决“能不能调”。

text=ZqhQzanResources