真实面经题目 · 原创解析

C++ 基类析构函数为什么通常要声明为虚函数?

C++ 基类析构函数通常要声明为虚函数,是为了保证通过基类指针或引用删除派生类对象时,能先调用派生类析构函数,再调用基类析构函数,完整释放派生类资源。否则行为可能未定义,容易造成资源泄漏或清理不完整。

出现于:字节跳动 · 客户端

60 秒回答模板

如果一个类会作为多态基类使用,也就是有虚函数并可能通过 Base* 指向 Derived,那么基类析构函数通常必须是 virtual。原因是 delete Base* 时,编译器需要动态绑定到真实对象类型的析构链。基类析构函数是虚函数时,会先执行 Derived::~Derived,再执行 Base::~Base,派生类持有的内存、句柄、锁、文件、网络连接等资源才能正确释放。如果基类析构函数不是虚函数,却通过基类指针删除派生对象,标准层面可能是未定义行为。例外是某些基类明确禁止通过基类指针删除,可以把析构函数设为 protected non-virtual,或者类不是多态基类时不必强行加 virtual。

考点 多态删除
难度 真实面经高频题
回答目标 讲清机制、边界和追问

深入解析

01

问题发生的场景

这个问题只在基类指针或引用承载派生类对象,并且对象生命周期通过基类接口结束时变得关键。例如工厂返回 Base*,容器保存 unique_ptr<Base>,框架回调只知道基类类型。此时外部代码执行 delete base 或智能指针释放时,静态类型是 Base,但真实对象可能是 Derived。析构函数是否虚决定了销毁动作能否动态分派到真实类型。

02

非虚析构的风险

如果基类析构函数不是 virtual,通过 Base* 删除 Derived 对象时,编译器通常只按 Base 的析构路径处理,派生类析构函数可能不会执行。派生类中管理的堆内存、文件描述符、互斥锁、GPU 资源、回调注册和业务状态都可能无法释放。更严格地说,这类删除在 C++ 标准中属于未定义行为,不能只描述成简单的内存泄漏。

03

虚析构如何解决

把基类析构函数声明为 virtual 后,析构调用会通过虚表进行动态绑定。delete Base* 时会找到真实对象类型的析构函数,先执行最派生类析构,再按继承层级向上执行各个基类析构。这符合 C++ 对象构造和析构的逆序规则,也让 RAII 能完整生效。只要类被设计为多态使用,虚析构就是生命周期契约的一部分。

04

智能指针也绕不开

使用 unique_ptr<Base> 或 shared_ptr<Base> 并不自动修复非虚析构问题。智能指针最终仍要调用删除器销毁对象,如果删除器按 Base* delete,而 Base 析构不是虚函数,风险仍然存在。shared_ptr 在某些构造方式下能保存创建时的删除器类型,但依赖这种细节会降低可读性和一致性。对多态基类来说,virtual destructor 是更清晰的设计。

05

什么时候不需要虚析构

不是所有基类析构都必须 virtual。如果类不是多态基类,不打算通过基类指针删除派生对象,就不必为了习惯增加虚表成本。另一种常见设计是把基类析构函数设为 protected 且非虚,禁止外部 delete Base*,要求对象通过派生类型或专门的 destroy 接口释放。关键不是机械加 virtual,而是让析构策略和类的使用方式一致。

易错点

  • 只说不加 virtual 会内存泄漏,没有指出通过基类指针删除派生对象属于未定义行为风险。
  • 认为用了智能指针就不需要虚析构,忽略智能指针最终仍要调用删除逻辑。
  • 给所有类无脑加虚析构,没有区分类是否被设计为多态基类。
  • 忘记纯虚析构函数也需要提供定义,导致链接错误或概念解释不完整。

面试官追问

有虚函数的类析构函数一定要 virtual 吗?

通常应该。只要这个类被当作多态基类,并可能通过基类指针释放对象,析构就要 virtual。若明确禁止这种释放,可以用 protected 析构等方式表达。

虚析构会带来什么成本?

如果类本身没有虚函数,增加虚析构会引入虚表指针和动态分派成本。若类已经是多态类,通常虚表成本已经存在,虚析构的额外成本很小。

纯虚析构函数可以吗?

可以,但纯虚析构也必须提供函数定义。因为派生类析构完成后仍会调用基类析构,链接时需要基类析构函数的实现。

protected 非虚析构有什么用?

它可以阻止外部通过基类指针 delete 对象,表达这个基类不负责多态销毁。对象必须通过派生类型或受控接口释放。