Rust之抽空学习系列(五)—— 所有权(上)

devtools/2024/12/23 16:06:41/

Rust之抽空学习系列(五)—— 所有权(上)

1、什么是所有权

所有权确保Rust程序安全的一种机制

  • 安全则是指程序中没有未定义的行为
  • 未定义的行为是指在执行一段代码时,结果不可预测且未被编程语言指定的情况
  • Rust的基础目标是确保程序永远不会有未定义的行为,次要目标是未定义行为应当在编译时被发现而不是运行时

Rust没有如同Java、Python等语言的垃圾回收机制,也不同于C/C++的纯手动内存操作,而是引入了所有权的概念以及相应的工具支持,来保证内存的安全可靠

1.1、初识

内存是计算机内一种有限的资源,因此如果想要编写的程序能够持续高效地在计算机上运行,那么对于内存的管理是不可或缺的

在这里插入图片描述
编程语言发展至今,主要有以下三种具有代表性的内存管理方式:
1、C/C++采用的成对的mallocfree以及newdelete,通过这些API直接操作系统的内存的申请和释放(这样操作的时候,你往往具有较高的权限,然而每一步需要操作者自己管理,效率是比较低下的,也容易犯错)
2、以Java、Python为首的具有垃圾回收机制的语言,它们通常除了程序本身的业务线程外,还会额外增设一个垃圾回收线程(根据既定的垃圾回收算法自动回收内存,牺牲少量的效率,但是保证了开发者的开发效率)
3、接下来要涉及到的Rust独创的所有权机制,依赖于制定的所有权规则进行约束和检查

1.2、所有权规则

既然上面提到了所有权规则,那接下来我们见识下都有哪些规则:
1、Rust中的每一个值都有一个对应的变量作为它的所有者
2、在同一时间内,值有且仅有一个所有者
3、当所有者离开自己的作用域时,它持有的值就会被释放掉
在这里插入图片描述
我觉着大概像是这样的关系吧⬆️

1.3、变量作用域

变量的作用域指的是变量在程序中有效的范围

rust">fn main() {// 未声明,不可用let s1 = String::from("hello");   // 声明s1,可用println!("{}", s1);    //  s1可用
}   // 超出作用域,不可用

以上是一段简单的程序,声明一个变量 s1,并且进行输出
main函数提供了一个代码块,s1就被声明在这个代码块内部,因此这也是 s1 的作用域
在 s1 变量被let声明前,当前作用域并不知道有s1,因此此时就是开头说的未定义状态,自然是不可用的
接下来,声明了s1,那么作用域知道它的存在,就可以使用 s1了,直到当前代码块结束前,都是有效的
而超出代码块,也就是离开作用域,s1就不可用了,并且Rust会释放它的内存

在这里插入图片描述
从变量声明到所处代码块结束,这段区间内便是变量有效的区间

  • 变量进入作用域开始,它就是有效的
  • 一直持续到离开作用域

1.4、初识 String 类型

接下来,通过一个数据类型String来更好地了解下所有权的内容
在Rust程序中,String类型的数据存储在堆上,这是由于很多场景下字符串往往不能在程序运行之前知道其需要多大的内存空间,像那种能够在编译期确定下来的字符串,被称为字符串字面量,本身是不变的

rust">use std::io;   //fn main() {println!("请输入一些文本:");// 用来存储输入的文本let mut input = String::new();// 从标准输入中读取一行文本,存储在input中io::stdin().read_line(&mut input).expect("读取输入时发生错误");println!("你输入的是:{}", input);
}

在这里插入图片描述
像上面这种从用户输入处读取字符串的场景下,用户输什么,输多输少就很难确定了,因此String在程序运行时申请堆内存进行存储

接下来,看一个更加简单的程序:

rust">// 创建一个String的实例
let str = String::from("风浪越大,鱼越贵");

这里使用String提供的from()基于一个字符串字面量创建一个String实例
在这里插入图片描述
可以看到文档的定义,from()返回的结果会被分配到堆空间

rust">let mut str = String::from("风浪越大,鱼越贵");   // 声明为可变
str.push_str("!");   // 可变的基础上添加内容
println!("{}", str);

在这里插入图片描述

使用mut关键字可以进一步将String声明为可变的,进而调用push_str()方法进行字符串的拼接

字符串String是可变的,而字符串字面量不是,这是由于此二者采用了不同的内存处理方式

1.5、内存与分配

字符串字面量我们能够在编译时就确定其内容,因而这部分硬编码的文本直接嵌入最终的可执行文件,这样访问字符串字面量会非常高效,这是由于字符串字面量的不变性

而对String类型来说,需要在运行时申请一块内存,先前通过String::from()的调用发起这个请求

在使用完之后,需要以某种形式将这些内存归还给操作系统,这个对于不同的编程语言有着不同的解决方式,比如Java、Python等采用的是垃圾回收机制,通过一个垃圾回收程序识别不再被使用的内存空间,将它们及时释放,此过程不需要开发人员参与,而C/C++则是由开发者进行手动处理,那么这就需要开发者把握恰当的时机,避免发生难以预测的问题

Rust有一套不同的方案:Rust会在变量离开作用域后释放其持有的内存

在这里插入图片描述

在作用域结束的地方,String类型会自动调用一个名叫drop()的特殊函数,这其实是实现了Drop这个trait(之后再探讨)
我们可以通过断点调试的方式证实一下

在这里插入图片描述

string.rs里面找到对应Drop出现的位置打上断点,然后Debug执行

在这里插入图片描述

可以看到,程序在执行完main的内容后停住了,来到了drop方法执行内存的释放,这便是Rust利用所有权机制进行的资源回收

1.5.1、移动

rust">let a = 2;   // a -> 2
let b = a;   // b -> a

先来看段简单的代码,根据Rust的类型推导可以了解到a和b会被推导为i32类型,这是一个赋值的过程

2绑定到变量a上,由于是i32大小是确定的,所以会再创建一个变量a的拷贝,绑定到b上,这样a和b的值都是2,这两个变量都被压入中,这里就涉及到了栈的内容

此时,a和b是相互独立的

再看另一段代码:

rust">let str1 = String::from("hello");
let str2 = str1;

似乎也是一段赋值,只不过这次换成了String类型,好像没什么不同,也许也是str2作为str1的拷贝?但是其实事实并非如此

在这里插入图片描述
简单表示一下String的内存布局,主要是由3个部分组成:指针(ptr)、长度(len)、容量(capacity),并且这些数据都存储在栈当中

右侧则是表示的存储的字符串的内容,体现了二者之间的绑定关系

len用来记录当前String中使用了多少字节内存,可以看到是5;capacity用来记录String向操作系统总共获取到的内存字节数量,也是5,尽管此时二者的值相等,但是是有区别的

将str1赋值给str2的时候,的确是发生了复制,但是复制的内容是栈里的,因而存储在栈当中的指针、长度、容量字段会在栈上再存在一份(这份便是str2)所对应的

在这里插入图片描述

此时,str1和str2中的指针均指向一块堆内存,这与深拷贝的结果是不一样的

如果是深拷贝,则会连同指向的内容一并拷贝一份

在这里插入图片描述
但是,如果是深拷贝,在数据足够大时,这种大篇幅的复制会带来很大的性能损耗

现在由于str1和str2两个变量同时指向一块区域,如果其中任意一方离开作用域时,Rust会自动去调用drop()释放内存,而后者再离开作用域时,将会释放一块已被释放的内存,这便引发了二次释放问题,这将会导致正在使用的数据发生问题,进而埋下安全隐患

那么Rust对于解决这样的问题的手段很彻底,那就是保留一份

let str2 = str1执行后,str1自动废弃,也就不再被程序作为一个有效的变量,自然也不需要对其清理
在这里插入图片描述
在这里插入图片描述

在发生移动后,继续使用变量str1会报错,因为此时str1已经失效了

浅拷贝和深拷贝
其实,这里的情况与浅拷贝有所区别,栈上内容复制的同时,Rust又将前者无效化,因而新增了移动
的概念,有点类似于str2是str1的接班人;而深拷贝便是将栈上与堆上的数据一并复制的概念

在这里插入图片描述
Rust永远不会自动地创建数据的深拷贝,因此,任何自动地赋值操作都将是高效的

1.5.2、克隆

如果说一定需要进行深度拷贝,而不是仅仅复制栈上的数据,那么可以使用一个clone()方法

rust">let str1 = String::from("hello");
let str2 = str1.clone();   // str1的深拷贝
println!("{}", str1);
println!("{}", str2);

通过调用clone(),str2把str1栈上和堆上的数据都拷贝了一份,而这可能会相当耗资源

但是对于那些在编译时就已经确定了大小的类型,无论是普通赋值(栈拷贝)或是clone都没有本质上的区别,总是高效的

1.5.3、Copy trait(简单了解)

Rust 有一个叫做 Copy trait 的特殊注解,可以用在类似整型这样的存储在栈上的类型上
如果一个类型实现了 Copy trait,那么一个旧的变量在将其赋值给其他变量后仍然可用,像是i32这种

Rust 不允许自身或其任何部分实现了 Drop trait 的类型使用 Copy trait

在这里插入图片描述

任何不需要分配内存或某种形式资源的类型都可以实现 Copy
如下是一些 Copy 的类型:

  • 所有整数类型,比如 u32
  • 布尔类型,bool
  • 所有浮点数类型,比如 f64
  • 字符类型,char
  • 元组,当且仅当其包含的类型也都实现 Copy 的时候。比如,(i32, i32) 实现了 Copy,但 (i32, String) 就没有

http://www.ppmy.cn/devtools/144735.html

相关文章

ArcGIS计算土地转移矩阵

在计算土地转移矩阵时,最常使用的方法就是在ArcGIS中将土地利用栅格数据转为矢量,然后采用叠加分析计算,但这种方法计算效率低。还有一种方法是采用ArcGIS中的栅格计算器,将一个年份的地类编号乘以个100或是1000再加上另一个年份的…

【jvm】主要参数

Java 虚拟机(JVM)有许多参数用于控制其行为和性能,下面是一些 主要的 JVM 启动参数,这些参数通常分为以下几类: 内存管理相关参数 这些参数主要用来配置 JVM 的内存分配策略、堆内存、栈内存等。 -Xms 设置 JVM 启动…

《计算机组成及汇编语言原理》阅读笔记:p28-p47

《计算机组成及汇编语言原理》学习第 3 天,p28-p47 总结,总计 20 页。 一、技术总结 1.Virtual Machine 2.stack 3.The fetch-execute Cycle 在控制单元(Control Unit, CU)里面有一个指令寄存器(Instruction Register, IR)和一个程序计数器(Program…

嵌入的律动,科技的心跳

在微观的世界中,有一种科技的生命以悄然无声的方式运作。它不张扬,却无处不在。嵌入式系统,正是这颗悄然跳动的科技之心。 它在汽车的引擎里点燃动力,让风驰电掣成为可能;它在智能手表中记录生命的律动,让…

挑战一个月基本掌握C++(第六天)了解函数,数字,数组,字符串

一 C函数 函数是一组一起执行一个任务的语句。每个 C 程序都至少有一个函数,即主函数 main() ,所有简单的程序都可以定义其他额外的函数。 您可以把代码划分到不同的函数中。如何划分代码到不同的函数中是由您来决定的,但在逻辑上&#xff…

rust学习: 有用的命令

在学习rust的过程中, 发现一些有用的命令, 记录之. cargo subcommands cargo 子命令(cargo subcommands)是一种有用的机制, 可以对名为cargo-xxx的命令通过 cargo xxx来调用. cargo官方整理了一份cargo subcommands在这里: https://github.com/rust-lang/cargo/wiki/Third-p…

Kubernetes网络管理

Kubernetes 网络管理是 Kubernetes 集群中非常重要的一部分,它涉及到 Pod 之间的通信、服务发现、负载均衡以及与外部网络的连接。以下是 Kubernetes 网络管理的关键概念和组件: 1. Kubernetes 网络模型 Kubernetes 网络模型基于以下几个基本原则&…