liangbm3's blog

Back

这是Effective C++的第 7 条款,Declare destructors virtual in polymorphic base classes,中文翻译为:为多态基类声明虚析构函数。

这个条款是用来解决一个非常具体且危险的问题:当通过基类指针删除派生类对象时,如果基类的析构函数不是虚函数,派生类的析构函数将不会被调用,导致资源泄漏和未定义行为。

以下代码为例:

// 基类
class TimeKeeper {
public:
    TimeKeeper();
    ~TimeKeeper(); // 注意:这是一个非虚 (non-virtual) 析构函数
    // ...
};

// 派生类
class AtomicClock : public TimeKeeper { ... };

// 工厂函数,返回一个指向派生类对象的基类指针
TimeKeeper* getTimeKeeper() {
    return new AtomicClock();
}

// 客户端代码
TimeKeeper* ptk = getTimeKeeper();
// ... 使用 ptk ...
delete ptk; // 危险!问题就在这里!
cpp

当调用 delete ptk; 时,由于 TimeKeeper 的析构函数不是虚函数,只有 TimeKeeper 的析构函数会被调用,而 AtomicClock 的析构函数不会被调用。这会导致 AtomicClock 中分配的资源(如动态内存、文件句柄等)无法正确释放,进而引发资源泄漏和未定义行为。而解决方法就是将基类的析构函数声明为虚函数:

class TimeKeeper {
public:
    TimeKeeper();
    virtual ~TimeKeeper(); 
    // ...
};
cpp

但是作者又指出,无端地将所有类的析构函数声明为虚函数也是错误的做法。

  • 性能与成本:最直接和最主要的原因是性能和内存成本的考虑,当为一个类添加虚函数时,会给该类的每个对象引入一个虚函数指针(vptr),这会导致对象体积的增大,文中举了一个非常形象的例子:一个用于表示二维空间点坐标的 Point 类,原本可能只包含两个 int,在64位系统上占用64位(8字节)。一旦为其添加了 vptr,整个对象的体积可能会增大50%~100%,达到96位(12字节)甚至128位(16字节)。另一方面,引入 vptr 会破坏对象的内存布局,使其不再是纯粹的数据集合,这样的对象将“不再可能把它们传递至(或接收自)其他语言所写的函数”,不再具有移植性。
  • 设计意图:一个类不含任何虚函数,通常是在明确地表示“它并不意图被用作一个基类”,或者至少不是一个“多态基类”。文中用 std::string 举例。std::string 没有任何虚函数,包括析构函数。这清楚地表明了它的设计者不希望用户继承它并进行多态操作。如果你错误地这样做了(例如,class SpecialString : public std::string),并且通过 std::string 指针去删除 SpecialString 对象,就会导致未定义行为。

文中还提到了纯虚析构函数,有时候你想让一个类成为抽象基类(不能被实例化),但这个类里又恰好没有任何其他适合声明为纯虚的函数,这时候就可以将析构函数声明为纯虚析构函数:

class AWOW { // Abstract w/o Virtuals
public:
    virtual ~AWOW() = 0; // 声明一个纯虚析构函数
};
cpp

需要注意的是,纯虚析构函数必须在类外进行定义,即使它是纯虚的,因为在派生类对象被销毁时,基类的析构函数仍然需要被调用:

AWOW::~AWOW() { } // 必须提供,哪怕是空的
cpp
Effective C++: 为多态基类声明虚析构函数
https://liangbm3.site/blog/effective-c-wei-duo-tai-ji-lei-sheng-ming-xu-xi-gou-han-shu
Author liangbm3
Published at 2025年10月18日
Comment seems to stuck. Try to refresh?✨