所有权-Rust

所有Rust程序都需要管理他们使用计算机内存的方式,主要用于对空闲内存的回收,在Rust编程语言在我们需要考虑代码运行在哪个上面

java的垃圾收集器会自动寻找不再使用的内存,C语言必须自己显示的分配和手动释放内存

栈内存stack

压栈出栈,存储在栈内存上的数据必须拥有固定的大小,可能发生变化的值要放在heap上,操作系统放入数据只需要存放在stack的顶端即可,不需要寻找空间,函数调用过程就相当于压栈,函数执行结束时会弹出

堆内存heap

堆内存存放数据会申请一区域,当存入数据时操作系统会分配这内存中的一部分并返回一个指针,操作系统放入数据需要记录,访问数据需要借助指针,延缓了访问速度

String

1
2
3
4
5
6
7
8
fn main() {

let mut s = String::from("hellow");//::表示from是来自String类型的函数,该类型可以被修改

s.push_str(" World!");

println!("{}",s);
}

image-20241204135013136

String类型存放在heap上,可以动态的修改,字符字面值char因为编译阶段被写入可行性文件里,所以不可修改

可以简单理解为在程序里定义好了一个字符字面值和一个String类型的,程序不运行我们就可以利用winhex等软件在程序中找到字符子面值的具体内容,但是String类型不一定

drop

Rust中当变量走出了自己的作用域,drop函数会删除该内存还给操作系统

在 Rust 的所有权系统中,一个值在任何时刻只能有一个所有者。当一个值被移动到另一个变量时,所有权也随之转移,原来的变量就不能再被使用了,因为它不再拥有那个值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fn main() {

let x=5;// x是一个i32类型的值
let y = x;//执行let y = x;时x的值被复制给了y因为i32是一个基本数值类型所以这里使用的是复制语义,x y各自持有自己的值(都是5)

println!("x:{},y:{}",x,y);

let s1 =String::from("hello");

let s2 = s1;//执行let s2 = s1;时,s1的所有权被移动到了s2。因为String是一个复合类型,所以这里使用的是移动语义。这意味着s1不再拥有原来的值,它的所有权已经转移到了s2

println!("s1:{},s2:{}",s1,s2);

}

image-20241204143935958

在一般语言里s1在释放的时候会销毁指向的具体数值,一旦s2销毁的时候也去销毁指向的数值就会出大问题,rust为了防止这种情况采用了移动所有权的方式,至于具体哪些类型会执行移动,哪些类型会复制取决于这个类型本身是否实现copy trait

image-20241204140226975

Rust在s1移动后只会让指针本身失效,不会操作数据,这就不存在二次销毁内存的操作了

image-20241204145229170

如果真的想把s1的数据复制给s2,可以使用clone方法

image-20241204145409437

image-20241204145429451

存在栈上面的数据不需要考虑如上问题

讨论函数赋值返回值过程中的所有权转移有助于我们理解rust安全机制

函数赋值

函数传参要么是复制要么是移动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fn main() {

let s1 = String::from("hello");

take_owe(s1);//在此以后s1的数值已经给了函数,那么它本身就不在有效果

let n1 =1314;

take_copy(n1);//n1由于是实现了copy trait类型所以复制进去仍然有效


}

fn take_owe(str1:String){
println!("{}",str1);
}

fn take_copy(num1:i32){
println!("{}",num1);
}

image-20241204150432782

返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
fn main() {
// 调用 take_s1_have_value 函数,并接收返回的 String 对象
// take_s1_have_value 函数会创建一个新的 String 对象并返回它
// 这里,s1 获得了这个新创建的 String 对象的所有权
let s1 = take_s1_have_value();

// 创建一个新的 String 对象 "hello",s2 获得了这个对象的所有权
let s2 = String::from("hello");

// 调用 take_own_to_an 函数,并将 s2 的所有权传递给该函数
// 这意味着 s2 不再拥有这个 String 对象,所有权被移动到了 s3
// 函数 take_own_to_an 简单地返回传入的 String 对象,因此 s3 获得了这个对象的所有权
let s3 = take_own_to_an(s2);
}

// take_s1_have_value 函数返回一个 String 类型的对象
// 这个函数创建一个新的 String 对象 "hello" 并返回它
fn take_s1_have_value() -> String {
// 在堆上创建一个新的 String 对象 "hello"
let str1 = String::from("hello");
// 返回这个 String 对象,函数调用结束后,这个对象的所有权会被移动到调用者
str1
}

// take_own_to_an 函数接受一个 String 类型的对象作为参数,并返回一个 String 对象
// 这个函数简单地返回传入的 String 对象
// 由于 Rust 的所有权规则,传入的 String 对象的所有权会被移动到这个函数
// 函数返回时,所有权会再次被移动到返回值,然后传递给调用者
fn take_own_to_an(str1: String) -> String {
// 直接返回传入的 String 对象,所有权会随着返回值移动
str1
}

返回值与作用域

当一个堆数据的变量离开作用域的时候,drop函数就会清理掉这个变量的数据,除非该数据的所有权移动到另一个变量身上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fn main() {

let s1 = String::from("hello");

let (s2,len) = use_not_get(s1);

println!("{},{}",s2,len);

}

fn use_not_get (str1:String) -> (String,usize){
let length = str1.len();
(str1,length)
}

引用与借用

image-20241204154450245

image-20241204154539882

因为s不拥有真实数据的所有权,所以即使它被销毁原来的数据也不会消失,

当被引用的变量为可变变量的时候那我们可以借助这个引用来修改原有数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fn main() {

let mut s1 = String::from("hello");

let len = use_not_get(&mut s1);

println!("s1 is {},len is {}",s1,len);

}

fn use_not_get (str1:&mut String) -> usize{
str1.push_str(" world!");
str1.len()
}

特定作用域内可变引用只能有一个,避免数据竞争,不可以同时存在可变引用和不可变引用,

不在一个作用域可以创建多个可变引用,在同一个作用域可以存在多个不可变引用,读写不能同时存在,可以有多个读

悬空指针

指曾经有效,现在无效的指针,Rust 的所有权和生命周期规则旨在防止悬空指针的出现。如果代码遵守 Rust 的借用规则,编译器会保证不会出现悬空指针

字符串切片是指向字符串其中一部分的引用,不获取值所有权

形式,切片值包含开始索引,不包括结束索引

[开始索引..结束索引]

[..结束索引]

[开始索引..]

1
2
3
4
5
6
7
8
9
10
fn main() {

let s = String::from("hello world");

let hello = &s[0..6];//0 1 2 3 4 5
let world = &s[6..11];//6 7 8 9 10

println!("s value {}{}",hello,world);

}

image-20241206104445661

image-20241206105906631

&str

字符串字面值是切片类型

image-20241206110348148

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1. 类型和所有权:
• 字符串切片 &str):是一个对字符串数据的不可变引用,它指向一段有效的 UTF-8 编码序列。&str本身不拥有数据,它只是指向数据。
• 字符串引用(&String):是对String类型的可变引用String是一个可增长的、可变的字符串类型,它拥有自己的数据。
2. 生命周期:
• 字符串切片 (&str):通常与特定的生命周期(lifetime)相关联,这意味着它只能存活到其引用的数据还有效的时候。例如,如果你从一个变量中创建了一个&str,那么这个&str必须在该变量的生命周期内有效。
• 字符串引用(&String):不需要特定的生命周期,因为它是一个对String的引用,而String管理自己的生命周期。
3. 可变性:
• 字符串切片(&str):总是不可变的,即使你从一个 String中创建它,你也不能通过这个切片改变String的内容。
• 字符串引用(&String):可以是可变的(&mut String),这意味着你可以通过这个引用改变String的内容。
4. 操作:
• 字符串切片(&str):可以用于执行不需要修改字符串的操作,如查找子串、比较等。
• 字符串引用(&String):可以用于执行需要修改字符串的操作,如添加字符、删除字符、修改内容等。
5. 内存管理:
• 字符串切片(&str):不涉及内存分配,因为它只是对现有数据的引用。
• 字符串引用(&String):涉及到内存分配,因为String可能会增长,需要动态分配内存。