C++ 智能指针怎么选?三种核心写法一次讲清

编程狮(w3cschool.cn) 2026-10-08 07:01:33 浏览数 (44)
反馈

C++ 智能指针怎么选?三种核心写法一次讲清

你写的 C++ 程序有没有出现过这种情况:函数里 new 了一个对象,中途抛了异常提前返回,delete 没来得及执行,内存就这么悄悄漏掉了。核心答案一句话:能用 unique_ptr 就别用 shared_ptr;只有需要多个地方共享同一对象所有权时才用 shared_ptr;而 weak_ptr 几乎只为一件事存在——打破 shared_ptr 之间的循环引用。这篇基于 C++11 起的标准库讲清三种 C++ 智能指针的边界和代价,帮你把内存安全写进代码习惯。今天这篇文章,编程狮就把这块讲透。

先看结论

三种智能指针不是等级递进,而是分工不同。下表把“谁该用哪一个”直接对齐:

你的场景 首选指针 一句话理由
一个对象只有一个所有者 unique_ptr 零额外开销,独占且不可拷贝
多个地方共享同一对象 shared_ptr 引用计数自动管理生命周期
需要观察但不可拥有 weak_ptr 不增加计数,专破循环引用

三种 C++ 智能指针怎么选

一、为什么需要智能指针

理解 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 对比

四、横向对比与选型建议

指针 所有权 额外开销 能否拷贝 典型用途
unique_ptr 独占 几乎为零 仅可移动 单一所有者对象
shared_ptr 共享 引用计数 可拷贝 多持有者共享
weak_ptr 不拥有 极小 可拷贝 打破循环、缓存观察

选型时还有个常被忽略的点:shared_ptr 的引用计数要额外占用一块控制块内存,当对象很小却大量共享时,这块开销也值得留意。

踩坑清单

  1. 现象:程序退出后内存不降;原因:shared_ptr 互相引用形成环;修复:把环上任意一条改成 weak_ptr。
  2. 现象:double free 崩溃;原因:同一裸指针被两个智能指针接管;修复:只用 make 系列,不混用裸指针。
  3. 现象:shared_ptr 拷贝变慢;原因:高频拷贝触发原子计数;修复:能用 unique_ptr 就别共享。
  4. 现象:weak_ptr 取不到值;原因:lock() 时对象已释放返回空;修复:使用前判空再操作。

总结

C++ 智能指针的选用顺序是先想所有权:只有一个所有者就用 unique_ptr,它零额外开销又最安全;确实需要多地方共享才上 shared_ptr;weak_ptr 则专门用来切断 shared_ptr 的循环引用。把“手动 delete”从你的代码里删掉,内存泄漏会少一大半。

要点带走:

  • unique_ptr 是默认首选,独占且几乎零开销;
  • shared_ptr 用于共享,但要警惕引用计数的额外代价;
  • weak_ptr 不拥有对象,是解决循环引用的钥匙。

下一步可以练手把项目里的裸指针逐步替换为 unique_ptr,体会 RAII 带来的安心感。

动手验证清单

下面这组动作帮你在本机验证智能指针行为,按你自己的环境执行,结果以实际输出为准(本文代码未在作者本机编译运行,仅给出预期形态):

  1. 安装:确认编译器支持 C++11 及以上,如 g++ --version;
  2. 配置:用 g++ -std=c++11 main.cpp -o main 开启标准;
  3. 命令:在代码里用 make_unique 造对象并 std::move 转移;
  4. 运行:./main 观察是否正常退出无崩溃;
  5. 检查:用 valgrind ./main 查看是否有内存泄漏报告;
  6. 预期:unique_ptr 场景下泄漏报告为空;
  7. 失败:若报 double free,说明裸指针被多个指针接管,检查构造方式;
  8. 修复:统一改用 make_shared / make_unique;
  9. 验证:把互相引用的 shared_ptr 之一改为 weak_ptr,再次跑 valgrind 确认归零。

延伸学习

想把 C++ 内存管理吃透,可以顺着这条线补:

  1. 先过一遍 C++ 入门课程,把语法地基打平;
  2. 内存边界相关看 C++ 栈空间魔法:释放内存的秘密;
  3. 想弄清 C 与 C++ 怎么选,读 C++ 与 C 语言区别。

常见问题

Q:unique_ptr 能放进容器里吗?

A:可以,而且推荐。用 std::vector<std::unique_ptr<Foo>> 既保证独占又能批量管理,元素移动时所有权跟着走,不会泄漏。

Q:shared_ptr 的引用计数是线程安全的吗?

A:计数本身的增减是线程安全的,但指向的对象内容不是。多个线程同时改对象仍需自己加锁,不要误以为用了 shared_ptr 就万事大吉。

Q:什么时候非用裸指针不可?

A:当你明确不做资源管理、只是临时借用(比如函数参数传递观测),可以用裸指针或引用。但对象的所有权必须清楚地由某个智能指针掌管。

0 人点赞