C++ 析构函数简介:自动清理资源的利器

清理问题

假设你正在编写一个需要通过网络发送数据的程序。由于与服务器建立连接代价高昂,你决定先收集一批数据,再一次性发送。该功能可能被封装为如下类:

// 本示例故意不完整,因此无法通过编译
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() 会立即终止程序。此时不会销毁局部变量,因而不会调用析构函数。若依赖析构函数完成关键清理,务必小心。

进阶提示
未捕获的异常也可能导致程序终止,且可能不会展开栈。若栈未展开,析构函数同样不会被调用。

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"