基类析构函数必须是virtual,否则通过基类指针删除派生类对象时,派生类析构函数不会被调用,导致资源泄漏;仅当类设计为可被多态销毁时才需virtual析构。

为什么基类析构函数必须是 virtual
不加 virtual 的基类析构函数,会导致派生类对象通过基类指针删除时,派生类部分的析构函数根本不会被调用——这不是“可能出问题”,而是确定会漏掉资源清理。常见现象是:对象内部申请的堆内存没释放、文件句柄没关闭、锁没解锁,但程序不崩溃、没报错,只在 Valgrind 或 ASan 下暴露泄漏。
典型场景:多态容器(如 std::vector<:unique_ptr>></:unique_ptr>)或工厂函数返回 Base*,后续统一 delete ptr。
- 只有当类设计为“被继承”且“可能通过基类指针/引用销毁”时,才需要
virtual析构函数 - 如果类明确禁止继承(如加了
final),或只作接口不管理资源,virtual反而增加虚表开销(虽小但存在) - 纯虚析构函数也必须提供定义:
virtual ~Base() = 0; Base::~Base() = default;,否则链接失败
什么时候可以不用 virtual 析构函数
不是所有基类都需要 virtual 析构。核心判断依据是“是否预期被多态销毁”。比如 std::string_view、std::span 这类轻量包装器,本身不持有资源,也不鼓励继承,就没必要加 virtual。
常见误用:给工具类、策略类(如 Logger、Comparator)盲目加 virtual ~Logger(),结果既没派生也没多态销毁,白增虚表和间接调用开销。
立即学习“C++免费学习笔记(深入)”;
- 类没有虚函数,且不作为多态基类使用 → 不需要
virtual析构 - 类有虚函数但资源全由 RAII 对象(
std::vector、std::shared_ptr)管理 → 析构逻辑已封装,virtual非必需,但加了也无害 - 类仅用于模板参数(如
template<typename t> struct Holder { T t; };</typename>)→ 绝对不需要virtual析构
RAII + unique_ptr 比手动 delete 更可靠
即使写了 virtual 析构,手动 new/delete 仍是泄漏高发区:异常路径跳过 delete、提前 return、忘记配对。现代 C++ 应优先让智能指针接管所有权。
对比示例:
// 危险:异常可能导致泄漏 Base* p = new Derived(); do_something(); // 可能抛异常 delete p; // 这行可能永远不执行 <p>// 安全:异常安全,自动调用 virtual ~Base() std::unique_ptr<Base> p = std::make_unique<Derived>(); do_something(); // 异常时 p 自动析构
-
std::unique_ptr要求所指类型析构函数可访问(public 或 friend),若基类析构是protected,需显式指定删除器 -
std::shared_ptr对析构函数要求更宽松,支持不完整类型,但注意循环引用风险 - 工厂函数应返回
std::unique_ptr<base>而非Base*,把所有权语义直接暴露给调用方
容易被忽略的隐式资源泄漏点
很多人盯着 new/delete,却漏掉更隐蔽的泄漏源:成员变量未正确初始化、移动后状态未置空、静态局部变量生命周期混乱。
例如:
class BadHolder {
int* data_;
public:
BadHolder(size_t n) : data_(new int[n]) {}
// 缺少拷贝构造、赋值、移动构造 —— 浅拷贝导致 double-delete 或悬垂指针
~BadHolder() { delete[] data_; }
};
- 只要类含裸指针或 FILE* 等系统资源,就必须显式定义全部五条特殊成员函数(或 =delete/=default 明确表态)
- 移动操作后,原对象的资源指针必须置为
nullptr,否则其析构函数会二次释放 - 静态局部变量的析构顺序不确定,若它依赖另一个静态对象(如全局 logger),可能在后者析构后还尝试写日志 → 崩溃或静默丢数据
虚析构只是多态销毁的起点,真正防泄漏得靠 RAII 贯彻到每个成员、每条路径、每次移动。











