【深入浅出C++】weak_ptr以及循环引用

std::weak_ptr 是 C++11 引入的智能指针,它设计的初衷就是为了配合 std::shared_ptr 工作,解决它最大的痛点——循环引用(死锁)

介绍weak_ptr

std::weak_ptr 是 C++11 中引入的一种智能指针,它与 std::shared_ptr 配合使用,能够解决 shared_ptr 引用计数所带来的问题,特别是循环引用问题。weak_ptr 允许你“观察”由 shared_ptr 管理的对象,但不会影响对象的引用计数。

weak_ptr 的特点

1、不增加引用计数:

  • 与 shared_ptr 不同,weak_ptr 不会增加目标对象的引用计数。它只是一个观察者,允许访问 shared_ptr 所管理的对象,但不会阻止对象被销毁。
  • 这意味着,weak_ptr 不会导致循环引用,也不会影响内存释放。

2、可以安全地转换为 shared_ptr:

  • weak_ptr 可以通过 lock() 方法转换为一个有效的 shared_ptr。如果该对象仍然存在(即引用计数不为 0),则 lock() 返回一个有效的 shared_ptr,可以使用该对象。如果对象已经被销毁,lock() 会返回一个空的 shared_ptr。

3、可以检查对象是否已经被销毁:

  • 通过 expired() 方法,可以检查目标对象是否已经被销毁。如果目标对象的引用计数为 0,则 expired() 会返回 true,表示该对象已被销毁。

为什么 weak_ptr 能解决循环引用问题?

在普通的 shared_ptr 使用过程中,如果对象之间存在相互引用,即两个对象通过 shared_ptr 相互引用,那么它们的引用计数会互相增加,从而造成引用计数永远不为 0的问题,这就是循环引用。

例如,假设有两个类 A 和 B,它们互相持有 shared_ptr,形成一个环:

  • A 中有一个 shared_ptr<B>,指向 B
  • B 中有一个 shared_ptr<A>,指向 A

这样,在程序结束时,A 和 B 会一直保持引用,不会被销毁,因为每个对象的引用计数至少为 1。导致它们的内存永远不会释放,形成内存泄漏

使用 weak_ptr 解决循环引用
通过将其中一个 shared_ptr 换成 weak_ptr,就可以打破这种循环引用关系。weak_ptr 不增加引用计数,它只是“观察”另一个对象,但不会阻止该对象的销毁。

  • weak_ptr 不增加引用计数:它不会让对象保持在内存中,避免了循环引用。
  • shared_ptr 负责销毁对象:当没有 shared_ptr 引用该对象时,引用计数为 0,对象会被销毁。
  • 可以使用 lock() 转换为 shared_ptr:当需要访问对象时,可以通过 lock() 方法将 weak_ptr 转换为 shared_ptr,并确保在访问时对象没有被销。

代码示例:

这是一个经典的父节点持有子节点,子节点指回父节点的场景(例如树或链表):
错误代码(使用 shared_ptr,导致内存泄漏)

#include <iostream>
#include <memory>

class Child; // 前向声明

class Parent {
public:
    std::shared_ptr<Child> child_ptr; // 指向孩子
    ~Parent() { std::cout << "Parent 析构" << std::endl; }
};

class Child {
public:
    std::shared_ptr<Parent> parent_ptr; // 指向父亲(循环引用关键点!)
    ~Child() { std::cout << "Child 析构" << std::endl; }
};

int main() {
    auto parent = std::make_shared<Parent>();
    auto child = std::make_shared<Child>();

    parent->child_ptr = child;   // Parent 的引用计数变为 1
    child->parent_ptr = parent;  // Child 的引用计数变为 1 (循环形成)

    // 函数结束,parent 和 child 离开作用域
    // 引用计数减为 1,不等于 0,永远不会析构 -> 内存泄漏!
    return 0;
}
// 没有任何析构函数被调用!

正确代码(使用 weak_ptr 打破循环)

#include <iostream>
#include <memory>

class Child;

class Parent {
public:
    std::shared_ptr<Child> child_ptr; // 父亲拥有孩子(强引用)
    ~Parent() { std::cout << "Parent 析构" << std::endl; }
};

class Child {
public:
    // ★ 关键:改用 weak_ptr,不增加 Parent 的引用计数
    std::weak_ptr<Parent> parent_ptr; 
    ~Child() { std::cout << "Child 析构" << std::endl; }
};

int main() {
    auto parent = std::make_shared<Parent>(); // Parent 引用计数 = 1
    auto child = std::make_shared<Child>();   // Child 引用计数 = 1

    parent->child_ptr = child;    // Child 引用计数变为 2
    child->parent_ptr = parent;   // Parent 引用计数不变,仍为 1(因为 weak_ptr)

    // 函数结束:
    // 1. parent 析构 -> Parent 引用计数 1 -> 0,Parent 内存释放
    // 2. Parent 析构函数执行,内部的 child_ptr 析构 -> Child 引用计数 2 -> 1
    // 3. child 析构 -> Child 引用计数 1 -> 0,Child 内存释放
    return 0;
}

运行结果:

Parent 析构
Child 析构

两个对象都正确析构了。

过程解析:

你可以把引用计数想象成门上的锁:

  1. 错误案例:父亲拿着孩子的钥匙(+1),孩子拿着父亲的钥匙(+1)。当你试图扔掉大门钥匙(外部 shared_ptr)时,因为孩子手里还有一把父亲的钥匙,门锁计数是 1,门永远打不开,谁也出不去。

  2. 正确案例:孩子不再拿父亲的钥匙,而是拿了一张门禁卡weak_ptr)。这张卡不影响门锁计数。当你扔掉大门钥匙时,门锁计数归零,门打开了(父亲析构)。父亲出去时,扔掉了他拿着的孩子钥匙,孩子那边门锁归零,孩子也出去了。

weak_ptr 解决循环引用的原理

其底层原理依赖于两个引用计数(在 shared_ptr 的控制块中):

  1. 强引用计数(Use Count):记录有多少个 shared_ptr 指向对象。当此数为 0 时,对象内存被销毁。

  2. 弱引用计数(Weak Count):记录有多少个 weak_ptr 指向对象。当此数为 0 时,控制块本身被销毁。

工作原理

  • weak_ptr 只增加弱引用计数,不增加强引用计数

  • 当对象析构时(强引用计数归零),weak_ptr 会得知对象已失效(expired() 返回 true),但它自己仍然存在(控制块还在),直到弱引用计数归零。

  • 这保证了 weak_ptr 可以安全地判断对象是否存在,如果存在,则通过 lock() 临时提升为 shared_ptr 来使用对象。

总结

要点内容
定位weak_ptr 是 shared_ptr 的“助手”,解决循环引用。
核心机制不增加强引用计数,打破引用环。
使用流程存在循环依赖时,将其中一方从 shared_ptr 改为 weak_ptr
访问方式不能直接访问,需要用 lock() 生成临时 shared_ptr 再访问(使用前检查空指针)。
典型场景观察者模式、父子双向关联、缓存(防止过期指针)、避免悬空指针。

一句话牢记weak_ptr 就像“借书证”,你有权查阅书籍(访问对象),但你不拥有这本书,图书馆(引用计数)不会因为你有借书证就不把书下架。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值