【Rust自学】15.7. 循环引用导致内存泄漏
说句题外话,这篇文章真心很难,有看不懂可以在评论区问,我会尽快作答的。
喜欢的话别忘了点赞、收藏加关注哦(加关注即可阅读全文),对接下来的教程有兴趣的可以关注专栏。谢谢喵!(=・ω・=)

15.7.1. 内存泄漏
Rust极高的安全性使得内存泄漏很难出现,但并不是完全不可能。
例如使用Rc<T>和RefCell<T>就可能创造出循环引用,造成内存泄漏:每个指针的引用计数都不会减少到0,值也不会被清理。
看个例子:
use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;#[derive(Debug)]
enum List {Cons(i32, RefCell<Rc<List>>),Nil,
}impl List {fn tail(&self) -> Option<&RefCell<Rc<List>>> {match self {Cons(_, item) => Some(item),Nil => None,}}
}fn main() {let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));println!("a initial rc count = {}", Rc::strong_count(&a));println!("a next item = {:?}", a.tail());let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));println!("a rc count after b creation = {}", Rc::strong_count(&a));println!("b initial rc count = {}", Rc::strong_count(&b));println!("b next item = {:?}", b.tail());if let Some(link) = a.tail() {*link.borrow_mut() = Rc::clone(&b);}println!("b rc count after changing a = {}", Rc::strong_count(&b));println!("a rc count after changing a = {}", Rc::strong_count(&a));
}
-
首先创建了一个链表
List,使用RefCell<T>包裹Rc<T>使其内部值可被修改 -
通过
impl块为List写了一个叫tail的方法,用于获取List下Cons变体附带的第二个元素,如果有就返回其值,用Some封装,是Nil就返回None。 -
然后在
main函数创建了a、b两个List的实例,b内部共享了a的值。这种链表的代码看着就犯恶心,所以我把其结构图放在
这里: -
main函数里还通过Rc::strong_count获取了a和b的强引用数量,使用自定义的tail方法获了Cons附带的第二个元素,用println!打印出来。 -
下面使用
if let语句把a的Cons的第二个值绑在link上,通过borrow_mut方法获得其可变引用&Cons,使用解引用符号*把它转为Cons,最后把b的值通过Rc::clone共享赋给了link,也就改变了a内部的结构,变为了:

PS:我觉得自己画的太烂了,所以这里就换成The Rust Programming Language里的图片了
输出:
a initial rc count = 1
a next item = Some(RefCell { value: Nil })
a rc count after b creation = 2
b initial rc count = 1
b next item = Some(RefCell { value: Cons(5, RefCell { value: Nil }) })
b rc count after changing a = 2
a rc count after changing a = 2
第1行到第5行:刚开始创建a时,引用数量就为1,当b被声明时,a被共享了,所以此时a的引用计数为2,b为1。
第六行到第7行:if let语句把a的内部结构改变了,使a的第二个元素指向b,b的引用数量加1变为2。此时a指向了b,b又指向了a,就会造成循环引用。
当a和b都走出了作用域,Rust删除了变量b ,这将b的引用计数从 2 减少到 1。此时Rc<List>在堆上的内存不会被删除,因为它的引用计数是1,而不是0。然后 Rust 删除a ,这会将a的Rc<List>实例的引用计数从 2 减少到 1,如图所示。这个实例的内存也不能被删除,因为另一个实例的内存 Rc<List>实例仍然引用它。分配给列表的内存将永远保持未回收状态。
接下来我们看看循环引用的内容是什么,使用这条代码:
println!("a next item = {:?}", a.tail());
Rust 将尝试打印此循环,其中a指向b指向a等等,直到溢出堆栈。最终的结果会是栈溢出错误。
15.7.2. 防止内存泄漏的方法
那有什么方法来防止内存泄漏吗?这只能依靠开发者,不能依靠Rust。
不然就只能重新组织数据结构,把引用拆分成持有和不持有所有权的两种情况,一些引用用来表达所有权,一些引用不表达所有权。循环引用的一部分具有所有权关系,另一部分不涉及所有权关系。这样写只有所有权的指向关系才会影响到值的清理。
15.7.3. 把Rc<T>换成Weak<T>以防止循环引用
我们知道Rc::clone会生成数据的强引用,使Rc<T>内部的引用计数加1,而Rc<T>只有在strong_count为0时才会被清理。
然而,Rc<T>实例通过调用Rc::downgrade方法创建值的弱引用(Weak Reference)。这个方法的返回类型是weak<T>(也是智能指针),每次调用Rc::downgrade会为weak_count加1而不是strong_count,所以弱引用并不影响Rc<T>的清理。
15.7.4. Strong vs. Weak
强引用(Strong Reference)是关于如何分析Rc<T>实例的所有权。弱引用(Weak Reference)并不表达上述意思,使用它不会创建循环引用:当强引用数量为0时,弱引用就会自动断开。
使用弱引用之前需要保证它指向的值仍然存在。在Weak<T>实例上调用upgrade方法,返回Option<Rc<T>>,通过Option枚举来完成值是否存在的验证。
看个例子:
use std::cell::RefCell;
use std::rc::Rc;#[derive(Debug)]
struct Node {value: i32,children: RefCell<Vec<Rc<Node>>>,
}fn main() {let leaf = Rc::new(Node {value: 3,children: RefCell::new(vec![]),});let branch = Rc::new(Node {value: 5,children: RefCell::new(vec![Rc::clone(&leaf)]),});
}
Node结构体代表一个节点,有两个字段:
value字段存储当前值,类型是i32。children字段存储子节点,类型是RefCell<Vec<Rc<Node>>>,这里使用Rc<T>是为了让所有子节点共享所有权。具体来说,我们希望一个Node拥有它的子节点,并且我们希望与储存这个节点的变量共享该所有权,以便我们可以直接访问树中的每个Node。为此,我们将Vec<T>项定义为Rc<Node>类型的值。
这个例子的需求是每个节点都能指向自己的父节点和子节点。
再看一下main函数:
- 创建了
leaf,是Node实例,value为3,children的值是被RefCell包裹的空Vector。 - 创建了
branch,是Node实例,value为5,children的值指向了leaf。
这意味着leaf它里面的Node节点有两个所有者。目前可以通过branch的children字段访问leaf;而反过来如果想通过leaf来访问branch暂时还不行,所以这里还需要修改。
想要实现需求就得用双向引用,但是双向的引用会创建循环引用,所以这时候就得使用Weak<T>,避免产生循环:
struct Node {value: i32,parent: RefCell<Weak<Node>>,children: RefCell<Vec<Rc<Node>>>,
}
添加了parent字段表示父节点,使用弱引用Weak<T>。这里不用Vec<>是因为这是个树结构,父节点只可能有一个。
这么写得把Weak<T>引入作用域,还得重构下文,修改完后的整体代码如下:
use std::cell::RefCell;
use std::rc::{Rc, Weak};#[derive(Debug)]
struct Node {value: i32,parent: RefCell<Weak<Node>>,children: RefCell<Vec<Rc<Node>>>,
}fn main() {let leaf = Rc::new(Node {value: 3,parent: RefCell::new(Weak::new()),children: RefCell::new(vec![]),});println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());let branch = Rc::new(Node {value: 5,parent: RefCell::new(Weak::new()),children: RefCell::new(vec![Rc::clone(&leaf)]),});*leaf.parent.borrow_mut() = Rc::downgrade(&branch);println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
}
在leaf被创建后先打印了其parent字段的内容(这时parent字段还没有内容);在branch被创建后打印了leaf的parent字段内容(这时其内容就是branch)。
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);这句话把branch的内容从Rc<Node>变为Weak<Node>,指向了leaf的parent字段:
leaf.parent是表示leaf父节点的字段,其类型是RefCell<Weak<Node>>,所以可以使用borrow_mut来获得其可变引用&mut RefMut<Weak<Node>>- 使用解引用符号
*把可变引用&mut RefMut<Weak<Node>>变为RefMut<Weak<Node>> - 通过
downgrade方法把branch的Rc<Node>变为Weak<Node>并赋给parent
输出:
leaf parent = None
leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) },
children: RefCell { value: [Node { value: 3, parent: RefCell { value: (Weak) },
children: RefCell { value: [] } }] } })
- 第一次打印时其
parent字段还没有被赋值,所以其值是Option下的None变体。 - 第二次打印时其父节点已被指定为
branch,不是无限输出表明此代码没有创建循环引用。
最后我们通过修改main函数——添加打印语句和修改作用域来看看强引用和弱引用的数量:
fn main() {let leaf = Rc::new(Node {value: 3,parent: RefCell::new(Weak::new()),children: RefCell::new(vec![]),});println!("leaf strong = {}, weak = {}",Rc::strong_count(&leaf),Rc::weak_count(&leaf),);{let branch = Rc::new(Node {value: 5,parent: RefCell::new(Weak::new()),children: RefCell::new(vec![Rc::clone(&leaf)]),});*leaf.parent.borrow_mut() = Rc::downgrade(&branch);println!("branch strong = {}, weak = {}",Rc::strong_count(&branch),Rc::weak_count(&branch),);println!("leaf strong = {}, weak = {}",Rc::strong_count(&leaf),Rc::weak_count(&leaf),);}println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());println!("leaf strong = {}, weak = {}",Rc::strong_count(&leaf),Rc::weak_count(&leaf),);
}
代码的逻辑是:
-
创建完
leaf之后打印里面有多少强引用和弱引用 -
这部分完了之后加了
{},创建了新的作用域:- 把
branch的声明和指定leaf父节点的操作放到里面 - 打印
branch和leaf在此时强引用、弱引用的数量
- 把
-
走出作用域后:
- 打印
leaf的parent - 打印
leaf的强引用、弱引用
- 打印
输出:
leaf strong = 1, weak = 0
branch strong = 1, weak = 1
leaf strong = 2, weak = 0
leaf parent = None
leaf strong = 1, weak = 0
- 第1行:创建了
leaf,只有一个强引用 - 第2行:创建了
branch,由于branch使用强引用对leaf进行了关联,其parent字段使用了Weak::new()创建,所以branch有1个强引用,一个弱引用 - 第3行:
branch使用了leaf的强引用,其本身在声明时又是一个强引用,所以此时leaf就有两个强引用 - 第4行:由于
branch已经走出其作用域,所以leaf的parent字段此时就为None - 第5行:
branch已经走出其作用域导致它对leaf的强引用失效,leaf的强引用减1变为1
相关文章:
【Rust自学】15.7. 循环引用导致内存泄漏
说句题外话,这篇文章真心很难,有看不懂可以在评论区问,我会尽快作答的。 喜欢的话别忘了点赞、收藏加关注哦(加关注即可阅读全文),对接下来的教程有兴趣的可以关注专栏。谢谢喵!(・ω…...
C#AWS signatureV4对接Amazon接口
马上要放假了,需要抓紧时间测试对接一个三方接口,对方是使用Amazon服务的,国内不多见,能查的资(代)料(码),时间紧比较紧,也没有时间去啃Amazon的文档,主要我的英文水平也不行,于是粗…...
C语言操作符(下)
上一篇文章传送门:操作符上 前言:上期我们介绍了C语言的操作符的使用方法,这期我们主要侧重讲当我们已经了解了操作符的基本知识后怎样样来看待运算路径的问题。 操作符 一,优先级和结合性1,优先级2,结合性…...
学习资料收藏 游戏开发
本文整理了本人在学习 Unity3D 游戏开发过程中知晓的一些学习资料。 视频教程 siki学院 M_Studio Unity中文课堂 博客 林新发 浅墨_毛星云 冯乐乐 Roystan Sorumi 宣雨松 陆泽西 书籍 《Unity 游戏设计与实现》(加藤政树) 《Unity Shader 入…...
我的2024年总结
趁着摸鱼赶紧写一下吧 去年目标review 还是将去年的目标完成了一些 【接纳不完美,多拍照片】 这个还是部分做到了,今年和一些朋友们见面时都注意拍照留记录了,不过还可以继续加强,因为外貌上发生了重大变化,下面细说…...
freeswitch在centos上编译过程
操作系统:centos9-last usr/local/freeswitch/bin/freeswitch -version FreeSWITCH version: 1.10.13-devgit~20250125T131725Z~3f1e4bf90a~64bit (git 3f1e4bf 2025-01-25 13:17:25Z 64bit)vi /etc/ssh/sshd_config ip a nmtui reboot ip a curl -o /etc/pki/rpm-…...
docker如何查看容器启动命令(已运行的容器)
docker ps 查看正在运行的容器 该命令主要是为了详细展示查看运行时的command参数 # 通过docker --no-trunc参数来详细展示容器运行命令 docker ps -a --no-trunc | grep <container_name>通过docker inspect命令 使用docker inspect,但是docker inspect打…...
正则表达式以及Qt中的使用
目录 一、正则表达式 1、基本匹配: 2、元字符: 2.1 .运算符: 2.2 字符集: 2.3 重复次数: 2.4 量词{} 2.5 特征标群() 2.6 或运算符 2.7 \反斜线转码特殊字符 2.8 锚点 3、简写字符 4、零宽度断言 4.1 正…...
当高兴、尊重和优雅三位一体是什么情况吗?
英语单词 disgrace 表示“失脸,耻辱,不光彩,名誉扫地”一类的含义,可做名词或动词使用,含义基本一致,只是词性不同。 disgrace n.丢脸;耻辱;不光彩;令人感到羞耻的人(或…...
Vue 3 中的 TypeScript:接口、自定义类型与泛型
在 Vue 3 中,TypeScript 提供了强大的类型系统,帮助我们更好地管理代码的类型安全。通过使用 接口(Interface)、自定义类型(Type Aliases) 和 泛型(Generics),我们可以编…...
【Super Tilemap Editor使用详解】(十六):高级主题:深入理解 Super Tilemap Editor
在本节中,我们将深入探讨 Super Tilemap Editor 的工作原理,特别是图块地图(Tilemap)的渲染机制以及如何优化性能。这些知识将帮助你更好地理解工具的内部机制,并在开发中做出更明智的决策。 一、图块地图与图块渲染 图块地图是 Super Tilemap Editor 的核心组件之一。它由…...
如何运用python爬虫爬取知网相关内容信息?
爬取知网内容的详细过程 爬取知网内容需要考虑多个因素,包括网站的结构、反爬虫机制等。以下是一个详细的步骤和代码实现,帮助你使用Python爬取知网上的论文信息。 1. 数据准备 首先,需要准备一些基础数据,如知网的URL、请求头…...
2025年数学建模美赛 A题分析(2)楼梯使用频率数学模型
2025年数学建模美赛 A题分析(1)Testing Time: The Constant Wear On Stairs 2025年数学建模美赛 A题分析(2)楼梯磨损分析模型 2025年数学建模美赛 A题分析(3)楼梯使用方向偏好模型 2025年数学建模美赛 A题分…...
云原生:构建现代化应用的基石
一、什么是云原生? 云原生是一种构建和运行应用程序的方法,旨在充分利用云计算的分布式系统优势,例如弹性伸缩、微服务架构、容器化技术等。云原生应用程序从设计之初就考虑到了云环境的特点,能够更好地适应云平台的动态变化&…...
18.Word:数据库培训课程❗【34】
目录 题目 NO1.2.3.4 NO5设置文档内容的格式与样式 NO6 NO7 NO8.9 NO10.11标签邮件合并 题目 NO1.2.3.4 FnF12:打开"Word素材.docx”文件,将其另存为"Word.docx”在考生文件夹下之后到任务9的所有操作均基于此文件:"Word.docx”…...
批量创建ES索引
7.x from elasticsearch import Elasticsearch# 配置 Elasticsearch 连接 # 替换为你的 Elasticsearch 地址、端口、用户名和密码 es Elasticsearch([http://10.10.x.x:43885],basic_auth(admin, XN272G9THEAPYD5N5QORX3PB1TSQELLB) )# # 测试连接 # try: # # 尝试获取集…...
RoboVLM——通用机器人策略的VLA设计哲学:如何选择骨干网络、如何构建VLA架构、何时添加跨本体数据
前言 本博客内解读不少VLA模型了,包括π0等,且如此文的开头所说 前两天又重点看了下openvla,和cogact,发现 目前cogACT把openvla的动作预测换成了dit,在模型架构层面上,逼近了π0那为了进一步逼近&#…...
25美赛ABCDEF题详细建模过程+可视化图表+参考论文+写作模版+数据预处理
详情见该链接!!!!!! 25美国大学生数学建模如何准备!!!!!-CSDN博客文章浏览阅读791次,点赞13次,收藏7次。通过了解比赛基本…...
基于RIP的MGRE VPN综合实验
实验拓扑 实验需求 1、R5为ISP,只能进行IP地址配置,其所有地址均配为公有IP地址; 2、R1和R5间使用PPP的PAP认证,R5为主认证方; R2与R5之间使用ppp的CHAP认证,R5为主认证方; R3与R5之间使用HDLC封…...
如何获取小程序的code在uniapp开发中
如何获取小程序的code在uniapp开发中,也就是本地环境,微信开发者工具中获取code,这里的操作是页面一进入就获取code登录,没有登录页面的交互,所以写在了APP.vue中,也就是小程序一打开就获取用户的code APP.…...
【网络安全产品大调研系列】2. 体验漏洞扫描
前言 2023 年漏洞扫描服务市场规模预计为 3.06(十亿美元)。漏洞扫描服务市场行业预计将从 2024 年的 3.48(十亿美元)增长到 2032 年的 9.54(十亿美元)。预测期内漏洞扫描服务市场 CAGR(增长率&…...
【SQL学习笔记1】增删改查+多表连接全解析(内附SQL免费在线练习工具)
可以使用Sqliteviz这个网站免费编写sql语句,它能够让用户直接在浏览器内练习SQL的语法,不需要安装任何软件。 链接如下: sqliteviz 注意: 在转写SQL语法时,关键字之间有一个特定的顺序,这个顺序会影响到…...
React19源码系列之 事件插件系统
事件类别 事件类型 定义 文档 Event Event 接口表示在 EventTarget 上出现的事件。 Event - Web API | MDN UIEvent UIEvent 接口表示简单的用户界面事件。 UIEvent - Web API | MDN KeyboardEvent KeyboardEvent 对象描述了用户与键盘的交互。 KeyboardEvent - Web…...
苍穹外卖--缓存菜品
1.问题说明 用户端小程序展示的菜品数据都是通过查询数据库获得,如果用户端访问量比较大,数据库访问压力随之增大 2.实现思路 通过Redis来缓存菜品数据,减少数据库查询操作。 缓存逻辑分析: ①每个分类下的菜品保持一份缓存数据…...
鸿蒙中用HarmonyOS SDK应用服务 HarmonyOS5开发一个医院查看报告小程序
一、开发环境准备 工具安装: 下载安装DevEco Studio 4.0(支持HarmonyOS 5)配置HarmonyOS SDK 5.0确保Node.js版本≥14 项目初始化: ohpm init harmony/hospital-report-app 二、核心功能模块实现 1. 报告列表…...
[Java恶补day16] 238.除自身以外数组的乘积
给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。 题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。 请 不要使用除法,且在 O(n) 时间复杂度…...
使用 Streamlit 构建支持主流大模型与 Ollama 的轻量级统一平台
🎯 使用 Streamlit 构建支持主流大模型与 Ollama 的轻量级统一平台 📌 项目背景 随着大语言模型(LLM)的广泛应用,开发者常面临多个挑战: 各大模型(OpenAI、Claude、Gemini、Ollama)接口风格不统一;缺乏一个统一平台进行模型调用与测试;本地模型 Ollama 的集成与前…...
服务器--宝塔命令
一、宝塔面板安装命令 ⚠️ 必须使用 root 用户 或 sudo 权限执行! sudo su - 1. CentOS 系统: yum install -y wget && wget -O install.sh http://download.bt.cn/install/install_6.0.sh && sh install.sh2. Ubuntu / Debian 系统…...
LOOI机器人的技术实现解析:从手势识别到边缘检测
LOOI机器人作为一款创新的AI硬件产品,通过将智能手机转变为具有情感交互能力的桌面机器人,展示了前沿AI技术与传统硬件设计的完美结合。作为AI与玩具领域的专家,我将全面解析LOOI的技术实现架构,特别是其手势识别、物体识别和环境…...
Kafka主题运维全指南:从基础配置到故障处理
#作者:张桐瑞 文章目录 主题日常管理1. 修改主题分区。2. 修改主题级别参数。3. 变更副本数。4. 修改主题限速。5.主题分区迁移。6. 常见主题错误处理常见错误1:主题删除失败。常见错误2:__consumer_offsets占用太多的磁盘。 主题日常管理 …...
