章节目录

析构函数简介

本节阅读量:

清理问题

假设您正在编写一个需要通过网络发送数据的程序。然而,建立与服务器的连接成本很高,因此您希望先收集一组数据,然后一次性发送。这样的类可以设计成如下结构:

 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. 析构函数不能有返回类型

类只能有一个析构函数。

通常,您不应该显式调用析构函数(因为当对象被销毁时它将自动调用),因为很少有情况下您希望多次清理对象。

析构函数可以安全地调用其他成员函数,因为对象要等到析构函数执行完毕后才会被销毁。


析构函数示例

 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() 函数,它可以用于立即终止程序。当程序立即终止时,程序就结束了。局部变量不会被销毁,因此不会调用析构函数。在这种情况下,如果您依赖析构函数执行必要的清理工作,就需要格外谨慎。


15.2 嵌套类型(成员类型)

上一节

15.4 具有成员函数的类模板

下一节