文章目录
循环引用问题
两个(或多个)对象互相持有对方的
shared_ptr
,这会导致它们的引用计数永远不会达到0,从而导致内存泄漏
A
和B
类互相持有对方实例的std::shared_ptr
而不是A
持有B
的std::shared_ptr
和B
持有A
的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::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
对象a
和b
,分别指向A
和B
的新实例。这时,每个实例的引用计数为1。 - 当
a->b_ptr = b;
执行时,B
的实例的引用计数增加到2,因为现在b
和a->b_ptr
都指向它。 - 当
b->a_ptr = a;
执行时,A
的实例的引用计数增加到2,因为现在a
和b->a_ptr
都指向它。 - 当
main
函数结束时,a
和b
的作用域结束,它们的析构函数被调用,本应使引用计数减少。然而,由于A
的实例持有B
的std::shared_ptr
,而B
的实例同时持有A
的std::shared_ptr
,这造成了循环引用。每个对象都至少有一个引用计数(A
持有B
,B
持有A
),所以它们的析构函数不会被调用,导致资源泄漏。
结果
由于循环引用,两个对象都不会被销毁,其析构函数也不会被调用,因此控制台上不会显示“A destroyed
”和“B destroyed
”。这种情况导致内存泄漏,因为这些对象所占用的内存不会被释放。
weak_ptr为啥能做到不增加引用计数,又能引用shared的对象呢?
- 共享控制块:
std::weak_ptr
和std::shared_ptr
共享相同的控制块,该控制块包含了对象的强引用计数和弱引用计数。强引用计数跟踪有多少个std::shared_ptr
指向对象,而弱引用计数跟踪有多少个std::weak_ptr
指向同一个控制块。 - 不增加强引用计数:当
std::weak_ptr
被创建并指向一个std::shared_ptr
管理的对象时,它只会增加控制块中的弱引用计数,而不影响强引用计数。这意味着std::weak_ptr
的存在不会阻止其所指向的对象被销毁。 - 提升为
std::shared_ptr
:std::weak_ptr
提供了lock
方法,这个方法尝试将std::weak_ptr
提升为std::shared_ptr
。如果所指向的对象还存活(即强引用计数大于0),lock
会成功返回一个有效的std::shared_ptr
实例。如果对象已经被销毁(强引用计数为0),则返回一个空的std::shared_ptr
实例。
保证引用的对象有效性
- 在对象存活时访问:通过
std::weak_ptr
的lock
方法提升为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
)。
- 强引用计数:跟踪有多少个
std::shared_ptr
实例指向同一个对象。当强引用计数达到0时,对象会被销毁。 - 弱引用计数:跟踪有多少个
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_ptr
的lock
方法来尝试获取一个std::shared_ptr
。如果指向的对象还存在(即,至少还有一个std::shared_ptr
指向该对象),lock
会成功返回一个有效的std::shared_ptr
;否则,它返回一个空的std::shared_ptr
。
通过这种方式,std::weak_ptr
提供了一种安全的机制来观察和访问由std::shared_ptr
管理的对象,同时避免了循环引用导致的内存泄漏问题。
让我们通过一个具体的例子来解释std::weak_ptr
是如何工作的,以及它如何能够在不增加引用计数的情况下允许对象被正确销毁,从而解决循环引用的问题。
场景设定
假设有两个类A
和B
,它们互相持有对方的指针。如果我们使用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;
}
初始状态
- 创建
std::shared_ptr<A> a
,指向A
的一个新实例。此时,A
实例的强引用计数为1。 - 创建
std::shared_ptr<B> b
,指向B
的一个新实例。此时,B
实例的强引用计数为1。
相互引用设置
a->b_ptr = b;
执行此操作后,B
实例的强引用计数增加到2,因为现在b
和a->b_ptr
都指向它。b->a_ptr = a;
执行此操作时,由于b->a_ptr
是一个std::weak_ptr
,它指向A
但不增加A
实例的强引用计数。因此,A
的强引用计数仍然是1。同时,A
的弱引用计数增加到1,因为有一个std::weak_ptr
(b->a_ptr
)指向它。
销毁过程
- 当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_ptr
在A
的析构过程中已经被销毁)。当b
的作用域结束,B
实例的强引用计数最终减少到0,导致B
实例被销毁。
- 首先,
在整个过程中,std::weak_ptr
允许B
实例持有对A
实例的非拥有性引用,而不影响A
实例的生命周期。这就避免了循环引用问题,使得两个对象可以被适时且正确地销毁。
结论
通过上述示例,我们可以看到,即便B
的实例持有A
的一个引用(通过std::weak_ptr
),A
的实例仍然能够在引用计数降至0时被正确销毁。std::weak_ptr
允许B
引用A
而不增加A
的引用计数,从而避免了循环引用问题,使得A
和B
的实例可以在它们不再被需要时自动且正确地销毁。这展示了std::weak_ptr
如何帮助管理复杂的对象关系,同时避免内存泄漏。