Zj_W1nd's BLOG

Rust?Rust!(2)

2023/12/26

简介

首先,对于那些没有手动分配释放内存的语言例如Java和C#(当然也包括python),它们拥有自动的垃圾收集器,用于寻找和收集没使用的内存,对内存进行管理。而像C这样的语言,程序员使用malloc()free()这样的函数手动进行内存分配和释放。垃圾收集器:(考虑单开一篇写?)
而Rust使用其独特的所有权系统来管理内存。其包含一种在编译时检查的规则,而不在运行时检查,因此没有开销,不会降低程序运行的速度。

在Stack上的数据必须拥有固定的大小,这个课讲的stack和heap很浅显不太涉及操作系统,更多像是数据结构层面的内容。指令在内存中跳转次数越少,访问速度就越快。而且类似于_cdecl方式,函数本地变量压栈结束回复

所有权可以跟踪代码哪些部分正在使用堆上的哪些数据,同时最小化堆上的重复数据量,清理堆上的未使用数据。类似一个编译时运行的堆管理器?“管理堆是所有权存在的原因。”

所有权规则

scope:作用域

  1. 每个值都有一个变量,我们称这个变量是这个值的所有者

  2. 每个值同时只能有一个所有者

  3. 当所有者超出作用域时,该值即被删除

以string类型为例,string类型可以一定程度上代替所有预先定义或自己创建的复杂数据类型。下面看一个简单的例子:

1
2
3
4
5
fn main(){
let mut s= String::from("Hello");//通过from()函数实现申请堆内存(可变大小)
s.push_str(", World");
println!("{}", s);
}//如何将s指向的空间free掉呢?

rust没有GC,因此使用所有权机制来管理内存的释放,而且能避免double free。参考规则3,变量离开作用域的时候,rust会调用drop()函数直接对齐进行释放。

移动Move

首先看这样的代码:

1
2
3
let x=5;
let y=x;
//向栈中压入两个5

而在面对复杂数据类型的时候就不一样了,string类型本身类似于一个结构体:

字段
ptr 指向实际内容
len 字符串长度
capacity 总申请的容量
而聪明的编译器在下面的代码中:
1
2
3
let s1=String::from("hello");
let s2=s1;//s1失效了
//println!("{}",s1)l; <--直接会报错

对s2只复制ptr而不复制整个一份内容,但如果按照先前的简单规则,s2和s1离开作用域的时候都会尝试释放一块内存,直接double free了。显然我们不允许这种情况发生,那么rust
采用的是让s1失效,即将s1“移动”到了s2。s2在这样声明后s1直接就用不了了。如果要想在s2声明后再使用s1,需要调用特定的方法如clone()。UAF达咩。
这里也体现了rust的思想:不自动为任何数据自动创建深拷贝,换言之,就运行时性能而言,任何自动赋值的操作都是廉价的

复制与克隆

1
2
3
4
5
let s1=String::from("hello");
let s2=s1.clone();//s2和s1都有效,复制数据

let x=5;
let y=x;//复制copy

rust真的嗯限制,它有一个trait(接口?)叫做copy能实现我们一般意义上的赋值。但是实现了copy的类型就不会有drop,实现了drop就不允许有copy了。所以不要指望堆上分配的东西能copy了。一般来说简单的标量(整型浮点型boolchar)都可copy,由可copy变量构成的tuple可copy。

函数传参

传值给函数将发生移动复制。将可复制的量传给函数就会发生复制,不可复制的量传给函数就会发生移动,即传完后后面就不能用了。所有权发生转移。

1
2
3
4
5
6
7
fn main(){
let s1=String::from("Hello");
take_ownership(s1);//s1在该代码块这之后就不可用了
let x=5;
make_copy(x);
}
//函数定义

同样,函数返回值也会发生所有权转移。思路和传参类似:

1
2
3
4
5
6
7
fn gives_ownership() -> String{
let s=String::from("Move!");
s//返回值
}
fn main(){
let s1=gives_ownership();
}

s的所有权此时就会转移给s1。综上,所有权的转移遵循这样的模式:

  • 将一个值赋给第二个变量时,所有权就会转移(一般发生移动)

  • 堆变量离开作用域时,rust会调用drop函数对它进行清理除非它的所有权被移动到另一个变量上了。

传递参数但是不给予所有权?

基于上面的理解一种最简单的方法是将参数传进去然后再返回来:

1
2
3
4
5
fn take_and_giveback(s1: String) -> (String, u64) {
let length: u64 = s1.len() as u64;
(s1, length)//用元组的方式传回两个参数,其中s1要传回是因为所有权要交还主函数
//这是纯纯的浪费,所以我们有了引用
}

引用和借用

即不取得所有权而使用值。用&String类型传入参数,这种行为称为借用。借用的变量默认不可修改,想引用修改必须要这样设置:

1
2
3
4
5
6
    let mut s1=String::from("text");//1. 创建的时候变量本身可变
let len=cal_len(&mut s1);//2. 传入的时候声明可变
fn cal_len(s:&mut String)->usize{ //3. 函数参数定义可变
s.push_str("add");
s.len()//返回值
}

而且rust还有这样的限制:

  1. 特定作用域内对某一数据只允许同时存在至多一个可变引用。

  2. 不允许同时存在可变引用和不可变引用。但多个读(不可变引用)可以同时存在。即下面的操作是非法的:

1
2
3
let mut s1=String::from("text");//1. 创建的时候变量本身可变
let s2=&mut s1;
let s3=&mut s1;//非法

防止了数据竞争(数据库?)。要想同时使用两个或以上的可变引用则需要手动控制作用域:

1
2
3
4
5
let mut s1=String::from("text");//1. 创建的时候变量本身可变
{
let s2=&mut s1;
}
let s3=&mut s1;//这样控制作用域时允许的

也就是要么就一个可变引用要么就没有可变引用。
这种类似编译加锁的机制防止UAF存在?真能?

切片slice

这种操作是单独针对字符串(或数组)的,类似python的切片方法,它不获得所有权。

1
2
3
4
let s=String::from("Hello World");
let hello=&s[0..5];//[..5]
let world=&s[6..11];//[6..]
//s=&s[..]

这样的形式称为字符串切片,是指向字符串中一部分内容的引用。它并不copy内容,也是返回一个包含指针的结构体类似的东西,含一个指针一个长度。它的类型是&str,本质是个不可变引用,而const字符串在rust中是&str类型,即一个二进制程序字节的切片。
这样写函数会好:

1
2
3
fn all_type(s:&str)->&str{
//传入String类型就创建整个字符串的切片
}

数组同理&a[1..3]

CATALOG
  1. 1. 简介
  2. 2. 所有权规则
    1. 2.1. 移动Move
    2. 2.2. 复制与克隆
    3. 2.3. 函数传参
    4. 2.4. 传递参数但是不给予所有权?
  3. 3. 引用和借用
  4. 4. 切片slice