基类析构函数必须声明为 virtual,否则通过基类指针删除派生类对象时,派生类析构逻辑不会执行,导致资源泄漏等未定义行为;即使无数据成员或纯接口类,也应加 virtual ~base() = default;。

基类析构函数不加 virtual 会直接导致内存泄漏
当通过基类指针(或引用)删除派生类对象时,如果基类析构函数不是 virtual,c++ 只会调用基类的析构函数,派生类自己的析构逻辑(比如释放 new 出来的资源、关闭文件句柄、解注册回调等)完全不会执行。这不是“可能出错”,而是确定发生未定义行为——常见表现是程序没崩溃但资源持续堆积,Valgrind 报告 definitely lost,或者 ASan 捕获到未释放内存。
典型场景:工厂函数返回 std::unique_ptr<base>,实际指向 Derived 对象;或容器里存的是 Base*,统一用 delete 释放。
- 只要基类设计为被继承(哪怕只是“理论上可能”),且存在多态删除需求,就必须声明
virtual ~Base() = default; - 即使基类没有数据成员、没写任何析构逻辑,也得加
virtual—— 否则派生类的隐式析构不会被触发 - 如果基类本就不该被实例化(纯接口类),建议同时加上
= default或= delete明确意图,比如:virtual ~Interface() = default;
virtual 析构函数和普通虚函数性能开销几乎可以忽略
有人担心加 virtual 会让每个对象多一个 vptr、影响 cache 局部性。现实是:只要类里已经有别的虚函数(比如 virtual void draw() = 0;),加 virtual 析构函数不新增任何开销;就算类原本没虚函数,单个额外指针(通常 8 字节)在绝大多数场景下不影响性能,更不会拖慢析构本身——析构的耗时主要来自你写的清理逻辑,不是虚调用跳转。
- 别为了“省 8 字节”放弃正确性;现代 CPU 对 vtable 查找优化得很好,分支预测也足够准
- 唯一需警惕的是:频繁构造/析构极小对象(如每帧上千次的
Vec2子类),且该类恰好是第一个带虚函数的类——此时可考虑避免继承,改用组合或std::variant - 编译器无法内联
virtual析构函数,但这不是问题:析构本就该做清理,不该靠内联优化逻辑
派生类析构函数不用显式写 virtual,但必须确保可访问
C++ 规定:一旦基类析构是 virtual,所有派生类析构自动成为虚函数,无论是否加 virtual 关键字。所以你写 ~Derived() 就够了,没必要重复声明 virtual——加了也不报错,但属于冗余。
立即学习“C++免费学习笔记(深入)”;
- 重点在于访问权限:如果派生类析构是
private或protected,而代码试图通过基类指针delete它,就会编译失败(错误信息类似‘Derived::~Derived()’ is private within this context) - 除非有特殊设计(如禁止外部直接销毁,只允许通过某个 manager 释放),否则派生类析构应保持
public - 若基类析构是
protected(常见于不可直接实例化的抽象基类),那派生类析构也得是protected或public,否则delete仍会失败
用 std::unique_ptr 时虚析构仍是刚需
有人觉得 “我用智能指针了,应该安全了吧”,其实不然。std::unique_ptr<base> 默认使用 delete,它依然依赖基类析构是否为 virtual 才能正确调用派生类析构。只有显式提供自定义 deleter(比如 Lambda)才能绕过这个限制,但代价是失去类型擦除和通用性。
- 错误写法:
std::unique_ptr<base> p = std::make_unique<derived>();</derived>—— 若Base::~Base()不是virtual,Derived的析构仍不会执行 - 正确写法:确保
Base有virtual ~Base() = default;,其余照常 - 例外情况:如果派生类完全无资源要清理(即析构函数为空且无副作用),且你 100% 确保未来也不会加,那技术上可不加
virtual—— 但这种假设极脆弱,不推荐
真正容易被忽略的是:虚析构不是“写一次就完事”的规范,它绑定在继承关系的设计源头。一旦基类暴露给外部继承,你就再也无法安全地删掉那个 virtual。