shared_ptr通过引用计数管理资源,析构或reset时计数减一,归零才释放对象;循环引用需用weak_ptr打破,其lock()获取临时shared_ptr,避免内存泄漏。

shared_ptr 的基本用法和资源释放时机
shared_ptr 是 C++11 引入的引用计数型智能指针,它不独占所指对象,而是通过内部的控制块(control block)维护一个引用计数。只要还有至少一个 shared_ptr 指向该对象,对象就不会被销毁。
常见错误是误以为赋值或函数传参会立即拷贝对象——其实只是增加引用计数,底层对象地址不变:
std::shared_ptra = std::make_shared (42); std::shared_ptr b = a; // 引用计数从1→2,不是深拷贝 std::cout << *a << " " << *b; // 都输出42
- 用
std::make_shared构造,比(...) shared_ptr更高效(一次内存分配)(new T(...)) - 析构或重置(
reset())时引用计数减一;计数归零才调用T的析构函数和控制块的释放 - 不能用
shared_ptr管理栈对象或重复管理同一块堆内存(否则 double-free)
循环引用为什么发生,以及如何识别
当两个(或多个)shared_ptr 相互持有对方管理的对象时,引用计数永远无法归零,导致内存泄漏。典型场景是父子结构、观察者模式、双向链表节点。
例如:父类持有一个 shared_ptr,子类又持有一个 shared_ptr —— 即使外部所有 shared_ptr 都离开作用域,这对父子仍互相“拉住”对方。
立即学习“C++免费学习笔记(深入)”;
可借助工具辅助识别:
- 运行时加日志:在类析构函数里打印,确认是否真的被调用
- 用 AddressSanitizer 或 Valgrind 检测未释放内存,配合堆栈回溯定位
- 静态分析工具(如 clang-tidy 的
misc-uniqueptr-reset-release类规则,虽不直接报循环引用,但能提示可疑的裸指针/共享持有)
用 weak_ptr 打破循环引用的实际写法
weak_ptr 不参与引用计数,只提供对 shared_ptr 所指对象的“弱观察”。它必须先调用 lock() 转成 shared_ptr 才能安全访问对象,而 lock() 返回空 shared_ptr 表示原对象已被释放。
关键点在于:谁该用 weak_ptr?一般由生命周期较短、或逻辑上“非拥有方”的一方使用。比如子节点不应强持有父节点:
struct Parent;
struct Child {
std::weak_ptr parent_; // 不增加 Parent 的引用计数
void do_something() {
auto p = parent_.lock(); // 尝试升级为 shared_ptr
if (p) {
std::cout << "parent still alive\n";
} else {
std::cout << "parent already destroyed\n";
}
}
}; -
weak_ptr构造只能来自同源的shared_ptr(或另一个weak_ptr),不能由裸指针构造 - 不要长期缓存
lock()的结果;若需多次访问,应保存该shared_ptr并检查是否为空 -
weak_ptr::expired()是lock().get() == nullptr的快捷写法,但不推荐用于条件分支——因为可能在判断后瞬间释放
自定义删除器与线程安全的注意事项
shared_ptr 的控制块本身是线程安全的(引用计数增减原子),但所指对象的读写仍需用户保证线程安全。另外,自定义删除器会影响对象释放方式,也容易出错。
常见陷阱:
- 删除器类型必须完全匹配:若用
shared_ptr,删除器类型是 lambda,会占用额外空间;而(new int, [](int* p){ delete p; }) shared_ptr可优化为无状态,更紧凑(new int, std::default_delete {}) - 跨 DLL/so 边界传递带自定义删除器的
shared_ptr时,若删除器定义不在同一模块,可能引发符号未定义或析构失败 - 多线程中频繁拷贝/析构
shared_ptr会有原子操作开销,高并发场景下可考虑用std::unique_ptr+ 明确所有权转移,或用对象池规避频繁分配
循环引用不是“用了 shared_ptr 就一定会出”,而是暴露了设计中隐含的生命周期耦合。真正难的从来不是怎么加 weak_ptr,而是判断哪边该“放手”——这得看业务语义,不是靠语法补丁能绕过去的。









