深入理解C++智能指针:循环引用详解与weak_ptr救赎

深入理解C++智能指针:循环引用详解与weak_ptr救赎

一、循环引用:智能指针的”致命拥抱”

循环引用是std::shared_ptr最常见且最棘手的问题,它会导致内存无法被释放,造成内存泄漏。让我们通过一个经典的例子来深入理解这个问题。

1.1 循环引用示例

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
32
33
34
35
36
#include <iostream>
#include <memory>

class Person {
public:
std::string name;
std::shared_ptr<Person> partner; // 指向伴侣的shared_ptr

Person(const std::string& n) : name(n) {
std::cout << name << " created\n";
}

~Person() {
std::cout << name << " destroyed\n";
}

void setPartner(const std::shared_ptr<Person>& p) {
partner = p;
}
};

int main() {
// 创建两个人
auto alice = std::make_shared<Person>("Alice");
auto bob = std::make_shared<Person>("Bob");

// 建立伴侣关系 - 这里埋下了循环引用的种子
alice->setPartner(bob);
bob->setPartner(alice);

std::cout << "Alice use count: " << alice.use_count() << std::endl; // 2
std::cout << "Bob use count: " << bob.use_count() << std::endl; // 2

// 退出作用域,但对象不会被销毁!
return 0;
}

运行这段代码,你会发现析构函数没有被调用,说明发生了内存泄漏。

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
创建阶段:
alice ──────────────> Person("Alice")对象
(use_count=1) │

bob ─────────────────> Person("Bob")对象
(use_count=1)

建立关系后:
alice ──────────────> Person("Alice")对象 <───────────── bob.partner
(use_count=2) │ (来自bob内部的shared_ptr)

└─────────────> Person("Bob")对象 <───────────── bob
(use_count=2) │

alice.partner ───┘
(来自alice内部的shared_ptr)

退出作用域时:
main函数中的alice和bob被销毁,但...
Person("Alice")对象的use_count从2减为1(因为bob.partner仍然引用它)
Person("Bob")对象的use_count从2减为1(因为alice.partner仍然引用它)

结果: 两个对象都无法被释放,形成内存泄漏

1.3 更复杂的循环引用场景

循环引用不一定总是这么明显,可能隐藏在复杂的对象关系中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Project {
public:
std::vector<std::shared_ptr<Employee>> employees;
};

class Employee {
public:
std::shared_ptr<Project> current_project;
std::shared_ptr<Employee> manager; // 可能指向自己或其他员工
};

// 可能创建复杂的循环引用链
auto project = std::make_shared<Project>();
auto manager = std::make_shared<Employee>();
auto employee = std::make_shared<Employee>();

project->employees.push_back(manager);
project->employees.push_back(employee);

manager->current_project = project;
employee->current_project = project;
employee->manager = manager; // 这里开始形成循环

// 更复杂的情况:manager->manager = employee; 形成双向循环

二、std::weak_ptr:打破循环引用的利器

2.1 weak_ptr的基本特性

std::weak_ptr是一种不控制对象生命周期的智能指针,它指向一个由std::shared_ptr管理的对象,但不会增加引用计数。

1
2
3
4
5
6
7
8
9
10
11
12
13
auto shared = std::make_shared<MyClass>(42);
std::weak_ptr<MyClass> weak = shared; // 不会增加引用计数

std::cout << "Use count: " << shared.use_count() << std::endl; // 1

// 要使用weak_ptr指向的对象,必须先转换为shared_ptr
if (auto temp = weak.lock()) {
// 对象还存在,可以安全使用
temp->doSomething();
} else {
// 对象已被释放
std::cout << "Object no longer exists\n";
}

2.2 使用weak_ptr解决循环引用

让我们用weak_ptr修复之前的Person类:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class Person {
public:
std::string name;
std::weak_ptr<Person> partner; // 使用weak_ptr代替shared_ptr

Person(const std::string& n) : name(n) {
std::cout << name << " created\n";
}

~Person() {
std::cout << name << " destroyed\n";
}

void setPartner(const std::shared_ptr<Person>& p) {
partner = p; // weak_ptr可以从shared_ptr构造
}

// 获取partner的shared_ptr(如果还存在)
std::shared_ptr<Person> getPartner() const {
return partner.lock();
}

void introduce() const {
if (auto p = partner.lock()) {
std::cout << name << "'s partner is " << p->name << std::endl;
} else {
std::cout << name << " has no partner or partner was destroyed\n";
}
}
};

int main() {
auto alice = std::make_shared<Person>("Alice");
auto bob = std::make_shared<Person>("Bob");

alice->setPartner(bob);
bob->setPartner(alice);

std::cout << "Alice use count: " << alice.use_count() << std::endl; // 1
std::cout << "Bob use count: " << bob.use_count() << std::endl; // 1

alice->introduce(); // Alice's partner is Bob
bob->introduce(); // Bob's partner is Alice

// 退出作用域时,对象会被正确销毁
return 0;
}

2.3 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
创建阶段(与之前相同):
alice ──────────────> Person("Alice")对象
(use_count=1) │

bob ─────────────────> Person("Bob")对象
(use_count=1)

建立关系后(使用weak_ptr):
alice ──────────────> Person("Alice")对象
(use_count=1) │
.partner (weak_ptr)
│ │
│ └──> 指向Person("Bob")对象(但不增加引用计数)

bob ─────────────────> Person("Bob")对象
(use_count=1) │
.partner (weak_ptr)
│ │
│ └──> 指向Person("Alice")对象(但不增加引用计数)

退出作用域时:
main函数中的alice和bob被销毁
Person("Alice")对象的use_count从1减为0 → 对象被销毁
Person("Bob")对象的use_count从1减为0 → 对象被销毁

weak_ptr会自动感知到对象已被销毁,lock()返回nullptr

2.4 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
class ExpensiveObject {
// 创建成本很高的对象
};

class ObjectCache {
private:
std::unordered_map<int, std::weak_ptr<ExpensiveObject>> cache;
std::mutex cache_mutex;

public:
std::shared_ptr<ExpensiveObject> getObject(int id) {
std::lock_guard<std::mutex> lock(cache_mutex);

// 检查缓存中是否已有对象的weak_ptr
auto it = cache.find(id);
if (it != cache.end()) {
// 尝试从weak_ptr获取shared_ptr
if (auto cached = it->second.lock()) {
return cached; // 对象还在使用中,直接返回
}
// 对象已被释放,从缓存中移除
cache.erase(it);
}

// 创建新对象并加入缓存
auto new_obj = std::make_shared<ExpensiveObject>(id);
cache[id] = new_obj;
return new_obj;
}
};

观察者模式

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
32
33
34
class Subject;

class Observer : public std::enable_shared_from_this<Observer> {
public:
virtual void update(const Subject& subject) = 0;
};

class Subject {
private:
std::vector<std::weak_ptr<Observer>> observers;

public:
void addObserver(const std::shared_ptr<Observer>& observer) {
observers.push_back(observer);
}

void notifyObservers() {
// 移除已失效的观察者
observers.erase(
std::remove_if(observers.begin(), observers.end(),
[](const std::weak_ptr<Observer>& weak_observer) {
return weak_observer.expired();
}),
observers.end()
);

// 通知有效的观察者
for (const auto& weak_observer : observers) {
if (auto observer = weak_observer.lock()) {
observer->update(*this);
}
}
}
};

三、检测和调试循环引用

3.1 使用工具检测循环引用

  1. Valgrind:

    1
    valgrind --leak-check=full ./your_program
  2. AddressSanitizer (更推荐):

    1
    2
    g++ -fsanitize=address -g your_program.cpp
    ./a.out
  3. 自定义调试工具:

    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
    #include <iostream>
    #include <memory>

    // 调试版本的shared_ptr,会输出引用计数变化
    template<typename T>
    class DebugSharedPtr : public std::shared_ptr<T> {
    public:
    template<typename... Args>
    DebugSharedPtr(Args&&... args)
    : std::shared_ptr<T>(std::forward<Args>(args)...) {
    std::cout << "DebugSharedPtr created, use_count: " << this->use_count() << std::endl;
    }

    ~DebugSharedPtr() {
    std::cout << "DebugSharedPtr destroyed, use_count: " << this->use_count() << std::endl;
    }

    DebugSharedPtr(const DebugSharedPtr& other)
    : std::shared_ptr<T>(other) {
    std::cout << "DebugSharedPtr copied, use_count: " << this->use_count() << std::endl;
    }

    DebugSharedPtr& operator=(const DebugSharedPtr& other) {
    std::cout << "DebugSharedPtr assigned, old use_count: " << this->use_count();
    std::shared_ptr<T>::operator=(other);
    std::cout << ", new use_count: " << this->use_count() << std::endl;
    return *this;
    }
    };

3.2 代码审查要点

在审查代码时,注意以下可能产生循环引用的模式:

  1. 双向关联:两个类互相持有对方的shared_ptr
  2. 树形结构中的父节点引用:子节点持有父节点的shared_ptr
  3. 观察者模式:观察者持有主题的shared_ptr,反之亦然
  4. 缓存系统:缓存持有对象的shared_ptr,对象又引用缓存

四、最佳实践总结

  1. 默认使用unique_ptr:表达独占所有权,避免不必要的引用计数开销
  2. 谨慎使用shared_ptr:只在真正需要共享所有权时使用
  3. 使用weak_ptr打破循环:对于可能形成循环引用的关联关系
  4. 优先使用make_sharedmake_unique:提高性能和异常安全性
  5. 明确所有权关系:在设计阶段就理清对象间的所有权关系
  6. 定期进行代码审查:特别关注shared_ptr的使用模式
  7. 使用工具进行检测:集成内存检测工具到开发流程中

五、现实世界中的循环引用案例

5.1 GUI框架中的循环引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 常见的GUI框架中的循环引用问题
class Window {
public:
std::shared_ptr<Button> ok_button;
std::shared_ptr<Button> cancel_button;
};

class Button {
public:
std::shared_ptr<Window> parent_window; // 这里可能造成循环引用
std::function<void()> onClick;
};

// 解决方案:使用weak_ptr
class Button {
public:
std::weak_ptr<Window> parent_window; // 使用weak_ptr
std::function<void()> onClick;
};

5.2 游戏开发中的循环引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 游戏对象间的循环引用
class GameEntity {
public:
std::shared_ptr<GameEntity> target;
std::vector<std::shared_ptr<GameEntity>> children;
};

// 可能形成的复杂引用网络:
// EntityA → target → EntityB
// EntityB → children → EntityA

// 解决方案:分析所有权关系,适当使用weak_ptr
class GameEntity {
public:
std::weak_ptr<GameEntity> target; // 目标可能是临时性的
std::vector<std::shared_ptr<GameEntity>> children; // 但children通常是所有权关系
};

结语

循环引用是shared_ptr使用中最常见的陷阱,但通过理解其原理和正确使用weak_ptr,我们可以有效地避免这个问题。关键在于在设计阶段就明确对象间的所有权关系,并在代码审查时特别关注shared_ptr的使用模式。

记住:不是所有关联都需要所有权。当对象间的关系是”使用”而非”拥有”时,weak_ptr是最佳选择。


深入理解C++智能指针:循环引用详解与weak_ptr救赎
https://www.psnow.sbs/2025/09/16/深入理解C-智能指针:循环引用详解与weak-ptr救赎/
作者
Psnow
发布于
2025年9月16日
许可协议