C++ RAII 与智能指针
更新时间:2026-08-25。本文是
languages/cpp/主题第 ② 篇。RAII(Resource Acquisition Is Initialization,资源获取即初始化)是 C++ 最重要的工程思想——它把"手动管理资源"变成"对象生命周期自动管理",从根上消灭内存泄漏。
本文要回答的问题
- RAII 到底是什么?为什么能自动释放内存?
unique_ptr/shared_ptr/weak_ptr怎么选?- 什么是循环引用?
weak_ptr如何打破它?
资源生命周期:RAII 的核心

一、RAII 思想:构造获取、析构释放
cpp
class File {
FILE *fp_;
public:
File(const char *path) : fp_(fopen(path, "r")) { // 构造 = 获取资源
if (!fp_) throw std::runtime_error("open failed");
}
~File() { if (fp_) fclose(fp_); } // 析构 = 释放资源
// ... 读写接口
};使用 RAII 类后:
cpp
void read() {
File f("/etc/hostname"); // 进入作用域:打开
// ... 中途 return、抛异常都行
} // 离开作用域:自动关闭(析构必执行)关键点:C++ 保证局部对象离开作用域时一定调用析构函数(包括 return、异常、break)——这就是 RAII 异常安全的根基。
| 手动管理(C 风格) | RAII(现代 C++) |
|---|---|
fopen / fclose 配对 | 构造函数打开 / 析构关闭 |
忘记 fclose → 句柄泄漏 | 不可能忘记,离开作用域自动关 |
| 异常路径漏释放 | 异常也触发析构,无泄漏 |
锁 lock/unlock 配对 | std::lock_guard 自动解锁 |
二、三种智能指针:怎么选
| 智能指针 | 所有权 | 拷贝语义 | 典型场景 |
|---|---|---|---|
std::unique_ptr | 独占所有权 | 不可拷贝,可移动 | 默认首选:工厂返回、容器元素 |
std::shared_ptr | 共享所有权(引用计数) | 可拷贝,计数 +1 | 多所有者:缓存、回调 |
std::weak_ptr | 不拥有,观察共享对象 | 可拷贝 | 打破循环引用、缓存弱引用 |
2.1 unique_ptr:独占所有权(首选)
cpp
auto p = std::make_unique<int>(42); // C++14,推荐
// std::unique_ptr<int> q = p; // 编译错误:不可拷贝
auto q = std::move(p); // 移动转移所有权,p 变空- 零开销:与裸指针同尺寸同性能,
perf下无任何额外成本。 - 语义即文档:
unique_ptr传给函数 = "我借给你,用完还我"。
2.2 shared_ptr:共享所有权
cpp
auto s1 = std::make_shared<int>(42);
auto s2 = s1; // 引用计数 2
s1.reset(); // 计数 1
// s2 离开作用域时计数归 0,自动 delete- 引用计数线程安全,但控制块额外分配一次内存(性能略低于 unique_ptr)。
- 循环引用会让计数永不归零 → 泄漏,见下文。
2.3 weak_ptr:打破循环引用
cpp
struct Node {
std::shared_ptr<Node> next;
std::weak_ptr<Node> parent; // 用 weak 避免环
};
auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b; // b 被 a 持有
b->parent = a; // weak:不增加 a 的计数循环引用问题:如果 parent 也用 shared_ptr,a 和 b 互相持有、计数永不归零,内存泄漏。用 weak_ptr 观察但不拥有,打破环。
访问 weak_ptr 指向的对象前需 lock():
cpp
if (auto sp = parent.lock()) { // lock 成功说明对象还活着
sp->doSomething();
}三、自定义删除器与裸指针陷阱
3.1 自定义删除器
cpp
// 管理非 new 资源:文件句柄
auto closer = [](FILE *fp) { if (fp) fclose(fp); };
std::unique_ptr<FILE, decltype(closer)> f(fopen("/etc/hostname", "r"), closer);RAII 不只管内存——锁、文件、socket、GPU 资源都能用智能指针或专用 RAII 类管理。
3.2 常见坑
| 坑 | 示例 | 后果 |
|---|---|---|
用 new 不用 make_* | unique_ptr<T>(new T) | 额外一次分配/异常不安全 |
| 混用裸指针与智能指针 | p.get() 后 delete | 双重释放 |
| 循环引用 | shared_ptr 成环 | 泄漏 |
shared_ptr 存数组 | make_shared<int[]> 需 C++17 语法 | 析构不完整 |
把 this 交给 shared_ptr | 从裸 this 构造 | 双重管理 |
四、与本站主线衔接
- 内存泄漏排查:即使用了 RAII,第三方库或历史代码仍可能泄漏——用 Valgrind 验证"零泄漏"。
- 性能:
shared_ptr控制块分配、引用计数原子操作是真实开销;高频路径优先unique_ptr或引用。剖析方法见 perf。 - 异常安全:RAII 是异常安全三级别的基础,配合 crash 异常与信号 理解资源生命周期。
- 移动语义:智能指针的可移动不可拷贝正依赖移动语义,见 移动语义与右值引用。
一句话总结
RAII = 构造获取、析构释放,让资源生命周期与对象作用域绑定;三指针选择口诀:默认 unique_ptr、多所有者 shared_ptr、打破循环 weak_ptr——用它替换裸指针,C++ 程序的内存泄漏从"常见"变成"罕见"。
上一节:C++ 现代特性总纲 下一节:移动语义与右值引用