一、智能指针循环引用问题的处理

1.1 循环引用的产生与危害

循环引用是shared_ptr使用过程中最常见的问题之一,当两个或多个shared_ptr形成引用闭环时就会产生循环引用。这种情况下,每个shared_ptr的引用计数都无法降到 0,导致其所管理的对象无法被释放,最终造成内存泄漏。

典型的循环引用场景:

  • 双向链表节点相互引用

  • 父对象持有子对象的shared_ptr,子对象同时持有父对象的shared_ptr

  • 观察者模式中,观察者与被观察者相互持有shared_ptr

1.2 循环引用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <memory>
#include <iostream>

class B; // 前向声明

class A {
public:
std::shared_ptr<B> b_ptr;
A() { std::cout << "A constructed\n"; }
~A() { std::cout << "A destructed\n"; } // 不会被调用
};

class B {
public:
std::shared_ptr<A> a_ptr;
B() { std::cout << "B constructed\n"; }
~B() { std::cout << "B destructed\n"; } // 不会被调用
};

int main() {
{
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();

a->b_ptr = b; // a持有b的shared_ptr
b->a_ptr = a; // b持有a的shared_ptr,形成循环引用
}
// 离开作用域后,A和B的析构函数都不会被调用,造成内存泄漏
std::cout << "Exiting main scope\n";
return 0;
}

在这个示例中,a和b形成了循环引用,当它们离开作用域时,各自的引用计数都是 1(互相引用),因此不会调用析构函数,导致内存泄漏。

1.3 解决循环引用的方案

解决循环引用的核心是打破引用闭环,最常用的方法是将循环中的一个shared_ptr替换为weak_ptr。

1.3.1 使用 weak_ptr 打破循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <memory>
#include <iostream>

class B; // 前向声明

class A {
public:
std::shared_ptr<B> b_ptr;
A() { std::cout << "A constructed\n"; }
~A() { std::cout << "A destructed\n"; } // 会被调用
};

class B {
public:
std::weak_ptr<A> a_ptr; // 使用weak_ptr替代shared_ptr
B() { std::cout << "B constructed\n"; }
~B() { std::cout << "B destructed\n"; } // 会被调用
};

int main() {
{
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();

a->b_ptr = b; // a持有b的shared_ptr
b->a_ptr = a; // b持有a的weak_ptr,打破循环引用
}
// 离开作用域后,A和B的析构函数都会被正确调用
std::cout << "Exiting main scope\n";
return 0;
}

在这个修改后的示例中,B类中使用weak_ptr来引用A,这样就打破了循环引用。当a和b离开作用域时:

  1. a的引用计数先减为 0,调用A的析构函数
  2. A的析构函数释放b_ptr,使b的引用计数减为 0
  3. 调用B的析构函数,完成所有资源的释放

1.3.2 weak_ptr 的正确使用方式

当需要通过weak_ptr访问对象时,应使用lock()方法获取shared_ptr:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class B {
public:
std::weak_ptr<A> a_ptr;

void doSomethingWithA() {
// 尝试获取shared_ptr
if (auto a = a_ptr.lock()) {
// 成功获取,现在可以安全使用a
std::cout << "Successfully accessed A from B\n";
} else {
// 获取失败,A对象已被销毁
std::cout << "A has been destroyed\n";
}
}
};

1.4 循环引用的预防策略

  • 明确所有权关系:在设计阶段明确对象间的所有权关系,区分 "所有者" 和 "观察者"
  • 优先使用 unique_ptr:在不需要共享所有权的情况下,优先使用unique_ptr,从根源上减少循环引用的可能
  • 合理使用 weak_ptr:在需要观察对象但不拥有所有权的场景下,使用weak_ptr
  • 定期代码审查:重点检查双向引用关系,确保没有形成shared_ptr的循环
  • 使用静态分析工具:利用 Clang、GCC 等编译器的静态分析功能,检测潜在的循环引用

1.5 复杂场景的循环引用处理

在更复杂的场景(如多节点循环)中,需要识别出循环中的一个或多个适当节点,将其引用改为weak_ptr:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 三节点循环的解决方案
class C;

class A {
public:
std::shared_ptr<B> b_ptr;
};

class B {
public:
std::shared_ptr<C> c_ptr;
};

class C {
public:
std::weak_ptr<A> a_ptr; // 使用weak_ptr打破三节点循环
};

通过这种方式,无论循环包含多少节点,只要打破其中一个引用,就能解决整个循环的内存泄漏问题。