11. 析构函数与资源释放:RAII思想、在析构函数中释放动态内存、关闭文件句柄

好,咱们今天聊一个C++里特别实在的话题——析构函数和资源释放。

说实话,我见过太多新手写C++代码,new了不知道delete,打开文件忘了关。程序跑起来没问题,一跑久了内存就爆了。嗯,这就是典型的资源泄漏。

那C++是怎么解决这个问题的?核心思想就四个字母:RAII

11.1 RAII:资源获取即初始化

RAII的全称是 Resource Acquisition Is Initialization,翻译过来就是「资源获取即初始化」。名字听着挺绕,其实说白了就是一句话:把资源和对象的生命周期绑定在一起

你想想看,对象在构造时获取资源,在析构时释放资源。只要对象活着,资源就在;对象死了,资源自动释放。这不就完美解决了资源泄漏的问题吗?

核心原则:

  • 构造函数:获取资源(分配内存、打开文件、获取锁)
  • 析构函数:释放资源(释放内存、关闭文件、释放锁)
  • 对象生命周期 = 资源持有期

我个人习惯把RAII叫做「C++的自动垃圾回收」。虽然它不像Java的GC那样后台跑,但胜在确定性——资源什么时候释放,程序员心里门儿清。

11.2 在析构函数中释放动态内存

咱们先看一个最经典的场景:动态内存管理。

class Buffer {
private:
    char* data;
    size_t size;

public:
    Buffer(size_t n) : size(n) {
        data = new char[n];
        // 构造时获取资源
    }

    ~Buffer() {
        delete[] data;
        // 析构时释放资源
    }
};

这段代码看着简单,但背后有个重要的设计哲学:谁分配,谁释放

我在项目中遇到过这样一个坑:有个同事写了一个类,构造函数里new了内存,但析构函数里忘了delete。结果程序跑了三天,内存从200MB涨到了2GB,最后OOM被系统杀了。排查了半天,就少了一行 delete[]

注意:如果类里有多个指针成员,析构函数里要逐个释放。但更推荐的做法是——直接用 std::unique_ptrstd::vector,让标准库帮你管内存。

比如上面的Buffer,用现代C++可以写成这样:

class Buffer {
private:
    std::vector<char> data;

public:
    Buffer(size_t n) : data(n) {}
    // 不需要写析构函数,vector自己会释放
};

你看,连析构函数都不用写了。这就是RAII的威力——标准库容器本身就是RAII的典范

11.3 关闭文件句柄

除了内存,另一个常见的资源就是文件句柄。操作系统对打开的文件数量是有限制的,你不关,别人就开不了。

咱们先看一个反面教材:

void processFile(const char* filename) {
    FILE* fp = fopen(filename, "r");
    if (!fp) return;

    // ... 处理文件内容 ...

    // 忘记 fclose(fp) 了!
    // 文件句柄泄漏了
}

这种代码我见过太多次了。函数正常返回还好,万一中间抛了个异常,fclose 根本执行不到。文件句柄就这么白白泄漏了。

正确的做法是封装一个RAII类:

class FileGuard {
private:
    FILE* fp;

public:
    FileGuard(const char* filename, const char* mode) {
        fp = fopen(filename, mode);
        if (!fp) {
            throw std::runtime_error("打开文件失败");
        }
    }

    ~FileGuard() {
        if (fp) {
            fclose(fp);
        }
    }

    // 禁止拷贝
    FileGuard(const FileGuard&) = delete;
    FileGuard& operator=(const FileGuard&) = delete;

    // 提供文件操作接口
    void write(const char* data) {
        fprintf(fp, "%s", data);
    }
};

用起来就简单了:

void safeProcess() {
    FileGuard file("log.txt", "w");
    file.write("程序启动\n");
    // 函数结束,file析构,文件自动关闭
    // 就算中间抛异常,析构函数也会执行
}

我曾经在一个嵌入式项目里,就是因为文件句柄泄漏,导致设备运行三天后无法写入日志。排查了一整天,最后发现是一个 fopen 没有对应的 fclose。从那以后,我所有涉及文件操作的代码,一律用RAII封装。

11.4 RAII的更多应用场景

RAII不止管内存和文件,它几乎可以管理所有资源:

资源类型 获取方式 释放方式 RAII封装示例
动态内存 new delete std::unique_ptr, std::vector
文件句柄 fopen fclose 自定义FileGuard
互斥锁 lock unlock std::lock_guard
数据库连接 connect disconnect 自定义ConnectionGuard
套接字 socket close 自定义SocketGuard

我的建议:只要遇到「获取-使用-释放」这种模式的资源,第一反应就是写一个RAII封装类。别嫌麻烦,这能省掉你未来无数个debug的夜晚。

11.5 析构函数中的异常处理

这里有个重要警告:析构函数里不要抛异常

为什么?因为如果析构函数在执行过程中抛了异常,而这时候栈展开已经在进行中(比如另一个异常正在传播),程序会直接调用 std::terminate 终止。

我曾经在一个高并发服务里踩过这个坑。析构函数里调了一个可能抛异常的函数,结果服务在高峰期莫名其妙地崩溃。查了core dump才发现是析构函数抛异常导致的。

正确的做法是:

class SafeResource {
public:
    ~SafeResource() noexcept {
        try {
            releaseResource();
        } catch (...) {
            // 记录日志,但绝不往外抛
            // 可以调用 std::terminate 或者吞掉异常
        }
    }
};

记住:析构函数默认就是 noexcept 的,你如果让它抛异常,编译器会直接调用 terminate。

11.6 知识体系总览

下面这张图总结了本章的核心逻辑:

RAII 核心思想与资源管理 RAII 思想 动态内存管理 new / delete std::unique_ptr std::vector 文件句柄管理 fopen / fclose FileGuard std::fstream 其他资源管理 互斥锁 数据库连接 套接字 核心原则:构造获取资源,析构释放资源

11.7 总结

聊了这么多,其实核心就几点:

  • RAII是C++资源管理的基石。把资源和对象生命周期绑定,让析构函数自动完成清理工作。
  • 动态内存用智能指针。能不用裸指针就不用,unique_ptrshared_ptr 是你的好朋友。
  • 文件句柄要封装。写一个简单的RAII包装类,比每次都手写 fclose 靠谱得多。
  • 析构函数别抛异常。这是铁律,违反就等着程序崩溃吧。

说实话,RAII这个思想我用了十几年,越用越觉得它精妙。它不像某些语言那样靠运行时GC来兜底,而是把资源管理的责任交给了对象的生命周期——你只要把对象构造好、析构好,资源就不会泄漏。

嗯,这就是C++的魅力所在。


公众号:蓝海资料掘金营,微信 deep3321