面试题:循环引用两个节点相互引用,如何判断哪个用 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.mzph.cn/web/76523.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

QT聊天项目DAY06

1.从git上同步项目 编译测试&#xff0c;编译通过 Post请求测试 测试成功 2. email is 打印有问题&#xff0c;检查 解析结果是存储在jsonResult中的&#xff0c;修改 3. 客户端实现Post验证码请求 3.1 同步Qt客户端项目 检查QT版本&#xff0c;由于我在公司用的还是QT5.12.9…

PHP腾讯云人脸核身获取FaceId

参考腾讯云官方文档&#xff1a; 人脸核身 合作方后台上传身份信息_腾讯云 前提&#xff1a;已经获取了SIGN Ticket。获取参考文档&#xff1a; PHP腾讯云人脸核身获取SIGN Ticket-CSDN博客 public function getTxFaceId($uid,$name,$idNo){$appId ;$userId $uid;$nonce …

用 Deepseek 写的uniapp油耗计算器

下面是一个基于 Uniapp 的油耗计算器实现&#xff0c;包含 Vue 组件和页面代码。 1. 创建页面文件 在 pages 目录下创建 fuel-calculator 页面&#xff1a; <!-- pages/fuel-calculator/fuel-calculator.vue --> <template><view class"container"…

Redis ④-通用命令

Redis 是一个 客户端-服务器 结构的程序&#xff0c;这与 MySQL 是类似的&#xff0c;这点需要牢记&#xff01;&#xff01;&#xff01; Redis 固然好&#xff0c;但也不是任何场景都适合使用 Redis&#xff0c;一定要根据当前的业务需求来选择是否使用 Redis Redis 通用命令…

HarmonyOs学习 环境配置后 实验1:创建项目Hello World

HarmonyOS开发入门&#xff1a;环境配置与Hello World实验 实验目标 掌握HarmonyOS开发环境配置&#xff0c;创建首个HarmonyOS应用并实现"Hello World"界面展示 实验准备 已安装DevEco Studio开发环境已配置HarmonyOS开发依赖项熟悉基本TypeScript/ArkTS语法&am…

HTTP:十.cookie机制

Cookie概念及类型 HTTP cookie,简称cookie,又称数码存根、“网站/浏览+魔饼/魔片”等,是浏览网站时由网络服务器创建并由网页浏览器存放在用户计算机或其他设备的小文本文件。Cookie使Web服务器能在用户的设备存储状态信息(如添加到在线商店购物车中的商品)或跟踪用户…

记录小程序第一次调用Api,基于腾讯云Serverless函数,实现小程序的成功接入api,以及数据调用

目录 创建腾讯云个人账户新建severless应用建立函数URL小程序中调用api示例 创建腾讯云个人账户 百度搜索即可&#xff0c;并注册 新建severless应用 作者以github下载的某Api为例&#xff0c;这里不展示具体Api&#xff0c;只关注操作即可&#xff0c;相信都是互通的 在腾…

ES6 第一讲 变量定义 堆与栈 字符串的扩展和数值型的扩展

文章目录 1.ES6变量定义2.ES6堆和栈3.字符串的扩展3.1 模板字符串3.2 判断是否以指定的字符串开头或结尾3.3 字符串重复输出3.4 填充方法3.5 去除前后字符串空格3.6 返回参数指定位置的字符 4. 数值型的扩展4.1 二进制0B 八进制0O4.2 判断是否是一个无穷大的数字 &#xff08;判…

LeetCode第158题_用Read4读取N个字符 II

LeetCode 第158题&#xff1a;用Read4读取N个字符 II 题目描述 给你一个文件&#xff0c;并且该文件只能通过给定的 read4 方法来读取&#xff0c;请实现一个方法来读取 n 个字符。 read4 方法&#xff1a; API read4 可以从文件中读取 4 个连续的字符&#xff0c;并且将它…

算法篇之单调栈

单调栈算法入门 单调栈是一种特殊的数据结构应用&#xff0c;它的核心在于维护一个栈&#xff0c;使得栈内元素保持单调递增或者单调递减的顺序。这种数据结构在解决很多算法问题时非常有效&#xff0c;例如求数组中每个元素的下一个更大元素、每日温度问题等。 一、单调栈的…

Kubernetes控制平面组件:调度器Scheduler(二)

云原生学习路线导航页&#xff08;持续更新中&#xff09; kubernetes学习系列快捷链接 Kubernetes架构原则和对象设计&#xff08;一&#xff09;Kubernetes架构原则和对象设计&#xff08;二&#xff09;Kubernetes架构原则和对象设计&#xff08;三&#xff09;Kubernetes控…

【网络】数据链路层知识梳理

全是通俗易懂的讲解&#xff0c;如果你本节之前的知识都掌握清楚&#xff0c;那就速速来看我的笔记吧~ 自己写自己的八股&#xff01;让未来的自己看懂&#xff01; &#xff08;全文手敲&#xff0c;受益良多&#xff09; 数据链路层 我们来重新理解一下这个图&#xff1a;…

机器学习(神经网络基础篇)——个人理解篇6(概念+代码)

1 在声明一个类中&#xff0c;构建一个属于类的函数&#xff0c;前面为什要加上“self”&#xff1f; 就像下面这一串代码&#xff1a; class TwoLayerNet:def __init__(self, input_size, hidden_size, output_size,weight_init_std0.01):# 初始化权重self.params {}self.p…

Cribl 对Windows-xml log 进行 -Removing filed-06

Removing Fields Description​ The Eval Function can be used to add or remove fields. In this example we will remove the extracted fields while preserving _raw, _time,index,source, sourcetype. Steps - Adding an Eval Function

chili3d调试6 添加左侧面板

注释前 一个一个注释看对应哪个窗口 无事发生 子方法不是显示的窗口 注释掉看看 没了 注释这个看看 零件页面没了 这个浏览器居然完全不用关的&#xff0c;刷新就重载了 注释看看 无工具栏版本 sidebar&#xff1a; 往框框里面加入 div({ className: style.input }, user_…

Linux学习——了解和熟悉Linux系统的远程终端登录

Linux学习——了解和熟悉Linux系统的远程终端登录 一.配置Ubuntu系统的网络和用户 1、设置虚拟机网络为桥接模式 打开VMWare&#xff0c;选择编辑虚拟机设置&#xff0c;在网络适配器设置中&#xff0c;选择“桥接模式”&#xff0c;保存设置并启动Ubuntu。 2、配置Ubuntu的…

【JAVA EE初阶】多线程(1)

这样的代码&#xff0c;虽然也能打印hello thread&#xff0c;但是没有创建新的线程&#xff0c;而是直接在main方法所在的主线程中执行了run的逻辑 start方法&#xff0c;是调用系统api&#xff0c;真正在操作系统内部创建一个线程。这个新的线程会以run作为入口方法&#xff…

javase 学习

一、Java 三大版本 javaSE 标准版 &#xff08;桌面程序&#xff1b; 控制台开发&#xff09; javaME 嵌入式开发&#xff08;手机、小家电&#xff09;基本不用&#xff0c;已经淘汰了 javaEE E业级发开&#xff08;web端、 服务器开发&#xff09; 二、Jdk ,jre jvm 三…

【Linux】Linux 操作系统 - 05 , 软件包管理器和 vim 编辑器的使用 !

文章目录 前言一、软件包管理器1 . 软件安装2 . 包管理器3 . Linux 生态 二、软件安装 、卸载三、vim 的使用1 . 什么是 vim ?2 . vim 多模式3 . 命令模式 - 命令4 . 底行模式 - 命令5. 插入模式6 . 替换模式7 . V-BLOCK 模式8 . 技巧补充 总结 前言 本篇笔者将会对软件包管理…

python基础知识点(1)

python语句 一行写一条语句 一行内写多行语句&#xff0c;使用分号分隔建议每行写一句&#xff0c;且结束时不写分号写在[ ]、{ }内的跨行语句&#xff0c;被视为一行语句\ 是续行符,实现分行书写功能 反斜杠表示下一行和本行是同一行 代码块与缩进 代码块复合语句&#xf…