本章重点探讨如何利用析构函数管理资源(特别是内存资源)。鉴于第1章已初步讨论过析构函数,此处先简要回顾这一强大机制的核心要点:
当对象生命周期结束时,会调用名为析构函数的特殊成员函数。对于类X,该函数命名为X::~X()。这个函数为类型X提供了执行“最终操作”的机会,析构函数的用途之一就是释放对象持有的资源;
在类继承体系中,对象生命周期结束时:(a)首先调用该对象自身的析构函数;(b)接着按声明顺序调用其非静态数据成员的析构函数;(c)最后按声明顺序调用其基类子对象(俗称“父类”)的析构函数;
通过delete操作符显式销毁对象时,该过程包含:(1)调用指针所指对象的析构函数;(2)释放对象所占内存块。
当类X包含至少一个虚函数时,则X*可能实际指向X的直接或间接派生类Y的对象。为确保调用的是Y的析构函数而非X的析构函数,惯例将X::~X()声明为virtual。若忽略这一点,可能导致调用错误的析构函数,进而引发资源泄漏。
简单示例如下:
#include <iostream>
struct Base {
~Base() { std::cout << "~Base()\n"; }
};
struct DerivedA : Base {
~DerivedA() { std::cout << "~DerivedA()\n"; }
};
struct VirtBase {
virtual ~VirtBase() {
std::cout << "~VirtBase()\n";
}
};
struct DerivedB : VirtBase {
~DerivedB() {
std::cout << "~DerivedB()\n";
}
};
int main() {
{
Base base;
}
{
DerivedA derivedA;
}
std::cout << "----\n";
Base *pBase = new DerivedA;
delete pBase; // bad
VirtBase *pVirtBase = new DerivedB;
delete pVirtBase; // Ok
}
若运行该代码,将看到 base 调用一次析构函数,而 derivedA 调用两次:先是派生类的析构函数,再是基类的析构函数。这部分代码行为符合预期且正确。
问题出在 pBase 这个 Base* 类型的指针上 —— 指向的是 Base 的派生类对象,但由于 Base 的析构函数非虚,通过基类指针删除派生对象显然违背设计初衷:delete pBase 仅调用 Base::~Base(),而不会调用 DerivedA::~DerivedA()。使用 pVirtBase 时则无此问题,因为 VirtBase::~VirtBase() 声明为虚函数。
当然,C++ 总是为特殊用例留有选择余地 —— 在某些特定场景下(基于充分理由),确实需要通过基类指针(其析构函数非虚)删除派生类对象。
需要注意的是:虚成员函数虽有用,但存在开销。实现会为包含虚函数的类型维护虚函数表,并在每个对象中存储指向该表的指针,这会略微增加对象体积。仅当需要通过基类指针调用派生类析构函数时,才应将析构函数声明为虚函数。
了解这些背景后,再来探讨其与资源管理的关联。