RAII 与智能指针笔记
RAII 与智能指针笔记
时间:2026/04/09
关键词:RAII、资源生命周期、异常安全、
unique_ptr、shared_ptr、weak_ptr、自定义 deleter、锁管理
核心目标:把“资源一定会被释放”这件事交给对象生命周期,而不是交给人脑记忆。
1. RAII 是什么
RAII 全称:
Resource Acquisition Is Initialization
资源获取即初始化
核心思想:
- 在对象构造时获取资源
- 在对象析构时释放资源
这样做的价值非常直接:
- 不容易忘记释放
- 遇到异常时也能自动回收
- 生命周期更清晰
- 接口更容易组合
这里的“资源”不只指内存,还包括:
- 动态内存
- 文件句柄
- socket
- 数据库连接
- 互斥锁
- 线程句柄
- GPU/系统资源
2. 为什么 RAII 在 C++ 里特别重要
C++ 有确定性析构:
1 | void f() { |
这意味着“资源释放”可以天然绑定到作用域结束。
相比手工写:
1 | open(); |
RAII 更稳,因为它不依赖你记得在每一条返回路径都调用 close()。
3. 没有 RAII 时最容易出什么问题
3.1 多返回路径漏释放
1 | FILE* fp = fopen("data.txt", "r"); |
3.2 异常打断控制流
1 | void f() { |
3.3 锁没有释放
1 | mtx.lock(); |
这些问题都可以通过 RAII 化简。
4. 最小 RAII 示例
1 |
|
要点:
- 构造函数负责获取资源
- 析构函数负责释放资源
- 禁止拷贝,避免多个对象同时认为自己拥有同一资源
5. 智能指针就是 RAII 的标准化实践
最常见的智能指针有三类:
std::unique_ptrstd::shared_ptrstd::weak_ptr
它们的本质都是:
- 用对象包装裸指针
- 把释放逻辑放进析构函数
6. std::unique_ptr:独占所有权
6.1 基本语义
一个资源在任意时刻只能有一个拥有者。
1 |
|
特点:
- 不可拷贝
- 可以移动
- 开销小
- 语义最清晰
6.2 为什么优先推荐 make_unique
1 | auto p = std::make_unique<std::string>("hello"); |
相比手写:
1 | std::unique_ptr<std::string> p(new std::string("hello")); |
更推荐 make_unique,因为:
- 更简洁
- 更不容易写出异常安全漏洞
- 类型推导更清楚
6.3 移动所有权
1 | std::unique_ptr<int> p1 = std::make_unique<int>(7); |
移动后:
p2接管资源p1变为空
6.4 作为参数怎么传
常见几种风格:
1 | void use(const std::unique_ptr<Foo>& p); // 只观察,不接管 |
经验上:
- 要表达“接管所有权”,就按值接收
unique_ptr - 只观察对象,不一定非要把参数写成智能指针类型
7. std::shared_ptr:共享所有权
7.1 基本语义
1 |
|
此时:
p1和p2共同拥有同一个对象- 内部通过引用计数决定何时释放
7.2 什么时候用 shared_ptr
只有在“确实存在共享拥有关系”时才使用。
典型场景:
- 对象要被多个异步任务共同持有
- 图结构/对象系统中需要共享生命周期
- 回调系统中对象可能跨作用域存活
7.3 成本是什么
shared_ptr 比 unique_ptr 重得多,因为它通常涉及:
- 控制块
- 原子引用计数
- 更复杂的生命周期管理
所以不要把它当默认选择。
8. std::weak_ptr:打破循环引用
8.1 为什么需要它
如果两个 shared_ptr 互相持有,就会形成循环,导致引用计数永远不归零。
1 | struct B; |
这会泄漏。
8.2 正确思路
把“非拥有关系”的那一侧改成 weak_ptr:
1 | struct B; |
8.3 使用方式
1 | if (auto sp = weak.lock()) { |
weak_ptr 的语义是:
- 我知道这个对象可能存在
- 但我不参与拥有
9. 自定义 deleter
有些资源不是 delete 释放的,例如:
FILE*要fclosemalloc对应free- socket / GPU handle / 句柄各有自己的释放函数
这时可以给智能指针指定自定义删除器。
1 |
|
这样离开作用域时就会自动 fclose。
10. make_shared 与 shared_ptr(new T)
10.1 为什么通常优先 make_shared
1 | auto p = std::make_shared<MyType>(args...); |
优点通常包括:
- 代码更简洁
- 常见实现里对象和控制块可一次分配
- 异常安全更直接
10.2 什么时候不能用 make_shared
比如你需要:
- 自定义 deleter
- 控制对象与控制块分离
- 与某些特殊分配策略配合
这时才考虑直接构造 shared_ptr。
11. RAII 不只用于内存
11.1 互斥锁
错误写法:
1 | mtx.lock(); |
推荐写法:
1 | std::lock_guard<std::mutex> lk(mtx); |
lock_guard 就是典型 RAII:
- 构造时加锁
- 析构时解锁
11.2 更灵活的 unique_lock
当你需要:
- 延迟加锁
- 手动
unlock - 配合条件变量
就用 std::unique_lock。
11.3 多锁场景
可以使用:
1 | std::scoped_lock lk(m1, m2); |
它也是 RAII,且能帮你规避一部分死锁风险。
12. 线程也应该 RAII 化
12.1 std::thread 的问题
std::thread 如果在析构前既没有 join() 也没有 detach(),程序会 std::terminate。
所以线程句柄本身也需要生命周期管理。
12.2 一个简单的 join guard
1 |
|
这也是典型 RAII:
- 构造时接管线程对象
- 析构时确保
join
如果是 C++20,可以直接优先考虑 std::jthread。
13. 异常安全为什么和 RAII 绑定得这么紧
RAII 最强的地方不是“少写一行 delete”,而是它天然支持异常安全。
看这段代码:
1 | void f() { |
即使 g() 抛异常:
lk会析构并解锁p会析构并释放对象
这就是现代 C++ 推荐“资源对象化”的原因。
14. 所有权设计比选哪种智能指针更重要
高质量 C++ 代码首先要明确:
- 谁拥有对象
- 谁只观察对象
- 谁负责释放对象
一个常见的经验顺序是:
- 默认用对象值语义
- 必须动态分配时,优先
unique_ptr - 只有确实共享拥有时才用
shared_ptr - 非拥有关系用裸指针、引用、
weak_ptr、std::span
也就是说,智能指针不是“越多越高级”,而是要表达清晰语义。
15. 常见误区
15.1 裸指针不等于拥有权
1 | Foo* p = get(); |
单看这一行,没人知道:
- 你是不是要
delete p - 这个对象是不是别人管理的
所以裸指针更适合表达“观察”,不适合表达“拥有”。
15.2 到处滥用 shared_ptr
很多代码用 shared_ptr 只是为了“省心”,结果带来:
- 生命周期更复杂
- 隐式共享关系
- 性能开销
- 循环引用问题
15.3 从 unique_ptr 里随手 get() 再乱传
1 | auto p = std::make_unique<Foo>(); |
这本身不一定错,但要非常清楚:
get()只拿观察指针- 不会转移所有权
- 不能让对方
delete
15.4 shared_from_this() 误用
如果对象不是由 shared_ptr 管理,直接 shared_from_this() 会出问题。
这类场景要配合 std::enable_shared_from_this 正确使用。
15.5 智能指针不是性能万能解
它们解决的是:
- 资源释放正确性
- 生命周期表达
而不是自动让程序变快。
在极热路径里,真正重要的还是:
- 数据布局
- 分配次数
- 缓存局部性
16. 和并发编程的关系
RAII 在并发里尤其重要,因为并发代码最怕“中途退出时资源没收干净”。
最常见的 RAII 对象:
std::lock_guardstd::unique_lockstd::scoped_lock- 线程 join guard
并发里的基本原则可以记成一句:
锁、线程、句柄这类资源,不要靠手动成对调用管理,优先对象化。
17. 一页总结
RAII 的本质不是某个库技巧,而是一种设计原则:
- 资源由对象接管
- 生命周期由作用域决定
- 释放在析构中自动完成
智能指针则是这条原则在内存管理上的标准化实现:
unique_ptr:独占所有权,默认首选shared_ptr:共享所有权,按需使用weak_ptr:非拥有观察,打破循环
如果只记三条经验:
- 能不用动态分配就不用
- 动态分配优先
unique_ptr - 共享拥有关系才用
shared_ptr
18. 建议继续补充的相关主题
和本篇衔接最紧密的内容:
- 左右值、移动语义与
std::move - 自定义 allocator 与内存池
enable_shared_from_thisscope_exit/ defer 风格工具- Rule of Five 与异常安全保证
19. 参考资料
cppreference: RAII
https://en.cppreference.com/w/cpp/language/raiicppreference:
std::unique_ptr
https://en.cppreference.com/w/cpp/memory/unique_ptrcppreference:
std::shared_ptr
https://en.cppreference.com/w/cpp/memory/shared_ptrcppreference:
std::weak_ptr
https://en.cppreference.com/w/cpp/memory/weak_ptr