析构函数简介
本节阅读量:
清理问题
假设您正在编写一个需要通过网络发送数据的程序。然而,建立与服务器的连接成本很高,因此您希望先收集一组数据,然后一次性发送。这样的类可以设计成如下结构:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
class NetworkData
{
private:
std::string m_serverName{};
DataStore m_data{};
public:
NetworkData(std::string_view serverName)
: m_serverName { serverName }
{
}
void addData(std::string_view data)
{
m_data.add(data);
}
void sendData()
{
// 连接服务器
// 发送数据
// 清理数据
}
};
int main()
{
NetworkData n("someipAddress");
n.addData("somedata1");
n.addData("somedata2");
n.sendData();
return 0;
}
|
然而,这个NetworkData存在潜在问题。它依赖用户在程序关闭前显式调用 sendData()。如果NetworkData的用户忘记这样做,数据就不会发送到服务器,并会在程序退出时丢失。现在,你可能会说:“嗯,记住这样做并不难!”在这种情况下,你是对的。但请考虑一个稍微复杂的示例,比如下面这个函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
bool someFunction()
{
NetworkData n("someipAddress");
n.addData("somedata1");
n.addData("somedata2");
if (someCondition)
return false;
n.sendData();
return true;
}
|
在这种情况下,如果someCondition为true,函数会提前返回,并且不会调用 sendData()。这类错误更容易发生,因为 sendData() 调用确实存在,但程序并不是在所有路径上都会执行它。
概括一下这个问题:使用资源的类(通常是内存,但有时是文件、数据库、网络连接等),其对象在销毁之前可能必须显式发送、关闭或释放资源。还有一些情况下,可能希望在销毁对象之前保留某些记录,例如将信息写入日志文件。术语“清理”通常指类在对象销毁之前必须执行的任何任务,以保证符合预期行为。如果必须依赖类的用户在对象销毁之前调用执行清理的函数,那么代码就非常容易出错。
但为什么要要求用户确保这一点?如果对象即将被销毁,我们就知道此时需要执行清理。清理能否自动进行呢?
析构函数
前面我们介绍过构造函数,它们是在创建非聚合类类型对象时调用的特殊成员函数。构造函数用于初始化成员变量,并执行所需的其他设置任务,以确保类对象可用。
类似地,类还有另一种特殊成员函数,它会在非聚合类类型对象被销毁时自动调用。这个函数称为析构函数。析构函数的设计目的,是让类能在对象销毁之前执行任何必要的清理。
析构函数命名
与构造函数一样,析构函数具有特定的命名规则:
- 析构函数的名字需要与类名一致,同时需要带一个前缀波浪号(~)
- 析构函数不能有参数
- 析构函数不能有返回类型
类只能有一个析构函数。
通常,您不应该显式调用析构函数(因为当对象被销毁时它将自动调用),因为很少有情况下您希望多次清理对象。
析构函数可以安全地调用其他成员函数,因为对象要等到析构函数执行完毕后才会被销毁。
析构函数示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
#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 对象
Simple simple1{ 1 };
{
Simple simple2{ 2 };
} // simple2 在这里销毁
return 0;
} // simple1 在这里销毁
|
该程序产生以下结果:
1
2
3
4
|
Constructing Simple 1
Constructing Simple 2
Destructing Simple 2
Destructing Simple 1
|
请注意,每个Simple对象被销毁时,都会调用析构函数,并打印一条消息。“Destructing Simple 1”打印在“Destructing Simple 2”之后,因为simple2在代码块结束时被销毁,而simple1直到main()结束时才被销毁。
记住,静态变量(包括全局变量和静态局部变量)在程序启动时构造,在程序关闭时销毁。
改进NetworkData程序
回到本课开头的示例,通过让析构函数调用 sendData() 函数,可以消除用户显式调用 sendData() 的需求:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
class NetworkData
{
private:
std::string m_serverName{};
DataStore m_data{};
public:
NetworkData(std::string_view serverName)
: m_serverName { serverName }
{
}
~NetworkData()
{
sendData(); // 确保对象被销毁时,所有的数据自动发送
}
void addData(std::string_view data)
{
m_data.add(data);
}
void sendData()
{
// 连接服务器
// 发送数据
// 清理数据
}
};
int main()
{
NetworkData n("someipAddress");
n.addData("somedata1");
n.addData("somedata2");
return 0;
}
|
有了这样的析构函数,NetworkData对象总会在销毁前发送它拥有的所有数据!清理会自动进行,这意味着出错的可能性更小,需要操心的事情也更少。
隐式析构函数
如果非聚合类类型没有用户声明的析构函数,则编译器会生成一个逻辑为空的析构函数。这个析构函数称为隐式析构函数,它实际上只是一个占位符。
如果类不需要在销毁时进行任何清理,那么完全可以不定义析构函数,让编译器为类生成隐式析构函数即可。
关于std::exit() 函数的警告
前面我们讨论过std::exit() 函数,它可以用于立即终止程序。当程序立即终止时,程序就结束了。局部变量不会被销毁,因此不会调用析构函数。在这种情况下,如果您依赖析构函数执行必要的清理工作,就需要格外谨慎。
对于高级读者
未处理的异常也将导致程序终止,并且在执行此操作之前可能不会展开堆栈。如果堆栈展开没有发生,则在程序终止之前不会调用析构函数。