Skip to content

虚析构 / 构造函数辨析 + 特殊成员函数

一、为什么析构函数要声明为虚函数?(面试必背)

一个对象通过基类指针/引用被 delete 时

  • 若析构非虚 → 静态绑定,只调用基类析构,派生类资源不会释放 → 未定义行为 / 内存泄漏(派生类成员、子对象不析构)。
  • 若析构 → 动态绑定到实际类,顺着继承链从最派生类逐层向上调用各层析构,完整释放。
cpp
class Base { public: ~Base(); };              // 非虚
class Derived : public Base { int* p = new int; };

Base* b = new Derived();
delete b;    // ❌ 只调 Base::~Base,Derived::p 泄漏;标准明确此为 UB

只要类会被当作基类多态使用,析构就必须是 virtual。

反面问题

  1. 不需要多态或不做基类的类,析构写成虚会增加 vptr/vtable 开销 + 阻止对象按值布局,故「默认没必要都虚,仅当该类作为多态基类时才虚」。
  2. 但实际工程惯例:凡是写了虚函数的类,几乎都应把析构写成虚(因为意味着会被多态持有)。

记忆顺口溜

只要“指针指基、实际是派生”,delete 时要走“最派生→向上”完整链,基类析构必须 virtual,否则只炸最上层。

二、为什么构造函数不能是虚函数?

  • vptr 此时还不存在:对象构造时 vptr 尚在建立,动态绑定需要 vtable,而构造阶段无法查最终类型的表(最终对象还没成型)。
  • 语义不存在:构造无法「重写」,不能对「还没构造出来的对象」发生多态。
  • 我们讨论的对象类型在构造时由 new 表达式就已确定(new Derived 就是在构造 Derived),并不需要虚构造函数来决定建哪个类。

三、为什么构造/析构函数内部调用虚函数不会多态?

  • 构造时 vptr 指向「当前正在构造的那一层」的表,尚未指向最派生类 → 调用的是本类的虚函数实现(静态绑定)。
  • 析构时 vptr 同样逐层回退,先执行最派生析构再到基类——每层调用该层可见版本。
  • 因此在基类构造函数里调虚函数,不会调到派生类的覆写。这是 C++ 与 Java/C# 的一大差异点(它们会调最终 override,容易踩构造未完成调虚的坑)。

四、防踩坑:从构造函数中调虚函数/多态初始化

经典做法是「非虚的 init + 派生类重写策略函数(template method pattern)」,或把构造逻辑上移到可安全多态的阶段;直接在建里做多态是错的。

五、6 个特殊成员函数(C++11 后)

#函数默认行为何时应手动
1默认构造逐成员默认构造需要特定初值/资源时
2析构逐成员析构持有原始资源时
3拷贝构造逐成员拷贝(shallow)深拷贝语义时(Rule of Three/Five)
4拷贝赋值逐成员拷贝同上
5移动构造(C++11)逐成员移动(无则退化拷贝)优化 / 语义
6移动赋值(C++11)同上优化

Rule of Three / Five:需要自定义析构、拷贝构造、拷贝赋值之任一 → 通常三者都要(旧规则为 Three;C++11 起 + 两个移动为 Five)。反之需求三则必写一,否则默认浅拷贝造成 double free。

=default 与 =delete=default 显式请求默认实现(如想让移动保留);=delete 删除(禁拷贝/禁默认构造)。

六、const/引用成员的 6 特殊成员坑

含 const 或引用 数据成员的类:拷贝赋值/移动赋值为删除(无法给 const/ref 重新赋值),只能用拷贝/移动构造。只读对象常用引用成员则只能构不能赋。

七、三规则示例(为什么都写)

cpp
class Buffer {
    char* p_;
public:
    explicit Buffer(int n) : p_(new char[n]) {}
    ~Buffer() { delete[] p_; }
    Buffer(const Buffer& o);      // 需深拷贝
    Buffer& operator=(const Buffer& o); // 深拷贝
    Buffer(Buffer&& o) noexcept;  // 移动
    Buffer& operator=(Buffer&& o) noexcept;
};

只写析构不写拷贝 → 拷贝会造成两个对象共享 p_,双析构 → double free。

C++ 面试八股 · VitePress 版