Item7

条款 7:为多态基类声明 virtual 析构函数

(Item 7: Declare destructors virtual in polymorphic base classes)

1. 核心原则

如果一个类是为了被当作基类(Base Class)使用,并且该类包含虚函数(Virtual Functions),那么它应该拥有一个虚析构函数(Virtual Destructor)。

2. 为什么要这样做?(The Problem)

当你使用基类指针(Base*)指向一个派生类对象(Derived),并对该指针执行 delete 操作时:

  • 如果基类的析构函数不是 virtual 的: C++ 标准规定这种行为是未定义的。通常发生的情况是,只有基类的部分被销毁了,而派生类的部分没有被销毁

  • 后果: 派生类中成员变量占用的内存不会被释放,派生类的析构函数也不会被调用。这会导致严重的内存泄漏 (Memory Leak) 和资源未释放问题。

3. 代码示例

❌ 错误的做法 (灾难现场):

class TimeKeeper {
public:
    TimeKeeper() {}
    // 错误!非虚析构函数
    ~TimeKeeper() { /* 销毁 TimeKeeper 的资源 */ }
    
    virtual void showTime() { /* ... */ } 
};

class AtomicClock : public TimeKeeper {
public:
    ~AtomicClock() { /* 销毁 AtomicClock 特有的复杂资源 */ }
};

// 实际使用
TimeKeeper* ptk = new AtomicClock();

// ... 使用对象 ...

// 灾难发生在这里!
// 因为 ~TimeKeeper 不是 virtual,delete 只会调用 ~TimeKeeper()。
// ~AtomicClock() 永远不会被调用!
delete ptk; 

✅ 正确的做法:

class TimeKeeper {
public:
    TimeKeeper() {}
    // 正确!声明为 virtual
    virtual ~TimeKeeper() { /* 销毁 TimeKeeper 的资源 */ }
    
    virtual void showTime() { /* ... */ } 
};

// 现在 delete ptk 会先调用 ~AtomicClock(),然后再调用 ~TimeKeeper()。
// 整个对象被完美销毁。

4. 什么时候不该用 Virtual 析构函数?

并非所有类的析构函数都应该是 Virtual 的。

  • 性能成本: 当一个类声明了虚函数(包括虚析构函数)时,编译器会为该类创建一个 vptr (virtual table pointer)vtbl (virtual table)。这会增加对象的体积(通常增加一个指针的大小)。

  • 非基类: 如果一个类(例如 Pointstd::string不旨在被继承,或者是为了多态目的而设计,那么将析构函数声明为 virtual 只会无谓地增加内存占用,甚至破坏与 C 语言结构体的兼容性。

切记: 不要继承没有 virtual 析构函数的标准库类(如 std::string, std::vector, std::map 等)。


总结 (Key Takeaways)

  1. 多态基类必须有虚析构函数: 只要类里有至少一个 virtual 函数,你就应该把析构函数也设为 virtual。

  2. 非多态类不要滥用: 如果类不打算作为基类使用,或者不通过基类指针进行操作,不要声明虚析构函数。

  3. 抽象类技巧: 如果你想把一个类变成抽象基类(Abstract Base Class),但又没有合适的纯虚函数,可以将析构函数声明为纯虚析构函数

    class AWOV { // Abstract Without Other Virtuals
    public:
        virtual ~AWOV() = 0; 
    };
    // 记得要在类外提供定义,否则链接会出错
    AWOV::~AWOV() {} 

对总结3的补充

1. 为什么要这么做?(目的)

目标: 你想要创建一个抽象基类(Abstract Base Class),使得用户无法实例化它(只能实例化它的派生类)。

问题: 通常,要让一个类变成抽象类,你需要在类中声明至少一个纯虚函数(例如 virtual void draw() = 0;)。但是,有时你的基类只是一个接口或辅助类,并没有其他合适的函数可以设为纯虚函数。

解决方案: 既然没有别的函数可选,那就拿析构函数开刀。将其设为纯虚函数 virtual ~AWOV() = 0;,这个类立刻就变成了抽象类,编译器会禁止创建 AWOV 的实例。


2. 为什么必须在类外提供定义?(核心难点)

这与 C++ 中析构函数的调用机制有关。

普通纯虚函数 vs. 纯虚析构函数

  • 普通纯虚函数: 如果父类有一个 virtual void method() = 0;,子类通常会覆盖(override)它。当调用子类的 method() 时,只会执行子类的代码。父类的那个纯虚函数根本不需要被调用,所以它不需要定义

  • 析构函数(特殊情况): 析构函数的调用顺序是从子类到父类(From Derived to Base)。 当你 delete 一个派生类对象时:

    1. 首先执行派生类的析构函数(~Derived())。

    2. 然后,编译器会自动插入代码,去调用基类的析构函数(~Base())。


如果你不提供定义会发生什么?

编译器在生成派生类的析构代码时,必须生成一条指令去 call AWOV::~AWOV()。 链接器(Linker)在最后链接程序时,会去寻找 AWOV::~AWOV() 的编译后代码。如果你只写了 = 0 而没有写函数体 {},链接器找不到目标,就会报错(通常是 Undefined reference to vtable...Unresolved external symbol)。


3. 代码实战演示

#include <iostream>

// 1. 声明抽象基类
class AbstractBase {
public:
    // 设为纯虚,强制类成为抽象类
    virtual ~AbstractBase() = 0; 
};

// 2. ★★★ 必须提供定义,否则链接报错 ★★★
// 哪怕它是纯虚的,它也必须有函数体,因为子类析构完后必须回调这里。
AbstractBase::~AbstractBase() {
    std::cout << "Base destructor called." << std::endl;
}

class Derived : public AbstractBase {
public:
    ~Derived() {
        std::cout << "Derived destructor called." << std::endl;
    }
};

int main() {
    // AbstractBase b; // 错误!无法实例化抽象类
    
    AbstractBase* ptr = new Derived();
    delete ptr; // 输出顺序:1. Derived destructor called.  2. Base destructor called.
    
    return 0;
}

4. 总结

这个技巧体现了 C++ 的两个规则的碰撞:

  1. 抽象类规则: 只要有一个纯虚函数,类就是抽象的(不能实例化)。

  2. 析构规则: 派生类析构必须调用基类析构。

一句话总结: 将析构函数设为纯虚是为了阻止基类实例化;而提供函数体是为了满足派生类析构时的回调需求,防止链接错误。

发表评论