图解C++智能指针的循环引用

循环引用是学习智能指针过程中的一个小难点,笔者愚钝,明明知道是两个指针互相引用导致了内存泄漏,但看各种文字资料时,脑子里总是一团浆糊,感觉似懂非懂,于是自己绘制了几张图片,思路和概念才清晰起来,希望这篇博客能帮助有同样困惑的同学解惑。

shared_ptr 的实现

要明白循环引用,我们首先需要知道shared_ptr是如何实现的,它内部包含两个指针,一个指向我们要管理的资源对象,另一个指向则指向这个对象所对应的控制块。在这个控制块中,包含了这个对象的强引用计数,当我们第一次声明一个shared_ptr变量时,就会为这个资源对象分配一个控制块,并将强引用计数初始化为1。
当有新的shared_ptr对象拥有这个资源的时候,强引用计数就会+1,当shared_ptr对象销毁或者调用.reset()方法时,强引用计数就会-1,当强引用计数为0时,这个资源就会析构。这样我们就通过RAII来避免了内存的泄露。

循环引用

这看起来是一个很完美的设计,但是shared_ptr管理的资源和shared_ptr对象本身是两块内存,并且shared_ptr管理的资源本身也可以包含一个shared_ptr对象并持有资源,这样就会导致,一旦两个资源本身互相持有,那么他们析构的条件就是对方析构,这就会导致内存的泄露。

这么说可能有些模糊,我们直接来看例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class A {
public:
std::shared_ptr<A> ptr;
...
}
int main() {
// code I
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<A> b = std::make_shared<A>();
// code II
a->ptr = b;
b->ptr = a;
return 0;
}

code I 运行完毕后,我们会在堆中创建两个A类的对象,我们将其称之为OBJ_AOBJ_B,同时栈上会有两个std::shared_ptr<A>对象ab,他们分别指向OBJ_AOBJ_B以及他们各自的控制块CB_ACB_B,如下图所示

此时两个控制块的引用计数都是1,当我们运行Code II后,OBJ_AOBJ_B会拥有对对方的所有权,他们的引用计数都变为2

这个时候代码运行完毕,ab作为栈上的对象被销毁,OBJ_AOBJ_B的引用计数都下降为1,但是由于此时他们都持有指向彼此的shared_ptr对象,引用计数仍为1,不会继续下降,这块内存就不会被释放,造成了内存泄露。

weak_ptr的引入

为了解决这个问题,我们引入了weak_ptr,它不拥有资源的所有权,指向资源的时候只会增加弱引用计数而非强引用计数,弱引用计数不决定资源的生命周期,只决定控制块的生命周期,当弱引用计数为0时销毁控制块。
需要注意的是,shared_ptr也会让弱引用计数+1。

1
2
3
4
5
6
7
8
9
10
11
12
class A {
public:
std::weak_ptr<A> ptr;
...
}
int main() {
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<A> b = std::make_shared<A>();
a->ptr = b;
b->ptr = a;
return 0;
}

当我们使用上面的代码时,几个对象的声明周期如下图所示:
使用`weak_ptr`时的资源生命周期
ba离开作用域后

  1. b析构,CB_B的强引用计数-1为0,弱引用计数-1=1

  2. OBJ_B析构,CB_A的弱引用计数-1=1

  3. a析构,CB_A的强引用计数-1为0,弱引用计数-1=0

  4. OBJ_ACB_A析构,CB_B的弱引用计数-1=0

  5. CB_B析构


图解C++智能指针的循环引用
https://guts.homes/2025/10/07/cpp-Circular-Reference/
作者
guts
发布于
2025年10月7日
许可协议