15-4 析构函数介绍

清理问题

假设您正在编写一个需要通过网络发送一些数据的程序。然而,建立与服务器的连接是昂贵的,因此您希望收集一堆数据,然后一次发送全部数据。这样的类的结构可能如下:

// This example won't compile because it is (intentionally) incomplete
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()
	{
		// connect to server
		// send all data
		// clear data
	}
};

int main()
{
    NetworkData n("someipAddress");

    n.addData("somedata1");
    n.addData("somedata2");

    n.sendData();

    return 0;
}

然而,这个NetworkData存在潜在问题。它依赖于在程序关闭前显式调用sendData()。如果NetworkData的使用者忘记执行此操作,数据将无法发送至服务器,并在程序退出时丢失。此刻你或许会说:“这不难记住啊!”——在这种特殊情况下,你的说法没错。但请考虑稍复杂的场景,例如这个函数:

bool someFunction()
{
    NetworkData n("someipAddress");

    n.addData("somedata1");
    n.addData("somedata2");

    if (someCondition)
        return false;

    n.sendData();
    return true;
}

在此情况下,若someCondition为真,函数将提前返回,sendData()不会被调用。这种错误更容易发生,因为sendData()调用确实存在,只是程序在某些情况下不会跳转至该处。

将此问题泛化来看,使用资源的类(通常是内存,有时也涉及文件、数据库、网络连接等)在类对象销毁前,往往需要显式地发送或关闭资源。在其他情况下,我们可能需要在对象销毁前进行记录保存操作,例如将信息写入日志文件,或向服务器发送遥测数据。“清理”一词通常指代类对象销毁前必须执行的任务集合,以确保行为符合预期。若依赖用户在对象销毁前调用清理函数,则极易引发错误。

但为何要让用户承担确保责任?既然对象即将被销毁,显然此时需要执行清理工作。这种清理操作是否应当自动完成?


析构函数的救援

第14.9节——构造函数简介中,我们介绍了构造函数。构造函数是特殊成员函数,当创建非聚合类型的对象时会被调用。构造函数用于初始化成员变量,并执行其他必要的设置任务,以确保该类对象准备就绪。

类似地,类还拥有另一类特殊成员函数,当非聚合类型的对象被销毁时会自动调用。该函数称为析构函数destructor。析构函数的设计初衷,是让类在对象被销毁前执行必要的清理工作。


析构函数命名规则

与构造函数类似,析构函数也有特定的命名规则:

  1. 析构函数必须与类同名,并在前面添加波浪号(~)。
  2. 析构函数不能带参数。
  3. 析构函数没有返回类型。

一个类只能拥有一个析构函数。

通常不应显式调用析构函数(因对象销毁时会自动调用),因为极少存在需要对对象进行多次清理的情况。

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


析构函数示例

#include <iostream>

class Simple
{
private:
    int m_id {};

public:
    Simple(int id)
        : m_id { id }
    {
        std::cout << "Constructing Simple " << m_id << '\n';
    }

    ~Simple() // here's our destructor
    {
        std::cout << "Destructing Simple " << m_id << '\n';
    }

    int getID() const { return m_id; }
};

int main()
{
    // Allocate a Simple
    Simple simple1{ 1 };
    {
        Simple simple2{ 2 };
    } // simple2 dies here

    return 0;
} // simple1 dies here

该程序产生以下结果:

image

请注意,当每个 Simple 对象被销毁时,其析构函数会被调用并打印一条消息。由于 simple2 在函数结束前已被销毁,而 simple1 直到 main() 结束时才被销毁,因此“销毁 Simple 1”会出现在“销毁 Simple 2”之后。

请记住,静态变量(包括全局变量和静态局部变量)在程序启动时构造,并在程序关闭时销毁。


改进NetworkData程序

回到本节开头的示例,我们可以通过让析构函数调用sendData()来消除用户显式调用该函数的需求:

class NetworkData
{
private:
    std::string m_serverName{};
    DataStore m_dataQueue{};

public:
	NetworkData(std::string_view serverName)
		: m_serverName { serverName }
	{
	}

	~NetworkData()
	{
		sendData(); // make sure all data is sent before object is destroyed
	}

	void addData(std::string_view data)
	{
		m_dataQueue.add(data);
	}

	void sendData()
	{
		// connect to server
		// send all data
		// clear data
	}
};

int main()
{
    NetworkData n("someipAddress");

    n.addData("somedata1");
    n.addData("somedata2");

    return 0;
}

有了这样的析构函数,我们的NetworkData对象在被销毁前总会发送所有数据!清理工作会自动完成,这意味着更少的出错机会,也减少了需要考虑的事项。


隐式析构函数

若非聚合类型的对象未声明用户定义的析构函数,编译器将自动生成一个空函数体的析构函数。此析构函数称为隐式析构函数,本质上仅作为占位符存在。

若类在析构时无需执行任何清理操作,则完全无需定义析构函数,可直接让编译器为该类生成隐式析构函数。


关于 std::exit() 函数的警告

在第 8.12 节——程序终止(提前退出程序)中,我们讨论了 std::exit() 函数,它可用于立即终止程序。当程序被立即终止时,程序会直接结束。局部变量不会被事先销毁,因此析构函数也不会被调用。若依赖析构函数执行必要清理工作,此类情况需格外谨慎。

进阶读者须知:
未处理的异常同样会导致程序终止,且终止前可能不会进行栈展开。若未执行栈展开,程序终止前将不会调用析构函数。

posted @ 2026-01-02 16:10  游翔  阅读(12)  评论(0)    收藏  举报