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

news2024/12/28 18:57:03

前面介绍了环境配置以及基础语法,掌握之后已经可以开始用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.coloradmin.cn/o/723288.html

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

相关文章

C语言究竟是一门怎样的语言?

对于大部分程序员&#xff0c;C语言是学习编程的第一门语言&#xff0c;很少有不了解C的程序员。 C语言除了能让你了解编程的相关概念&#xff0c;带你走进编程的大门&#xff0c;还能让你明白程序的运行原理&#xff0c;比如&#xff0c;计算机的各个部件是如何交互的&#xf…

40、使用elementUI分别实现前端,后端表格分页

说明&#xff1a;前端单独做的表格分页—用于解决数据过多页面渲染压力&#xff0c;如果是服务器响应数据过慢&#xff0c;使用第二种分页方法–后端分页。以下为分页效果 一、前端分页 1、创建表格 <el-table:key"new Date().getTime()":data"tableData.s…

Linux samba服务器配置教程

此教程适用于Centos 和 Ubuntu&#xff0c;其它Linux系统一般大概率配置命令相同&#xff01; 一、关闭防火墙 1. Centos 查看防火墙状态&#xff1a;systemctl status firewalld.service 关闭防火墙&#xff1a;systemctl stop firewalld.service 永久关闭防火墙&#xff…

Node中npm基本使用

1.创建文件夹 说明&#xff1a;创建一个空文件夹&#xff0c;然后以此目录为工作目录&#xff0c;执行npm init,交互式创建package.json文件(包的配置文件) npm init 注意&#xff1a;package下的name属性不能使用中文&#xff0c;大写&#xff0c;默认值为文件名的名称&…

eNSP-ACL分类

ACL分类 文章目录 ACL分类一、题目要求二、题目分析三、拓扑结构四、基本配置五、测试验证 一、题目要求 1 、 client1能够 ping通server&#xff0c;但是不能telnet 2 、 client2能够 telnet, 但是不能 ping 通server 3 、 使用一张 ACL列表 二、题目分析 使用高级ACL在AR3的…

商用车自动驾驶线控底盘测试报告

一&#xff0e;概述 商用车线控底盘主要用于接收控制指令&#xff0c;完成相应的驱动、档位、制动、转向、声光等动作&#xff0c;从而实现自动驾驶功能。 底盘线控系统测试目的是对驾驶模式&#xff08;Drive Mode&#xff09;、油门&#xff08;Throttle&#xff09;、档位&a…

window10环境下安装pycocotools报错及解决办法

相信大家入门安装相关深度学习项目时可能会遇到相关pycocotools&#xff0c;或者ninja的相关报错&#xff0c;下面图是网上拿的&#xff0c;真实报错我忘了截图&#xff0c;但是报错信息如果提到安装Microsoft Visual Tolls&#xff0c;大概率这个可行。 在网上找了很多推荐的…

Kubernetes(k8s)超详细的安装步骤

目录 一、环境设置 二、基本环境配置 &#xff08;一&#xff09;主机名配置 1、在master虚拟机上操作 2、在node1r虚拟机上操作 3、在node2虚拟机上操作 &#xff08;二&#xff09;VMware网络配置 &#xff08;三&#xff09;虚拟机网络配置 1、在master虚拟机上操作…

关于微服务治理的一些理解

关于微服务治理的一些理解 微服务架构存在的意义 根本意义 其主要目的还是为了解耦&#xff0c;提高灵活性和可扩展性&#xff01; 参考&#xff1a;https://zhuanlan.zhihu.com/p/462078779 相比单体 单体架构的性能高于微服务架构&#xff0c;微服务的负载能力低于单体架构…

『MySQL快速上手』Centos 7安装MySQL详解

文章目录 1.卸载掉之前安装的MySQL2.检查系统安装包3.卸载默认安装包4.获取mysql官方yum源5.安装mysql yum源6.安装mysql服务7.查看配置⽂件和数据存储位置8.启动mysql服务9.登录mysql方法一 使用临时密码方法二 直接登录方法三 设置免密码登录 10.设置开机自动启动&#xff08…

【探索AI未来】自动驾驶时代下的人工智能技术与挑战

自我介绍⛵ &#x1f4e3;我是秋说&#xff0c;研究人工智能、大数据等前沿技术&#xff0c;传递Java、Python等语言知识。 &#x1f649;主页链接&#xff1a;秋说的博客 &#x1f4c6; 学习专栏推荐&#xff1a;MySQL进阶之路、C刷题集、网络安全攻防姿势总结 欢迎点赞 &…

TensorFlow项目练手(一)——天气预测

项目介绍 通过以往的天气数据和实际天气温度&#xff0c;做一次回归预测&#xff0c;模型的输入是当前的所有特征值&#xff0c;而模型的输出是当天的实际天气温度 字段分析 目前已有的数据有348条svc数据&#xff0c;他们的字段分别代表 year&#xff1a;年month&#xff…

法雷奥汽车 研发工程师笔试题

Adding to pointer that points to an array will______. A. cause an error B.increase the value of the element that the pointer is pointing to C.cause the pointer to point to the next element in the array D.none of the above C. 使指针指向数组中的下一个元素。 …

厄尔尼诺连续高温会导致能源中的原油、天然气、煤炭和电力等期货品种价格下跌

厄尔尼诺是一种发生在热带海洋中的异常现象&#xff0c;其显著特征是赤道太平洋东部和中部海域海水出现显著增温。厄尔尼诺会导致全球气候变化&#xff0c;影响农业、林业、畜牧业、渔业、交通运输业等领域&#xff0c;进而对大宗商品期货行业产生重大影响。本文将从以下几个方…

docker安装的mysql更改全文检索分词配置

这里使用的是mysql8.0&#xff0c;默认使用ngram分词 这里是已经将文件从容器中挂载出来了&#xff0c;没挂载出来要去容器内部更改my.cnf文件并重启mysql容器 步骤 一、查看mysql的分词大小 show variables like %token%;ngram_token_size这里默认是2我已经改为1了 这个值…

Lua学习笔记:浅谈table的实现

前言 本篇在讲什么 Lua中的table的实现 本篇适合什么 适合初学Lua的小白 本篇需要什么 对Lua语法有简单认知 依赖Sublime Text编辑器 本篇的特色 具有全流程的图文教学 重实践&#xff0c;轻理论&#xff0c;快速上手 提供全流程的源码内容 ★提高阅读体验★ &…

CIO40---22亿灯塔工厂建设规划之工业4.0

1-灯塔工厂规划&#xff1a; 行业趋势 在人工智能、物联网和5G技术的深度渗透下&#xff0c;3C既能作为交互的入口又能是交互的出口&#xff0c;3C产业已成为场景最丰富的产业领域&#xff0c;柔性化生产、个性化定制才能给用户提供更好的体验。市场需求要求企业进行数字化升级…

UE4 如何设置玩家Character的两个位置和角度之间的切换

问题&#xff1a;玩家Character的角度不能直接去设置其中的Camera角度&#xff0c;因为Camera的角度是由鼠标X/Y移动增量决定的&#xff0c;同时把Camera的角度传给PlayController中的PlayCameraManneger&#xff0c;PlayCameraManneger是所有Pawn类型的Camera视口总管&#xf…

Netty的事件驱动模型nio,epoll,oio各个使用场景和支持的网络通讯协议

1.首先说一下nio和epoll有什么区别 在Netty中&#xff0c;Epoll和NIO是两种不同的事件驱动模型&#xff0c;用于实现网络通信。它们在底层的实现和性能特征上有一些区别。 1. NIO&#xff08;Non-blocking I/O&#xff09;&#xff1a;NIO是Java原生的非阻塞I/O模型&#xff…

【NX】NX二次开发中判断曲线是否重合

在NX二次开发中&#xff0c;并没有直接的函数判断两条曲线是否重合&#xff0c;那么我们自己有没有办法判断两条曲线是否重合呢&#xff0c;自然是有的&#xff0c;那么首先我们得定义一下什么叫做重合&#xff0c;几乎重合的曲线算重合吗&#xff0c;这里就涉及到一个容忍度的…