在C++现代内存管理中,shared_ptr通过引用计数自动释放堆对象,但两个shared_ptr互相指向会形成闭环,导致计数永远不小于一,析构时内存无法回收。weak_ptr正是为标准库设计的弱引用智能指针,它绑定到shared_ptr管理的对象却不参与所有权,从而专门化解这类循环引用问题。

一、shared_ptr循环引用是怎么发生的
假设我们有一个双向链表节点,或者一个包含父指针和子指针的对象树。若父节点用shared_ptr保存子节点,子节点又用shared_ptr保存父节点,便构成了典型的循环引用。下面代码演示了这种错误结构:
#include <memory>
#include <iostream>
class B; // 前向声明
class A {
public:
std::shared_ptr<B> b_ptr;
~A() { std::cout << "A destroyedn"; }
};
class B {
public:
std::shared_ptr<A> a_ptr;
~B() { std::cout << "B destroyedn"; }
};
int main() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->b_ptr = b; // A持有B
b->a_ptr = a; // B持有A
// 离开作用域时,a和b的引用计数均为2,互相等待对方释放
return 0;
}
运行上面程序,终端不会打印任何析构信息。这是因为栈上的a和b离开main时,各自计数从二减到一,堆里的A和B仍被彼此的shared_ptr引用,控制块无法归零,操作系统最终回收进程空间才顺便清掉,属于明确的内存泄漏。
这种问题在观察者模式里也很常见:主题用shared_ptr保存观察者,观察者为了回调用shared_ptr保存主题,结果双方都死不掉。单纯增加代码复杂度去手动打破关系容易出错,所以标准库给出了weak_ptr作为语言级方案。
二、weak_ptr的基本能力与底层原理
weak_ptr必须从一个shared_ptr或另一个weak_ptr构造,它本身不控制对象生命周期,只观测。标准库把引用计数拆成两部分:共享计数(strong count)和弱计数(weak count)。shared_ptr拷贝影响强计数,weak_ptr拷贝只影响弱计数。当强计数归零,对象被销毁;只有当强弱计数都为零,控制块才释放。
#include <memory>
#include <iostream>
int main() {
auto sp = std::make_shared<int>(42);
std::weak_ptr<int> wp = sp; // 不增加强计数
std::cout << "use_count=" << sp.use_count() << "n"; // 输出1
if (auto locked = wp.lock()) { // 尝试提升
std::cout << "value=" << *locked << "n";
}
sp.reset(); // 强计数归零,对象销毁
if (wp.expired()) {
std::cout << "object gonen";
}
return 0;
}
上例中wp.lock()在对象存活时返回非空shared_ptr,用完即释放,不影响原生命周期;sp.reset()后wp.expired()立刻为真。这样我们既保留了“知道对方在哪”的能力,又避免把对方拴死。
从实现看,weak_ptr内部存的是控制块指针和原始指针的观测副本。它的大小通常和shared_ptr一样为两个机器字,不会带来额外堆分配。使用weak_ptr不会阻止对象析构,非常适合缓存、回边指针、调试追踪等非拥有关系。
三、用weak_ptr改写循环引用代码
回到第一节的A/B例子,只需把其中一侧改为weak_ptr即可打破环。通常规则是:从属方持有拥有方的弱引用,拥有方持有从属方的强引用。下面把B中的a_ptr改成weak_ptr:
#include <memory>
#include <iostream>
class B;
class A {
public:
std::shared_ptr<B> b_ptr;
~A() { std::cout << "A destroyedn"; }
};
class B {
public:
std::weak_ptr<A> a_ptr; // 改为弱引用
~B() { std::cout << "B destroyedn"; }
};
int main() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->b_ptr = b;
b->a_ptr = a;
// 离开作用域:a强计数1,b强计数1
// a先析构,b强计数变0,B销毁;B析构后a强计数变0,A销毁
return 0;
}
程序现在会正常打印A destroyed与B destroyed。因为B不直接拥有A,A析构时b里的weak_ptr自动失效,不会阻止A释放;A释放后b失去外部引用也释放,整个环自然解开。
实际工程中,如果双向关系频繁互相调用,建议在访问弱引用前都用lock()取临时shared_ptr,防止正在用的时候对方被别的线程释放。相比原始指针,weak_ptr提供了安全的存活判定,是C++多线程共享对象设计的常用手法。
四、常见误用与注意点
有人以为weak_ptr可以完全替代原始指针做所有非拥有引用,其实不然。weak_ptr每次访问需lock,有微小开销;且它只能从shared_ptr体系来,不能指向栈对象。若对象本就用unique_ptr管理,就没必要套weak_ptr。
#include <memory>
struct Node {
std::weak_ptr<Node> parent; // 正确:配合shared_ptr树使用
// std::weak_ptr<Node> sibling; // 若树用unique_ptr拥有子节点则不应如此写
};
void bad_example() {
int x = 10;
// std::weak_ptr<int> wp(&x); // 编译错误:不能从裸指针构造
}
另一个坑是长期持有lock()出来的shared_ptr。若函数在拿到locked后把它存进全局容器,就等于偷偷恢复了强引用,可能重新引入泄漏。weak_ptr的价值在于“临时、按需、可失效”,把它当永久句柄用就失去了设计初衷。
在调试阶段,可以打印use_count和weak_ptr.expired()来确认环是否真的断开。现代Sanitizer工具也能捕获shared_ptr泄漏,但理解原理才能写出根本不漏的代码。
五、总结
weak_ptr解决的是shared_ptr所有权模型里的固有缺陷:强引用环导致计数死锁。它用弱计数观测对象,不延长生命周期,通过lock方法提供受控的临时访问。在父子结构、观察者、缓存回指等场景,把其中一侧改为weak_ptr即可消除循环引用。掌握控制块中强弱计数的区别,才能在现代C++里既享受自动内存管理,又避开隐藏的资源泄漏。
weak_ptrshared_ptr循环引用修改时间:2026-08-07 19:42:34