当前位置: 首页 > backend >正文

面试题:循环引用两个节点相互引用,如何判断哪个用 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。

http://www.xdnf.cn/news/746.html

相关文章:

  • ThreadLocal - 原理与应用场景详解
  • 蓝桥杯 二进制问题 刷题笔记
  • 一个旅行攻略需要调用多少个MCP的服务?
  • 松灵Cobot Magic双臂具身遥操机器人(基于ROS的定位建图与协同导航技术)
  • 网工_DHCP协议
  • AI与思维模型【67】——元认知
  • 使用docker任意系统编译opengauss
  • Vue.js 入门教程
  • Spring 微服务解决了单体架构的哪些痛点?
  • 分布式入门
  • 七段码 路径压缩 并查集 dfs
  • 思维题专题
  • K8s-Pod详解
  • 第一讲 生成式ai是什么
  • 头歌java课程实验(函数式接口及lambda表达式)
  • 【AI论文】CLIMB:基于聚类的迭代数据混合自举语言模型预训练
  • 2026《数据结构》考研复习笔记四(第一章)
  • 单例模式与消费者生产者模型,以及线程池的基本认识与模拟实现
  • Java学习手册:Filter 和 Listener
  • synchronized 与分布式锁
  • 约束:常见约束(常见约束-例子,外键约束)
  • Laravel-vite+vue开发前端模板
  • 最新扣子空间实操指南
  • QML--全局对象Qt
  • 1.Vue自动化工具安装(Vue-cli)
  • 自定义请求头导致跨域的解决办法
  • C++学习:六个月从基础到就业——内存管理:RAII原则
  • 键入网址到网页显示,期间发生了什么?
  • Arduino示例代码讲解:Project 08 - Digital Hourglass 数字沙漏
  • DAY 50 leetcode 1047--栈和队列.删除字符串中的所有相邻重复项