Rust 第三天---内存管理与所有权

前面介绍了环境配置以及基础语法,掌握之后已经可以开始用Rust编写一些简单的程序了,今天就要来介绍一下Rust核心的功能—内存管理与所有权

1. 关于内存管理

无论什么高级语言必须考虑到的一点就是编写程序时对于内存的管理问题,更简单一点解释,利用编程语言能快速高效的分配内存空间,并且将一些不再使用的变量内存空间进行释放回收而不是让内存无限增长.

目前最常见的应该就是两种管理方式:手动管理自动管理.其中手动管理的代表就是C,C++,而Java,Go这种带有GC的则属于自动管理内存.当然,这里提到的内存管理主要是针对堆上的内存空间,毕竟栈上的内存空间管理在编译期间就已经完成了,这也是为什么栈上的对象大小确定的原因,这样更易于内存管理.

但是像C或C++这样需要程序员手动进行内存管理的语言,对于开发者的细心程度要求极高,如果忘记delete,free又或者是编程时出现“野指针”等,都会对编写出的程序造成严重危害.而带有GC的编程语言虽然降低了开发者的开发难度,但同样有一定损耗.在程序运行时必须花费一些额外的代价对程序内的变量进行追踪来确定是否需要释放,而且释放的及时性没有保证,GC时可能会打断工作线程…

说到底,内存管理其实就是在追踪对象的生命周期,当生命周期结束时进行释放就是最理想的状态,说到底就是一个分配对应一个回收的道理,既不遗漏也不重复回收.根据这个想法Rust抛开了之前的手动内存管理也放弃了GC,转而用所有权借用规则来保证内存安全.

2. 所有权与借用规则

所有权和借用规则其实原理非常简单,可以总结成下面的几句话

  1. 在一个作用域内一个值只能有一个所有者.
  2. 当所有者离开作用域时,这个值就会被丢弃.
  3. 值可以从一个作用域移动到另一个作用域,但是当前作用域的所有者会失去对值的所有权.
  4. 值可以被借用,但是借用的生命周期不超过所有者的生命周期.
  5. 在同一作用域中允许多个不可变借用.
  6. 在同一作用域中允许至多一个可变借用.

根据这些规则,Rust编译器会在编译期间进行检查,如果存在安全问题那么这段代码是无法通过编译的,换句话说如果你的代码通过了编译那么一定就是内存安全的.而Rust的作用域划分也很清晰,以大括号进行划分.在大括号结束的时候,就可以调用Drop对作用域中的变量进行释放.

在这里对于资源的初始化与回收,必须提到一个很常见的名词:RAII (Resource Acquisition is Initialization) ,中文翻译资源获取即初始化,字面意思理解就是对象初始化时能导致资源的初始化(获取到),更深层的含义更代表着对象释放时资源也能随之释放,这也是内存安全的重要保障.这种资源释放的思想在很多其他语言其实都有相应的实现,如Python中的with,Go中的defer.

有了上面的这些,理解Rust的内存管理就很简单了.Rust并不关心堆上的内存对象,而只在乎栈上拥有它的所有者.擒贼先擒王,无论怎么借用移动或者引用,只要当所有者离开作用域就把对应堆上的内存资源进行释放,保证了对象与资源的生命周期统一.

上面说了这么多,直接来几个例子来解释一下

2.1 移动

demo1

fn main(){let string_1=String::from("test");let string_2=string_1;println!("string_1={}",string_1);
}

image-20230611192434358

在这里编译器给我们提供了一个报错信息,value borrowed here after move.什么意思呢,代表着字符串的所有权移动给了string_2,而原本的string_1就无法再有效访问了.

根据之前提到过的内容,String类型是动态的,长度可以发生变化,因此它的空间是在堆上分配的,而在栈中只有指向堆中内容的指针.如果let string_2=string_1;这条语句是对堆中内容进行复制,那么会对性能带来额外的消耗;但如果是指向同一地址,那么当两个变量离开作用域后,会对同一内存空间进行两次释放即二次释放,这也是会造成安全性问题的.因此Rust根据上面的规则,直接将string_1无效化而将所有权给新的接收变量string_2,这样既保证了性能也不会造成二次释放,这样的过程称为移动(move)

2.2 拷贝

与上述例子相似的是栈上数据的拷贝

demo2

fn main(){let x=111;let y=x;println!("x={},y={}",x,y);
}

image-20230612082556558

在这里代码与上面的demo类似,却没有任何报错成功编译运行.为什么这里不发生移动?因为i32整型大小确定存储在栈中,在栈中的拷贝几乎没有性能影响且栈中内存由编译器进行管理,因此这里不需要移动所有权.

延伸一下,Rust中有一个Copytrait用于存储在栈上的类型,如果类型拥有这个trait那么旧的变量在赋值给新的变量之后依旧可以访问使用.当然如果拥有了Copy那么就肯定不会拥有Drop,毕竟Drop是针对离开作用域需要特殊处理的类型,二者是矛盾的.

2.3 克隆

接着demo1来讲,如果我们在赋值之后依然想访问string_1呢?这当然也不是没有办法的,我们可以使用clone()对堆上数据进行复制保证两个变量都能同时访问.

demo3

fn main(){let string_1=String::from("test");let string_2=string_1.clone();println!("string_1={} string_2={}",string_1,string_2);
}

image-20230612083948747

这个熟悉一些其他语言或者写过opencv的开发者应该相当熟悉,类似于opencv中不想修改原始Mat,这里使用clone()会对堆上的数据进行克隆复制.不过从demo1就知道,一旦使用clone()就会造成额外的资源消耗,不过也不必太过惊慌,偶尔为了开发效率写出一点“烂代码”也是可以理解的.

2.4 当所有权遇到函数

上面都是在主函数中的demo,如果遇到了函数调用会发生什么呢

fn print_something(message:String){println!("message is {}",message);
}
​
​
fn main(){let string_1=String::from("test");print_something(string_1);println!("string_1={}",string_1);
}

image-20230612084730236

又是熟悉的报错,所有权移动到了函数里,函数结束离开作用域内存就被释放,原本main中的string_1也就无法访问.怎么解决这个问题,一个最简单的想法就是“有借有还”,我们把传入的变量再作为返回值传给原始变量就好了,这样兜兜转转所有权还是在原变量手中.

fn print_something(message:String)->String{println!("message is {}",message);return message;
}
​
​
fn main(){let mut string_1=String::from("test");string_1=print_something(string_1);println!("string_1={}",string_1);
}
​

image-20230612085551256

这样修改后,我们依旧可以在函数调用之后访问string_1.但是注意这里做的修改,除了函数增加返回值,还必须将string_1设为mut也就是可变的,也就是说传回来的虽然值内容没变,但是实际已经改变了.这不正是那句话:”看起来你没变,实际你变了”.当然也不一定非要这样,简单点的话,传入一个clone后的对象也能实现同样功能.

fn print_something(message:String){println!("message is {}",message);
}
​
​
fn main(){let string_1=String::from("test");print_something(string_1.clone());println!("string_1={}",string_1);
}

但是上面也说了,使用clone会造成额外的开销.那有没有更好的解决办法呢?熟悉C或者C++的立马回答道,使用引用!!!

在这里我们可以传入对象的引用而不是真正的值,这样所有权也就不会转移.

fn print_something(message:&String){println!("message is {}",message);
}
​
​
fn main(){let string_1=String::from("test");print_something(&string_1);println!("string_1={}",string_1);
}

其实,这才是真正意义上的“有借有还”.引用只是设置一个访问变量的指针,传入的也是这个指针,所以不会涉及到所有权的转移,同时原始的变量也不会受影响.相比起之前假的“有借有还”,这里使用之后原封不动的退还称为借用

2.5 可变引用

当我们在调用函数中想根据条件然后修改原始值,如果用上面的引用写出来会是这样.

fn change_string(s:&String){if s.len()<10{s.push_str(" len < 10");}else{s.push_str(" len > 10");}
}
​
​
fn main(){let string_1=String::from("test");change_string(&string_1);println!("string_1={}",string_1);
}

编译器微微泛红一笑,告诉你借用的东西不能随便更改.

image-20230612091532227

不过编译器也好心提示了,可以将引用设置为mut,也就是可变引用

fn change_string(s:&mut String){if s.len()<10{s.push_str(" len < 10");}else{s.push_str(" len > 10");}
}
​
​
fn main(){let mut string_1=String::from("test");change_string(&mut string_1);println!("string_1={}",string_1);
}

image-20230612091743606

这就类似于所有者给你权限,允许你更改它所拥有的东西,相当于权限更大的借用.

fn change_string(s1:&mut String,s2:&mut String){if s1.len()<10{s1.push_str(" len < 10");}else{s1.push_str(" len > 10");}
​if s2.len()<5{s2.push_str(" len < 5");}else{s2.push_str(" len > 5");}
}
​
​
fn main(){let mut string_1=String::from("test");change_string(&mut string_1,&mut string_1);println!("string_1={}",string_1);
}

当我们故意写了一段匪夷所思的代码,原本意思是想对传入的String长度设置两个阈值条件,但是写成了这样,编译器再次报错.image-20230612092507682

编译器说这里有两次可变借用发生,因此在同一作用域至多只能有一个可变引用,这样就可以防止数据竞争.

3. 总结

到这里,对Rust的所有权特性应该有了大致的感受,和以往的编程语言不同,Rust对编译时格外严格以求运行时顺利.这对于开发者应该也是比较好的体验,看到顺利编译出可执行文件就可以放心下班而不用随时担心测出Bug然后慢慢trace去找Bug的原因.当然这也不是绝对的,毕竟机器之外我们还得与人打交道,哈哈哈.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/15884.html

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

相关文章

【零基础入门学习Python---Python中机器学习和人工智能之快速入门实践】

&#x1f680; 零基础入门学习Python&#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜…

GitHub Pages + Hexo

步骤 参考如下步骤&#xff1a;https://blog.csdn.net/yaorongke/article/details/119089190 出现的问题 1 Fluid主题 其更换Fluid主题时&#xff1a; 下载最新 release 版本 解压到 themes 目录&#xff0c;并将解压出的文件夹重命名为 fluid 按照上面执行后&#xff0c;后…

历史上的今天发生了什么事?华为日历带你“穿越”涨知识

探寻过去就会发现各种有趣的历史故事和知识值得细细品味&#xff0c;如果你想“穿越”过去&#xff0c;回顾历史上的今天曾经发生过的事件&#xff0c;或是以更轻松的方式增长历史知识&#xff0c;不妨试试订阅华为日历【历史上的今天】订阅服务。 无论是对历史一知半解的小白&…

基于PyQt5的桌面图像调试仿真平台开发(8)锐化

系列文章目录 基于PyQt5的桌面图像调试仿真平台开发(1)环境搭建 基于PyQt5的桌面图像调试仿真平台开发(2)UI设计和控件绑定 基于PyQt5的桌面图像调试仿真平台开发(3)黑电平处理 基于PyQt5的桌面图像调试仿真平台开发(4)白平衡处理 基于PyQt5的桌面图像调试仿真平台开发(5)…

CTFSHOW 每周大挑战 RCE极限挑战

RCE挑战1 开题直接给了源码 过滤了括号和点号。 使用内敛绕过。 payload&#xff1a; codeecho tac /f1agaaa;RCE挑战2 源码直接给了。 基本把能用的都过滤了&#xff0c;只剩下$()_;[],./字符&#xff0c;自增RCE无疑。 //相当于 ($_GET[_])($_GET[__]) 使用的时候url编…

基于simulink使用二维规范化互相关进行模式匹配和目标跟踪(附源码)

一、前言 此示例演示如何使用二维规范化互相关进行模式匹配和目标跟踪。双击“编辑参数”块以选择要检测的类似目标的数量。您还可以更改金字塔因子。通过增加它&#xff0c;您可以更快地将目标模板与每个视频帧匹配。更改金字塔因子可能需要更改阈值。 此外&#xff0c;还可…

华为云流水线CodeArts Pipeline怎么样?能实现哪些功能?

华为云流水线服务CodeArts Pipeline&#xff0c;旨在提升编排体验&#xff0c;开放插件平台&#xff0c;并提供标准化的DevOps企业治理模型&#xff0c;将华为公司内的优秀研发实践赋能给伙伴和客户。 灵活编排、高效调度 开放流水线插件 内置企业DevOps研发治理模型 体验通…

学习单片机的三个步骤:基础知识、编程语言和实践项目

当然&#xff01;学习单片机的过程可以分为以下三个步骤&#xff1a; 学习基础知识&#xff1a;在开始学习单片机之前&#xff0c;首先需要掌握一些基础知识。了解数字电子学和模拟电子学的基本原理是很重要的&#xff0c;包括数字和模拟信号、逻辑门、计数器、寄存器等。还需…

Python: 如何批量预处理FY4A L1 DISK和REGC产品?(辐射定标/裁剪/GLT校正/HDF5转TIFF文件等)

目录 01 前言 1.1 想要说 1.2 Requirements 1.3 程序适用数据集 02 函数说明 2.1 读取HDF5文件某一数据集 2.2 读取HDF5文件数据集属性 2.3 对FY4A数据集进行辐射定标 2.4 基于官方地理对照表获取经纬度数据(仅适用DISK) 2.5 依据行列号计算经纬度数据(仅适用DISK) …

【观察】新五丰联合华为“躬身实践”,推动猪场实现智慧化跨越升级

中国是全球的生猪生产和消费大国&#xff0c;生猪存栏量、出栏量以及猪肉产量均居世界第一。不仅如此&#xff0c;我国的人口数量和饮食结构还决定了猪肉在国内肉类消费中具有“不可撼动”的地位&#xff0c;可以说猪肉的供应与国计民生息息相关。 数据显示&#xff0c;2022年中…

4-软件错误(BUG)

目录 1.什么是bug? 2.如何描述一个bug? ①发现问题的版本 ②问题出现的环境 ③错误重现的步骤 ④预期行为的描述 ⑤错误行为的描述 ⑥其他 ⑦不要把多个bug放到一起 PS&#xff1a;案例1 PS&#xff1a;案例2 3.如何定义bug的级别&#xff1f; ①Blocker&#x…

Kubernetes Pod卷 - Pod镜像的升级和回滚 - 探针

目录 扩展&#xff1a; Pod创建的拓扑图&#xff1a; 提出的问题&#xff1a; Pod 卷的使用&#xff1a;Pod的数据持久化问题 配置 Pod 以使用卷进行存储 参考文档&#xff1a;配置 Pod 以使用卷进行存储 | Kubernetes 有状态应用和无状态应用&#xff1a; Pod 配置卷 1…