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

基类指针 delete 派生类对象时,不加 virtual 会跳过派生类析构
核心问题不是“析构函数该不该虚”,而是:当用 Base* 指向 Derived 对象并调用 delete 时,若 Base::~Base() 不是虚函数,编译器只调用 Base 的析构函数,完全跳过 Derived::~Derived() —— 这意味着派生类中所有资源(如 new 出的内存、打开的文件句柄、锁等)都不会被释放。
常见错误现象:
– 程序运行无 crash,但 Valgrind 报告“definitely lost”内存块
– 对象里有 std::vector 或 std::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)通常不需要虚析构,因为模板实例化后一般不用于多态场景
最容易被忽略的一点:析构函数的访问控制(public/protected)和虚性无关,但若设为 private,连派生类都无法调用,直接编译失败 —— 虚只是解决“调哪个”,不是解决“能不能调”。