std::shared_ptr通过控制块实现引用计数管理:控制块含对象指针、强/弱引用计数、销毁逻辑;拷贝增计数,析构减计数,归零时删除对象;控制块与对象默认分内存分配,make_shared可合并分配提升性能;控制块线程安全,但托管对象需额外同步;weak_ptr不增计数,须lock()获取临时shared_ptr才可安全访问;适用于多所有者场景,单一所有者应选unique_ptr或栈对象。

std::shared_ptr 是怎么靠引用计数活下来的
它不靠魔法,就靠一个额外的控制块(control block):里面存着指向对象的指针、当前引用计数、销毁逻辑(比如自定义 deleter),还有弱引用计数。每次 shared_ptr 拷贝,控制块里的引用计数加 1;每次析构或重置,减 1;减到 0 就 delete 对象。
注意:控制块和托管对象**不一定在一块内存里**。默认情况下,shared_ptr<T>(new T) 会分别分配两块内存——对象一块,控制块一块。这有性能开销,也影响缓存局部性。
- 用
make_shared<T>()可以把对象和控制块**一次分配、连续布局**,省一次 malloc,也更快 - 但
make_shared不支持自定义分配器传给 T 的构造函数(C++17 起部分支持),如果 T 的构造函数需要非常规参数且依赖分配器,可能得退回到new+shared_ptr - 控制块本身是线程安全的(引用计数增减是原子操作),但被管理的对象不是——别以为用了
shared_ptr就能随便多线程读写对象
weak_ptr 不增加引用计数,但为什么不能直接解引用
weak_ptr 只观察,不参与所有权。它内部只存指向控制块的指针,不碰强引用计数。所以它自己析构不会影响对象生死,也不会阻止对象被释放。
正因为如此,你不能直接用 operator* 或 operator->——它没保证对象还活着。必须先调 lock(),得到一个临时 shared_ptr,或者用 expired() 判断。
立即学习“C++免费学习笔记(深入)”;
-
lock()成功返回非空shared_ptr,失败返回空;这是唯一安全拿强引用的方式 - 别写
if (!wp.expired()) { auto p = wp.lock(); ... }—— 这中间可能已被其他线程释放,还是得用lock()结果判断 -
weak_ptr常用于打破循环引用:A 持有shared_ptr<B>,B 用weak_ptr<A>回指,这样 A 和 B 都能正常析构
手动实现简易 shared_ptr 时最容易崩在哪
核心就三件事:控制块生命周期、引用计数线程安全、析构时机。崩点全在这儿。
- 控制块必须和
shared_ptr实例共存亡——不能用裸指针存控制块地址,得用shared_ptr<control_block>管理它自己?不行,死循环。正确做法是控制块里用裸指针指向数据,再用原子整数管计数 - 引用计数必须是原子操作(如
std::atomic_int),否则多线程拷贝/析构会撕裂计数器,导致提前释放或内存泄漏 - 别忘了 weak 引用计数:即使没人用
shared_ptr,只要还有weak_ptr活着,控制块就得留着(因为要回答expired());只有强+弱计数都为 0,控制块才能删 - 自定义 deleter 如果捕获了外部变量,得确保它和控制块一起分配,否则
weak_ptr::lock()后执行 deleter 时可能访问已销毁闭包
什么时候不该用 shared_ptr
它解决的是「多个所有者共享同一资源」的问题。不是万能胶,乱贴反而出事。
- 单一所有者场景(比如函数内创建、函数末尾销毁)——用
unique_ptr或栈对象,零开销 - 只是临时观察、绝不会延长对象生命——用裸指针或引用更轻量,也更清晰表达意图
- 高频小对象(如 vector 里存几千个
shared_ptr<int>)——每个都带控制块开销,不如存值或用对象池 - 跨 DLL 边界传递(Windows 下尤其危险)——不同模块可能用不同堆,
shared_ptr在 A 模块 new,在 B 模块 delete,崩溃
引用计数本身不复杂,难的是边界条件:多线程、自定义删除器、异常安全、与裸指针混用……这些地方一松懈,就是夜半 core dump。










