清理问题
假设你正在编写一个需要通过网络发送数据的程序。由于与服务器建立连接代价高昂,你决定先收集一批数据,再一次性发送。该功能可能被封装为如下类:
// 本示例故意不完整,因此无法通过编译
class NetworkData
{
private:
std::string m_serverName{};
DataStore m_dataQueue{};
public:
NetworkData(std::string_view serverName)
: m_serverName{ serverName }
{
}
void addData(std::string_view data)
{
m_dataQueue.add(data);
}
void sendData()
{
// 连接服务器
// 发送全部数据
// 清空数据
}
};
int main()
{
NetworkData n("someipAddress");
n.addData("somedata1");
n.addData("somedata2");
n.sendData();
return 0;
}
然而,NetworkData
存在一个潜在隐患:它依赖用户显式调用 sendData()
。若使用者忘记调用,程序退出时数据将丢失。你可能会说:“记住调用并不难!”——在这个简单示例中确实如此。但考虑下面更复杂的函数:
bool someFunction()
{
NetworkData n("someipAddress");
n.addData("somedata1");
n.addData("somedata2");
if (someCondition)
return false;
n.sendData();
return true;
}
若 someCondition
为真,函数提前返回,sendData()
便不会被执行。这种错误更易出现,因为 sendData()
明明存在,但并非所有路径都会走到它。
推广来看,凡占有资源(内存、文件、数据库连接、网络连接等)的类,往往需要在对象销毁前完成“发送”“关闭”或“记录”等操作,这类任务统称为清理(clean up)。若把确保清理的责任交给使用者,极易出错。
既然对象销毁时必然需要清理,为何不让清理自动发生?
析构函数登场
第 14.9 课《构造函数简介》指出:构造函数是在非聚合类对象创建时自动调用的特殊成员函数,用于初始化成员并完成必要的准备工作。
类还有另一类特殊成员函数——析构函数(destructor),它在非聚合类对象销毁时自动调用,用于在对象生命期结束前完成所有必要的清理工作。
析构函数命名规则
与构造函数类似,析构函数遵循严格的命名约定:
- 名称必须为
~
后接类名; - 不得接受任何参数;
- 无返回类型;
- 每个类只能有一个析构函数。
通常不应显式调用析构函数(因为对象销毁时会自动调用),几乎不存在需要重复清理的场景。
析构函数内部可安全调用其他成员函数;对象在析构函数执行完毕后才正式销毁。
析构函数示例
#include <iostream>
class Simple
{
private:
int m_id {};
public:
Simple(int id)
: m_id { id }
{
std::cout << "Constructing Simple " << m_id << '\n';
}
~Simple() // 析构函数
{
std::cout << "Destructing Simple " << m_id << '\n';
}
int getID() const { return m_id; }
};
int main()
{
Simple simple1{ 1 };
{
Simple simple2{ 2 };
} // simple2 在此处销毁
return 0;
} // simple1 在此处销毁
运行结果:
Constructing Simple 1
Constructing Simple 2
Destructing Simple 2
Destructing Simple 1
对象销毁时自动调用析构函数并打印信息。simple2
先于 simple1
销毁,对应输出顺序。
注意:静态变量(含全局变量及静态局部变量)在程序启动时构造,在程序结束时销毁。
改进 NetworkData 示例
回到本节开头示例,可通过析构函数自动调用 sendData()
,从而消除用户显式调用的需求:
class NetworkData
{
private:
std::string m_serverName{};
DataStore m_dataQueue{};
public:
NetworkData(std::string_view serverName)
: m_serverName{ serverName }
{
}
~NetworkData()
{
sendData(); // 确保对象销毁前发送全部数据
}
void addData(std::string_view data)
{
m_dataQueue.add(data);
}
void sendData()
{
// 连接服务器
// 发送全部数据
// 清空数据
}
};
int main()
{
NetworkData n("someipAddress");
n.addData("somedata1");
n.addData("somedata2");
return 0;
}
借助析构函数,无论函数如何返回,NetworkData
对象销毁前总会自动发送数据,降低错误概率,减轻使用者负担。
隐式析构函数
若非聚合类未声明析构函数,编译器将生成一个空体的隐式析构函数,仅作占位符。
若类确实无需任何清理工作,可不自定义析构函数,直接依赖隐式版本即可。
关于 std::exit()
的警告
第 8.12 课《提前终止程序》提到,std::exit()
会立即终止程序。此时不会销毁局部变量,因而不会调用析构函数。若依赖析构函数完成关键清理,务必小心。
进阶提示
未捕获的异常也可能导致程序终止,且可能不会展开栈。若栈未展开,析构函数同样不会被调用。