外观
Stage07|Modern C++ Foundation
Lesson084|RAII:用对象生命周期管理资源
Tags: #C++17 #RAII #Resource #ExceptionSafetyDifficulty: ⭐⭐⭐⭐☆
一、问题场景:中途 return 后谁来解锁
cpp
mutex.lock();
if (!ready()) return; // 忘记 unlock
doWork();
mutex.unlock();资源泄漏不只指内存。锁、文件、Socket、GPU 句柄、临时状态都需要在每条退出路径上恢复。RAII 把资源释放绑定到对象析构,使正常返回和异常路径使用同一套清理机制。
二、本课目标
- 理解 RAII 的获取、持有和释放。
- 使用标准库管理内存、文件和锁。
- 为 C API 句柄设计自定义 deleter。
- 理解异常安全的基本保证。
- 识别析构函数的责任边界。
三、回到 TaskSystem:谁负责锁和线程
TaskSystem 会持有 mutex、条件变量、线程和可能的文件/Native 句柄。任何 return、异常或构造失败都不能遗留这些资源。RAII 让 owner 的析构负责释放,但析构之前仍要定义停止顺序:先禁止新任务,再唤醒 worker,最后 join。
四、RAII 不是“智能指针技巧”
text
构造成功 → 对象拥有资源
对象可用 → 资源可用
离开作用域 → 析构释放资源标准库例子:
cpp
std::vector<int> buffer(1024); // 管动态内存
std::ifstream input("config.json"); // 管文件
std::lock_guard<std::mutex> lock(m); // 管锁它们让资源状态服从词法作用域,不必依赖调用者记住某个 cleanup()。
五、所有退出路径自动清理
cpp
bool save(const std::string& path) {
std::ofstream out(path, std::ios::binary);
if (!out) return false;
out.write("OK", 2);
return out.good();
}无论在哪个 return 离开,out 都会析构并关闭文件。注意“关闭”不等于“写入一定成功”,仍要检查错误状态。
六、包装 C 句柄
cpp
#include <cstdio>
#include <memory>
struct FileCloser {
void operator()(std::FILE* file) const noexcept {
if (file) std::fclose(file);
}
};
using FilePtr = std::unique_ptr<std::FILE, FileCloser>;
FilePtr openFile(const char* path) {
return FilePtr(std::fopen(path, "rb"));
}类型本身表达“这个指针退出作用域时应调用 fclose”,避免把 delete 用在错误资源上。
七、异常安全
常见保证:
text
不抛异常保证:操作承诺不抛
强保证:失败后对象保持调用前状态
基本保证:失败后对象仍有效、没有资源泄漏
无保证:失败可能破坏状态析构函数通常应 noexcept,不能在栈展开期间再次抛异常。可能失败的显式提交动作,例如网络 flush,应提供可检查的 commit(),而不是把所有错误藏到析构中。
八、可编译验证:作用域锁
cpp
#include <iostream>
#include <mutex>
std::mutex mutex;
void update(bool skip) {
std::lock_guard<std::mutex> guard(mutex);
std::cout << "locked\n";
if (skip) return;
std::cout << "updated\n";
}
int main() {
update(true);
update(false);
}两次调用都不会死锁,因为 guard 在每次函数退出时解锁。然后把它改为手工 lock/unlock,复现早退导致的阻塞。
九、TaskSystem 的停止顺序
析构时不能先销毁队列或 mutex,再让 worker 继续运行;也不能只设置标志而不唤醒等待线程。正确顺序是状态转换、通知、worker 取完允许的任务、join、最后释放共享字段。
十、Cocos/Native 连接
原生扩展常持有纹理、文件、线程任务或平台句柄。应让 C++ wrapper 拥有和释放资源,再通过 JSB 暴露安全操作;不要让 JavaScript 猜测应该调用哪一种平台释放函数。跨线程资源还要规定在哪个线程析构。
十一、诊断清单
text
资源由哪个对象拥有?
构造失败是否泄漏已获取资源?
移动对象后所有权是否唯一?
析构需要在哪个线程执行?
释放函数是否和创建函数匹配?
显式 close 失败如何报告?十二、练习与答案
1. RAII 为什么能处理异常?
异常展开会析构已经完整构造的局部对象,因此资源 wrapper 的析构仍会执行。
2. 为什么不能只依赖 GC 管原生资源?
GC 回收时机不确定,且不了解文件、锁、GPU 句柄的线程和释放语义。
3. 哪些资源不适合只在析构中报告错误?
需要业务确认结果的提交型操作,例如文件落盘或网络发送,应有显式方法返回错误,析构只兜底释放。
十三、本课总结
RAII 的核心是把“资源可用期”变成“对象生命周期”。它减少清理分支,也是智能指针、容器、锁和可靠 C++ 接口的共同基础。
