
你写的 C++ 程序有没有出现过这种情况:函数里 new 了一个对象,中途抛了异常提前返回,delete 没来得及执行,内存就这么悄悄漏掉了。核心答案一句话:能用 unique_ptr 就别用 shared_ptr;只有需要多个地方共享同一对象所有权时才用 shared_ptr;而 weak_ptr 几乎只为一件事存在——打破 shared_ptr 之间的循环引用。这篇基于 C++11 起的标准库讲清三种 C++ 智能指针的边界和代价,帮你把内存安全写进代码习惯。今天这篇文章,编程狮就把这块讲透。
先看结论
三种智能指针不是等级递进,而是分工不同。下表把“谁该用哪一个”直接对齐:
| 你的场景 | 首选指针 | 一句话理由 |
|---|---|---|
| 一个对象只有一个所有者 | unique_ptr | 零额外开销,独占且不可拷贝 |
| 多个地方共享同一对象 | shared_ptr | 引用计数自动管理生命周期 |
| 需要观察但不可拥有 | weak_ptr | 不增加计数,专破循环引用 |

一、为什么需要智能指针
理解 C++ 智能指针之前,先看清裸指针的问题:释放责任靠人记。在 new 和 delete 之间,只要有一条提前 return 的路径、一次抛异常、或一处漏写,内存就永远回不来。更糟的是重复 delete 会直接让程序崩溃。
智能指针用的是 RAII 思路:把资源绑定到对象生命周期上,对象离开作用域时析构函数自动释放。你不再手动 delete,编译器替你兜底。
现代 C++ 的官方指南也明确建议优先用智能指针管理动态资源,而不是裸指针。先过一遍 C++ 教程 把语法地基打平。
⚠️ 注意:不要写
std::shared_ptr<Foo& p(new Foo)这种混用裸指针的写法,一旦中途异常或拷贝,容易 double free。统一用make_shared/make_unique。
二、unique_ptr:最该优先用的独占指针
unique_ptr 是 C++ 智能指针里表达“独占所有权”的那一个:同一时刻只有一个 unique_ptr 指向这个对象,不能拷贝,只能移动。它没有引用计数,开销和裸指针几乎一样,却能保你不会漏释放。
#include <memory>
struct Foo { int x = 0; };
// 用 make_unique 创建,离开作用域自动释放
std::unique_ptr<Foo> p = std::make_unique<Foo>();
p->x = 18;
// 转移所有权给另一个 unique_ptr
std::unique_ptr<Foo> q = std::move(p);
上面这段先用 make_unique 造对象,离开作用域时自动 delete;std::move(p) 把所有权转给 q,此后 p 变成空指针。预期结果是对象只被销毁一次,没有泄漏。
绝大多数“一个所有者”的场景都该默认用 unique_ptr,它是智能指针里的第一选择。
三、shared_ptr 与 weak_ptr:共享与破环
当多个容器或对象都要长期持有同一个实例时,C++ 智能指针里的 shared_ptr 就派上用场,unique_ptr 搬不动。它内部维护一个引用计数,拷贝时加一、销毁时减一,归零才真正释放。如果你从 C 语言转来,可以先看 C 语言教程 对比一下内存模型。而 weak_ptr 只“观察”这个对象,不增加计数,因此不会阻止对象释放。
3.1 shared_ptr:引用计数下的共享
#include <memory>
auto a = std::make_shared<int>(42);
auto b = a; // 引用计数变为 2
// a 和 b 都离开作用域后,计数归零,对象才被释放
auto b = a 让两个 shared_ptr 指向同一块内存,计数加一;只有当最后一个持有者析构,内存才释放。代价是每次拷贝都要原子地更新计数,比 unique_ptr 稍重。
3.2 weak_ptr:打破循环引用
如果两个对象互相用 shared_ptr 指着对方,计数永远不为 0,就会形成内存泄漏。weak_ptr 正是解药:它不增加计数,需要时再用 lock() 临时提升成 shared_ptr。
#include <memory>
struct Node {
std::weak_ptr<Node> next; // 用 weak_ptr 而不是 shared_ptr
};
把互相指向的一方改成 weak_ptr,循环就被切断,计数能够正常归零。

四、横向对比与选型建议
| 指针 | 所有权 | 额外开销 | 能否拷贝 | 典型用途 |
|---|---|---|---|---|
| unique_ptr | 独占 | 几乎为零 | 仅可移动 | 单一所有者对象 |
| shared_ptr | 共享 | 引用计数 | 可拷贝 | 多持有者共享 |
| weak_ptr | 不拥有 | 极小 | 可拷贝 | 打破循环、缓存观察 |
选型时还有个常被忽略的点:shared_ptr 的引用计数要额外占用一块控制块内存,当对象很小却大量共享时,这块开销也值得留意。
踩坑清单
- 现象:程序退出后内存不降;原因:shared_ptr 互相引用形成环;修复:把环上任意一条改成 weak_ptr。
- 现象:double free 崩溃;原因:同一裸指针被两个智能指针接管;修复:只用 make 系列,不混用裸指针。
- 现象:shared_ptr 拷贝变慢;原因:高频拷贝触发原子计数;修复:能用 unique_ptr 就别共享。
- 现象:weak_ptr 取不到值;原因:
lock()时对象已释放返回空;修复:使用前判空再操作。
总结
C++ 智能指针的选用顺序是先想所有权:只有一个所有者就用 unique_ptr,它零额外开销又最安全;确实需要多地方共享才上 shared_ptr;weak_ptr 则专门用来切断 shared_ptr 的循环引用。把“手动 delete”从你的代码里删掉,内存泄漏会少一大半。
要点带走:
- unique_ptr 是默认首选,独占且几乎零开销;
- shared_ptr 用于共享,但要警惕引用计数的额外代价;
- weak_ptr 不拥有对象,是解决循环引用的钥匙。
下一步可以练手把项目里的裸指针逐步替换为 unique_ptr,体会 RAII 带来的安心感。
动手验证清单
下面这组动作帮你在本机验证智能指针行为,按你自己的环境执行,结果以实际输出为准(本文代码未在作者本机编译运行,仅给出预期形态):
- 安装:确认编译器支持 C++11 及以上,如
g++ --version; - 配置:用
g++ -std=c++11 main.cpp -o main开启标准; - 命令:在代码里用
make_unique造对象并std::move转移; - 运行:
./main观察是否正常退出无崩溃; - 检查:用
valgrind ./main查看是否有内存泄漏报告; - 预期:unique_ptr 场景下泄漏报告为空;
- 失败:若报 double free,说明裸指针被多个指针接管,检查构造方式;
- 修复:统一改用
make_shared/make_unique; - 验证:把互相引用的 shared_ptr 之一改为 weak_ptr,再次跑 valgrind 确认归零。
延伸学习
想把 C++ 内存管理吃透,可以顺着这条线补:
- 先过一遍 C++ 入门课程,把语法地基打平;
- 内存边界相关看 C++ 栈空间魔法:释放内存的秘密;
- 想弄清 C 与 C++ 怎么选,读 C++ 与 C 语言区别。
常见问题
Q:unique_ptr 能放进容器里吗?
A:可以,而且推荐。用 std::vector<std::unique_ptr<Foo>> 既保证独占又能批量管理,元素移动时所有权跟着走,不会泄漏。
Q:shared_ptr 的引用计数是线程安全的吗?
A:计数本身的增减是线程安全的,但指向的对象内容不是。多个线程同时改对象仍需自己加锁,不要误以为用了 shared_ptr 就万事大吉。
Q:什么时候非用裸指针不可?
A:当你明确不做资源管理、只是临时借用(比如函数参数传递观测),可以用裸指针或引用。但对象的所有权必须清楚地由某个智能指针掌管。

TRAE-AI编程



