基类析构函数必须是 virtual,否则通过基类指针删除派生类对象时,派生类析构函数不执行,导致资源泄漏;只要类可能被继承且通过基类指针管理生命周期,就必须声明 virtual ~Base() = default;。

为什么基类析构函数必须是 virtual
不加 virtual 的基类析构函数,会导致派生类对象通过基类指针删除时,派生类的析构逻辑完全不执行——不是内存泄漏,是资源泄漏(比如文件句柄没关、动态内存没释放、锁没解锁)。这是 C++ 多态销毁场景下最隐蔽也最危险的问题之一。
常见错误现象:delete ptr; 看似正常返回,但调试发现派生类的析构函数断点根本没命中,std::cout 语句没输出,fclose 没调用,delete[] 没执行。
- 只要类设计为被继承(哪怕当前没写派生类),且可能通过基类指针/引用管理对象生命周期,就必须声明
virtual ~Base() = default; - 如果基类已有虚函数(比如
virtual void foo();),析构函数也必须是virtual,否则行为未定义 - 纯虚析构函数可以存在,但必须提供定义(哪怕空实现):
virtual ~Base() = 0;后面得跟Base::~Base() {}
什么时候可以不写 virtual 析构函数
当类明确不作为多态基类使用时,比如:工具类(StringUtils)、仅含静态成员的类、或明确禁止继承(C++11 起用 final 修饰类)。
使用场景判断比语法更重要:如果代码里出现过 Base* p = new Derived; 或 std::unique_ptr<base> ptr = std::make_unique<derived>();</derived>,那就必须有 virtual 析构。
立即学习“C++免费学习笔记(深入)”;
-
struct基类同样适用——访问控制不影响虚析构必要性 - 模板基类(如
template<typename t> class ContainerBase</typename>)若预期被继承,析构也应为virtual - 即使派生类析构函数为空,也不能省略基类的
virtual;编译器不会帮你“推导”需要虚析构
virtual ~Base() = default 和 virtual ~Base() {} 的区别
二者在绝大多数情况下等价,但细节影响异常安全和移动语义。
= default 让编译器生成隐式析构函数,它会自动调用所有成员和基类的析构函数,并保持 noexcept(true);而空花括号 {} 声明的析构函数默认是 noexcept(false)(除非显式加 noexcept),可能破坏容器(如 std::vector)的强异常保证。
- 推荐统一用
virtual ~Base() = default;,简洁且符合现代 C++ 风格 - 如果基类有需要手动清理的资源(比如裸指针),就不能用
= default,得写具体逻辑并加noexcept:virtual ~Base() noexcept { delete ptr_; } - 注意:
= default只能用于无自定义析构逻辑的类;一旦写了任何析构代码,就失去隐式noexcept优势
Clang/GCC 提示 “destructor called on non-final class” 是什么信号
这是编译器在警告你:你正通过非 final 类型的指针(或引用)销毁一个对象,而该类的析构函数不是 virtual。它不一定报错,但意味着你可能正在踩多态销毁的坑。
典型触发代码:Base* b = new Derived; delete b; + Base 没有 virtual 析构 → Clang 给出 -Wdelete-non-virtual-dtor 警告。
- 开启
-Wdelete-non-virtual-dtor(GCC/Clang 默认不启用,建议加进构建配置) - 不要靠
final类绕过问题:把class Derived final : public Base并不能解决Base缺少virtual析构的问题 - 静态分析工具(如 clang-tidy)检查
cppcoreguidelines-special-member-functions也能捕获这类疏漏
虚析构不是语法糖,是对象生命周期契约的一部分。很多人只在 crash 后才意识到它缺失,而那时资源状态早已不可追溯。










