shared_ptr的引用计数是原子的,但仅保证同一shared_ptr对象的拷贝/赋值/析构线程安全;其指向资源的访问仍需手动同步,且循环引用需用weak_ptr破除。

shared_ptr 的引用计数不是原子的?别被默认假设骗了
默认情况下,shared_ptr 的引用计数操作是线程安全的——但仅限于对**同一个 shared_ptr 对象**的拷贝、赋值、析构。它不保护你指向的资源本身,也不保证多个 shared_ptr 指向同一块内存时的并发修改安全。
常见错误现象:std::shared_ptr 然后在两个线程里分别执行 p.reset() 或 p = nullptr; —— 这没问题;但如果一个线程在改 *p = 100;,另一个在 p.reset(),就可能触发未定义行为(UB),因为 *p 访问和析构竞争。
- 引用计数内部使用原子操作(如
std::atomic),所以shared_ptr对象本身的生命周期管理是线程安全的 - 但指向的对象(即
operator*/operator->访问的目标)完全不自动加锁 - 若需多线程读写共享对象,仍要配合
std::mutex或std::atomic显式同步
make_shared 比 new + shared_ptr(...) 更高效,但不能用于自定义删除器
std::make_shared 在一次内存分配中同时构造控制块和对象,而 shared_ptr 构造函数配合 new 需要两次分配(一次给对象,一次给控制块),性能差异在高频创建场景下明显。
但如果你需要自定义删除器(比如关闭文件描述符、调用 sqlite3_finalize),make_shared 无法传入删除器参数——它只接受构造参数列表,删除器必须通过 shared_ptr 的构造函数指定:
立即学习“C++免费学习笔记(深入)”;
auto p1 = std::make_shared(42); // ✅ 简洁高效 auto p2 = std::shared_ptr (fopen("log.txt", "w"), [](FILE* f) { fclose(f); }); // ✅ 自定义删除器 // auto p3 = std::make_shared ("log.txt", "w", [](FILE* f){...}); // ❌ 编译失败
-
make_shared不支持自定义分配器(除非 C++20 的allocate_shared) - 若对象构造可能抛异常,
make_shared保证“全有或全无”:要么控制块+对象都成功,要么都不分配 - 注意:
make_shared会转发参数给对象的构造函数,不会调用operator new的重载版本(除非你特化了分配器)
循环引用导致内存泄漏:weak_ptr 是解药,不是装饰品
当两个 shared_ptr 相互持有(比如双向链表节点、观察者与被观察者),引用计数永远不会降到 0,对象无法释放——这就是循环引用。编译器不会报错,运行时也无提示,只会悄悄吃掉内存。
典型场景:class Node { std::shared_ptr 若 a->next = b; 且 b->prev = a;,则 a 和 b 的引用计数各为 2,即使外部所有 shared_ptr 都离开作用域,它们仍互相“挽留”。
- 解决方式:将其中一个方向改为
std::weak_ptr(如prev),访问前用lock()转成临时shared_ptr -
weak_ptr不增加引用计数,也不阻止对象销毁;lock()返回空shared_ptr表示目标已被释放 - 不要用
weak_ptr::operator->()直接访问——它不检查有效性;必须先if (auto p = wp.lock()) { use(*p); }
reset()、assign()、swap() 的行为差异影响资源释放时机
shared_ptr 的几个成员函数看似都能“换掉”当前指针,但释放旧资源的时机和异常安全性不同。
例如:p.reset(new int(10)); 先销毁旧对象,再构造新对象;而 p = std::make_shared 是先构造新对象、再交换控制块、最后销毁旧对象——后者更安全,因为如果构造失败(如内存不足),原 p 不受影响。
-
reset():立即释放当前所拥有的资源;若传入新指针,会先释放旧资源再接管新资源;不提供异常安全保证 -
operator=(赋值):强异常安全——新资源构造成功后才释放旧资源 -
swap():无异常,常用于避免临时对象开销,比如在函数返回前交换局部shared_ptr和成员变量 - 慎用
get():返回裸指针,但不转移所有权;若用它构造另一个shared_ptr(如shared_ptr),会引发双重释放(p.get())
引用计数本身轻量,但误用 weak_ptr、忽略线程边界、或把 get() 当万能接口,才是实际项目中最容易漏掉的坑。










