深入理解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; 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; std::cout << "Bob use count: " << bob.use_count () << std::endl; 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;
二、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; 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; 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; } 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; std::cout << "Bob use count: " << bob.use_count () << std::endl; alice->introduce (); bob->introduce (); 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) ; auto it = cache.find (id); if (it != cache.end ()) { 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 使用工具检测循环引用
Valgrind :
1 valgrind --leak-check=full ./your_program
AddressSanitizer (更推荐):
1 2 g++ -fsanitize=address -g your_program.cpp ./a.out
自定义调试工具 :
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> 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 代码审查要点 在审查代码时,注意以下可能产生循环引用的模式:
双向关联 :两个类互相持有对方的shared_ptr
树形结构中的父节点引用 :子节点持有父节点的shared_ptr
观察者模式 :观察者持有主题的shared_ptr,反之亦然
缓存系统 :缓存持有对象的shared_ptr,对象又引用缓存
四、最佳实践总结
默认使用unique_ptr :表达独占所有权,避免不必要的引用计数开销
谨慎使用shared_ptr :只在真正需要共享所有权时使用
使用weak_ptr打破循环 :对于可能形成循环引用的关联关系
优先使用make_shared和make_unique :提高性能和异常安全性
明确所有权关系 :在设计阶段就理清对象间的所有权关系
定期进行代码审查 :特别关注shared_ptr的使用模式
使用工具进行检测 :集成内存检测工具到开发流程中
五、现实世界中的循环引用案例 5.1 GUI框架中的循环引用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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; };class Button {public : std::weak_ptr<Window> parent_window; 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; };class GameEntity {public : std::weak_ptr<GameEntity> target; std::vector<std::shared_ptr<GameEntity>> children; };
结语 循环引用是shared_ptr使用中最常见的陷阱,但通过理解其原理和正确使用weak_ptr,我们可以有效地避免这个问题。关键在于在设计阶段就明确对象间的所有权关系,并在代码审查时特别关注shared_ptr的使用模式。
记住:不是所有关联都需要所有权 。当对象间的关系是”使用”而非”拥有”时,weak_ptr是最佳选择。