Effective C++: 为多态基类声明虚析构函数
Declare destructors virtual in polymorphic base classes
views
| comments
这是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