weak_ptr如何解决share_ptr的循环引用

循环引用问题

两个(或多个)对象互相持有对方的shared_ptr,这会导致它们的引用计数永远不会达到0,从而导致内存泄漏

AB类互相持有对方实例的std::shared_ptr而不是A持有Bstd::shared_ptrB持有Astd::weak_ptr,那么将会发生循环引用。这里是这种情况下可能的代码示例和结果解释:

#include <iostream>
#include <memory>

class B; // 前向声明

class A {
public:
    std::shared_ptr<B> b_ptr; // A持有B的一个shared_ptr
    ~A() { std::cout << "A destroyed\n"; }
};

class B {
public:
    std::shared_ptr<A> a_ptr; // B也持有A的一个shared_ptr
    ~B() { std::cout << "B destroyed\n"; }
};

int main() {
    std::shared_ptr<A> a = std::make_shared<A>();
    std::shared_ptr<B> b = std::make_shared<B>();
    
    a->b_ptr = b; // A通过shared_ptr持有B
    b->a_ptr = a; // B通过shared_ptr持有A

    return 0;
}

发生了什么?

  • main函数中,创建了两个std::shared_ptr对象ab,分别指向AB的新实例。这时,每个实例的引用计数为1。
  • a->b_ptr = b;执行时,B的实例的引用计数增加到2,因为现在ba->b_ptr都指向它。
  • b->a_ptr = a;执行时,A的实例的引用计数增加到2,因为现在ab->a_ptr都指向它。
  • main函数结束时,ab的作用域结束,它们的析构函数被调用,本应使引用计数减少。然而,由于A的实例持有Bstd::shared_ptr,而B的实例同时持有Astd::shared_ptr,这造成了循环引用。每个对象都至少有一个引用计数(A持有BB持有A),所以它们的析构函数不会被调用,导致资源泄漏。

结果

由于循环引用,两个对象都不会被销毁,其析构函数也不会被调用,因此控制台上不会显示“A destroyed”和“B destroyed”。这种情况导致内存泄漏,因为这些对象所占用的内存不会被释放。

weak_ptr为啥能做到不增加引用计数,又能引用shared的对象呢?

  1. 共享控制块std::weak_ptrstd::shared_ptr共享相同的控制块,该控制块包含了对象的强引用计数和弱引用计数。强引用计数跟踪有多少个std::shared_ptr指向对象,而弱引用计数跟踪有多少个std::weak_ptr指向同一个控制块。
  2. 不增加强引用计数:当std::weak_ptr被创建并指向一个std::shared_ptr管理的对象时,它只会增加控制块中的弱引用计数,而不影响强引用计数。这意味着std::weak_ptr的存在不会阻止其所指向的对象被销毁。
  3. 提升为std::shared_ptrstd::weak_ptr提供了lock方法,这个方法尝试将std::weak_ptr提升为std::shared_ptr。如果所指向的对象还存活(即强引用计数大于0),lock会成功返回一个有效的std::shared_ptr实例。如果对象已经被销毁(强引用计数为0),则返回一个空的std::shared_ptr实例。

保证引用的对象有效性

  • 在对象存活时访问:通过std::weak_ptrlock方法提升为std::shared_ptr来访问对象,可以保证在对象访问期间对象不会被销毁,因为提升成功意味着强引用计数会临时增加。这就提供了一种安全访问被std::shared_ptr管理的对象的方式,即使在多线程环境下。
  • 在对象不存活时安全失败:如果对象已经被销毁,lock方法会返回一个空的std::shared_ptr,这使得调用者可以通过检查返回的std::shared_ptr是否为空来安全地处理对象不存在的情况,避免了悬挂指针和无效访问。

weak_ptr的内部实现

std::weak_ptr是C++标准库中的一个智能指针,它设计用来解决std::shared_ptr间的循环引用问题。std::weak_ptr持有对一个由std::shared_ptr管理的对象的非拥有(weak)引用,这意味着std::weak_ptr的存在不会增加对象的引用计数。这就允许std::shared_ptr管理的对象在存在std::weak_ptr引用的情况下被销毁。

内部实现机制

std::weak_ptr的实现通常依赖于两个主要的控制结构:控制块(control block)和数据指针。控制块通常包含两个计数器——一个是强引用计数(用于std::shared_ptr),另一个是弱引用计数(用于std::weak_ptr)。

  1. 强引用计数:跟踪有多少个std::shared_ptr实例指向同一个对象。当强引用计数达到0时,对象会被销毁。
  2. 弱引用计数:跟踪有多少个std::weak_ptr实例指向同一个控制块(即间接指向对象)。当最后一个std::shared_ptr被销毁,对象本身会被释放,但控制块会保留在内存中,直到弱引用计数也降到0。这时,控制块才会被释放。

解决循环引用

在循环引用的场景中,两个或多个对象通过std::shared_ptr相互引用,导致它们的引用计数永远不会降到零,因此这些对象永远不会被销毁。如果将其中一个或多个引用改为std::weak_ptr,则这些std::weak_ptr不会增加对象的强引用计数。这意味着当其他所有的std::shared_ptr都被销毁时,即便还有std::weak_ptr存在,对象的强引用计数也会降到零,从而触发对象的销毁。因为std::weak_ptr不阻止其指向的对象被销毁,它允许循环中的对象被正确地回收,从而解决了循环引用问题。

使用std::weak_ptr

  • std::weak_ptr获得std::shared_ptr:可以通过调用std::weak_ptrlock方法来尝试获取一个std::shared_ptr。如果指向的对象还存在(即,至少还有一个std::shared_ptr指向该对象),lock会成功返回一个有效的std::shared_ptr;否则,它返回一个空的std::shared_ptr

通过这种方式,std::weak_ptr提供了一种安全的机制来观察和访问由std::shared_ptr管理的对象,同时避免了循环引用导致的内存泄漏问题。

让我们通过一个具体的例子来解释std::weak_ptr是如何工作的,以及它如何能够在不增加引用计数的情况下允许对象被正确销毁,从而解决循环引用的问题。

场景设定

假设有两个类AB,它们互相持有对方的指针。如果我们使用std::shared_ptr来管理这些指针,就会出现循环引用的问题,导致两个对象都无法被销毁。现在,我们将其中一个引用改为std::weak_ptr来解决这个问题。

#include <iostream>
#include <memory>

class B; // 前向声明

class A {
public:
    std::shared_ptr<B> b_ptr; // A持有B的一个shared_ptr
    ~A() { std::cout << "A destroyed\n"; }
};

class B {
public:
    std::weak_ptr<A> a_ptr; // B持有A的一个weak_ptr
    ~B() { std::cout << "B destroyed\n"; }
};

int main() {
    std::shared_ptr<A> a = std::make_shared<A>();
    std::shared_ptr<B> b = std::make_shared<B>();
    
    a->b_ptr = b; // A通过shared_ptr持有B
    b->a_ptr = a; // B通过weak_ptr持有A

    return 0;
}

初始状态

  1. 创建std::shared_ptr<A> a,指向A的一个新实例。此时,A实例的强引用计数为1。
  2. 创建std::shared_ptr<B> b,指向B的一个新实例。此时,B实例的强引用计数为1。

相互引用设置

  1. a->b_ptr = b; 执行此操作后,B实例的强引用计数增加到2,因为现在ba->b_ptr都指向它。
  2. b->a_ptr = a; 执行此操作时,由于b->a_ptr是一个std::weak_ptr,它指向A不增加A实例的强引用计数。因此,A强引用计数仍然是1。同时,A弱引用计数增加到1,因为有一个std::weak_ptrb->a_ptr)指向它。

销毁过程

  1. 当main函数的作用域结束,a和b会被销毁。
    • 首先,std::shared_ptr<A> a被销毁,这导致A实例的强引用计数减少到0。因为没有std::shared_ptr指向A了,A实例被销毁。此时,如果尝试通过b->a_ptr.lock()访问A实例,将得到一个空的std::shared_ptr<A>,因为A已经不存在了。
    • 然后,std::shared_ptr<B> b被销毁,这导致B实例的强引用计数减少到1(记住,a->b_ptrA的析构过程中已经被销毁)。当b的作用域结束,B实例的强引用计数最终减少到0,导致B实例被销毁。

在整个过程中,std::weak_ptr允许B实例持有对A实例的非拥有性引用,而不影响A实例的生命周期。这就避免了循环引用问题,使得两个对象可以被适时且正确地销毁。

结论

通过上述示例,我们可以看到,即便B的实例持有A的一个引用(通过std::weak_ptr),A的实例仍然能够在引用计数降至0时被正确销毁。std::weak_ptr允许B引用A而不增加A的引用计数,从而避免了循环引用问题,使得AB的实例可以在它们不再被需要时自动且正确地销毁。这展示了std::weak_ptr如何帮助管理复杂的对象关系,同时避免内存泄漏。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值