面试题:循环引用两个节点相互引用,如何判断哪个用 shared_ptr?哪个用 weak_ptr?
目录
1.引言
2.原理
3.所有权模型与指针选择
4.复杂场景的决策策略
5.注意事项
6.总结
1.引言
当两个对象通过 shared_ptr 相互引用时,会产生循环引用问题,导致内存泄漏。因为这两个对象的引用计数永远不会变为 0,即使它们在程序的其他部分已经不被使用了。
典型循环引用:
#include <memory>
#include <iostream>
using namespace std;classB; // 前置声明classA {
public:shared_ptr<B> b_ptr;~A() { cout << "A destroyed" << endl; }
};classB {
public:shared_ptr<A> a_ptr;~B() { cout << "B destroyed" << endl; }
};voidtest(){shared_ptr<A> a = make_shared<A>();shared_ptr<B> b = make_shared<B>();a->b_ptr = b;b->a_ptr = a;
}
// test结束时,a和b的引用计数均为1,对象未销毁
解决方案是打破这个循环,通常是让其中一个对象使用 weak_ptr 指向另一个对象,而另一个对象使用 shared_ptr。这里决定使用 shared_ptr 还是 weak_ptr 的关键在于所有权关系的分析。
2.原理
std::shared_ptr
会对所管理的对象进行引用计数,每有一个std::shared_ptr
指向该对象,引用计数就会加 1;当引用计数降为 0 时,对象会被自动销毁,影响引用次数的场景包括:构造、赋值、析构。std::weak_ptr
是一种弱引用,它不会增加所管理对象的引用计数,主要用于观察std::shared_ptr
所管理的对象,避免循环引用。
3.所有权模型与指针选择
一般来说,应该在拥有对象所有权的地方使用 std::shared_ptr
,在只需要观察对象、不拥有对象所有权的地方使用 std::weak_ptr
。以下通过几个具体场景进行说明:
场景一:父节点和子节点的关系
#include <iostream>
#include <memory>class Child;class Parent {
public:std::shared_ptr<Child> child;~Parent() {std::cout << "Parent destroyed" << std::endl;}
};class Child {
public:std::weak_ptr<Parent> parent;~Child() {std::cout << "Child destroyed" << std::endl;}
};int main() {auto parent = std::make_shared<Parent>();parent->child = std::make_shared<Child>();parent->child->parent = parent;return 0;
}
在上述代码里,Parent
类使用 std::shared_ptr
拥有 Child
对象的所有权,而 Child
类使用 std::weak_ptr
观察 Parent
对象,这样就避免了循环引用。
场景二:观察者模式
在观察者模式中,主题对象通常拥有观察者对象的所有权,而观察者对象只是对主题对象进行观察。所以,主题对象使用 std::shared_ptr
指向观察者对象,观察者对象使用 std::weak_ptr
指向主题对象。
#include <iostream>
#include <memory>
#include <vector>class Observer;class Subject {
public:std::vector<std::shared_ptr<Observer>> observers;~Subject() {std::cout << "Subject destroyed" << std::endl;}
};class Observer {
public:std::weak_ptr<Subject> subject;~Observer() {std::cout << "Observer destroyed" << std::endl;}
};int main() {auto subject = std::make_shared<Subject>();auto observer = std::make_shared<Observer>();subject->observers.push_back(observer);observer->subject = subject;return 0;
}
在这个例子中,Subject
类使用 std::shared_ptr
拥有 Observer
对象的所有权,而 Observer
类使用 std::weak_ptr
观察 Subject
对象,防止了循环引用的产生。
4.复杂场景的决策策略
场景一:多所有者场景
若多个对象共享某资源,需由顶层管理者持有shared_ptr,其余使用weak_ptr。
示例:缓存系统设计
#include <unordered_map>class CacheManager;class Resource {
public:weak_ptr<CacheManager> manager; // 弱引用管理器
};class CacheManager : public enable_shared_from_this<CacheManager> {
public:void addResource(int id){resources[id] = make_shared<Resource>();resources[id]->manager = shared_from_this(); // 关键行}
};
说明:CacheManager拥有所有Resource,Resource通过weak_ptr反向引用管理器。
场景二:无明确所有权场景
若对象间无明确从属关系,需重新审视设计或使用双向weak_ptr。
示例:聊天室和用户
#include <memory>
#include <vector>
#include <iostream>
usingnamespace std;class ChatRoom;class User {
public:string name;vector<weak_ptr<ChatRoom>> rooms; // 弱引用聊天室
};class ChatRoom {
public:string name;vector<weak_ptr<User>> users; // 弱引用用户
};intmain(){auto alice = make_shared<User>("Alice");auto general = make_shared<ChatRoom>("General");return 0;
}
说明:用户(User) 可以加入多个聊天室,聊天室(ChatRoom) 包含多个用户,但是他们互相并没有所有权关系,所以使用双向weak_ptr,用户和聊天室的生命周期由外部系统管理!
5.注意事项
1)weak_ptr 的安全访问
使用weak_ptr时需通过lock()获取shared_ptr,并检查有效性。
void accessParent(shared_ptr<Child> child) {if (auto parent = child->parent.lock()) {cout << "Parent is alive: " << parent << endl;} else {cout << "Parent has been destroyed" << endl;}
}
2)循环引用的检测工具
Valgrind、AddressSanitizer 等工具可辅助检测内存泄漏。
静态代码分析器(如 Clang-Tidy)可识别潜在循环引用。
6.总结
核心准则:通过分析对象生命周期控制权,确定shared_ptr和weak_ptr的使用。始终确保至少有一条所有权路径不形成闭环。
谁管理生命周期,谁用 shared_ptr。
谁仅需引用对方,谁用 weak_ptr。