19-3 析构函数
析构函数是另一种特殊的类成员函数,当该类的对象被销毁时执行。构造函数用于初始化类,而析构函数则用于清理工作。
当对象正常退出作用域,或动态分配的对象被显式用delete关键字删除时,类析构函数(若存在)会自动调用,在对象从内存移除前执行必要的清理工作。对于简单类(仅初始化普通成员变量值的类),无需析构函数,因为C++会自动清理内存。
但若类对象持有资源(如动态内存、文件句柄或数据库句柄),或需在对象销毁前执行维护操作,析构函数便是理想的处理场所——它通常是对象销毁前的最后执行环节。
析构函数命名规则
与构造函数类似,析构函数也有特定的命名规则:
- 析构函数必须与类同名,并在前面添加波浪号(~)。
- 析构函数不能带参数。
- 析构函数没有返回类型。
一个类只能拥有一个析构函数。
通常不应显式调用析构函数(因对象销毁时会自动调用),因为极少需要对对象进行多次清理。但析构函数可安全调用其他成员函数,因为对象在析构函数执行完毕后才会被销毁。
析构函数示例
让我们看一个使用析构函数的简单类:
#include <iostream>
#include <cassert>
#include <cstddef>
class IntArray
{
private:
int* m_array{};
int m_length{};
public:
IntArray(int length) // constructor
{
assert(length > 0);
m_array = new int[static_cast<std::size_t>(length)]{};
m_length = length;
}
~IntArray() // destructor
{
// Dynamically delete the array we allocated earlier
delete[] m_array;
}
void setValue(int index, int value) { m_array[index] = value; }
int getValue(int index) { return m_array[index]; }
int getLength() { return m_length; }
};
int main()
{
IntArray ar ( 10 ); // allocate 10 integers
for (int count{ 0 }; count < ar.getLength(); ++count)
ar.setValue(count, count+1);
std::cout << "The value of element 5 is: " << ar.getValue(5) << '\n';
return 0;
} // ar is destroyed here, so the ~IntArray() destructor function is called here
提示:
若编译上述示例时出现以下错误:error: ‘class IntArray’ 包含指针数据成员 [-Werror=effc++]| error: 但未重写 ‘IntArray(const IntArray&)’ [-Werror=effc++]| error: 或 ‘operator=(const IntArray&)’ [-Werror=effc++]|则可通过以下方式解决:从该示例的编译设置中移除“-Weffc++”标志,或在类中添加以下两行代码:
IntArray(const IntArray&) = delete;
IntArray& operator=(const IntArray&) = delete;
我们在第14.14节——复制构造函数介绍中讨论了成员的delete操作。
该程序输出结果:

在 main() 的第一行,我们实例化了一个名为 ar 的 IntArray 类对象,并传入长度为 10 的参数。这会调用构造函数,为数组成员动态分配内存。我们必须在此使用动态分配,因为编译时无法确定数组的长度(由调用方决定)。
在 main() 结束时,ar 脱离作用域。这将触发 ~IntArray() 析构函数的调用,从而销毁我们在构造函数中分配的数组!
重要提示:
在第16.2节 std::vector与list构造函数入门 中我们提到:初始化数组/容器/列表类时,若指定长度(而非元素列表),应使用括号初始化形式。因此我们通过 IntArray ar ( 10 ); 初始化IntArray。
构造函数与析构函数的调用时机
如前所述,构造函数在对象创建时被调用,而析构函数在对象销毁时被调用。在下面的示例中,我们通过在构造函数和析构函数内部使用cout语句来展示这一特性:
#include <iostream>
class Simple
{
private:
int m_nID{};
public:
Simple(int nID)
: m_nID{ nID }
{
std::cout << "Constructing Simple " << nID << '\n';
}
~Simple()
{
std::cout << "Destructing Simple" << m_nID << '\n';
}
int getID() { return m_nID; }
};
int main()
{
// Allocate a Simple on the stack
Simple simple{ 1 };
std::cout << simple.getID() << '\n';
// Allocate a Simple dynamically
Simple* pSimple{ new Simple{ 2 } };
std::cout << pSimple->getID() << '\n';
// We allocated pSimple dynamically, so we have to delete it.
delete pSimple;
return 0;
} // simple goes out of scope here
该程序产生以下结果:

请注意,“Simple 2”之后“Simple 1”会被销毁,因为我们在函数结束前删除了pSimple,而simple直到main()结束时才被销毁。
全局变量在main()之前被构造,并在main()之后被销毁。
RAII
RAII(资源获取即初始化Resource Acquisition Is Initialization)是一种编程技术,它将资源使用与具有自动持续时间的对象的生命周期绑定(例如非动态分配的对象)。在C++中,RAII通过具有构造函数和析构函数的类实现。资源(如内存、文件或数据库句柄等)通常在对象构造函数中获取(若合理也可在对象创建后获取)。该资源可在对象存活期间被使用。资源在对象被销毁时的析构函数中释放。RAII的核心优势在于能有效防止资源泄漏(如内存未被释放),因为所有持有资源的对象都会被自动清理。
本课开头的IntArray类即为实现RAII的示例——构造函数中分配资源,析构函数中释放资源。标准库中的std::string和std::vector也遵循RAII原则:动态内存于初始化时获取,并在销毁时自动清理。
关于 std::exit() 函数的警告
请注意,若使用 std::exit() 函数,程序将立即终止且不会调用析构函数。若您的程序依赖析构函数执行必要清理工作(例如退出前向日志文件或数据库写入数据),请务必谨慎使用。
摘要
如您所见,当构造函数与析构函数协同工作时,您的类能够自动完成初始化与清理工作,无需程序员进行任何特殊操作!这不仅降低了出错概率,更显著提升了类的使用便捷性。

浙公网安备 33010602011771号