C4 Rust所有权
所有权
- 所有权是rust最独特的特性,它让rust无需GC(垃圾收集器)就可以保证内存安全。
什么是所有权
- rust的核心特性就是所有权
- 所有程序在运行时都必须管理他们使用计算机内存的方式
- 有些语言有垃圾收集机制,在程序运行时,他们会不断寻找不再使用的内存
- 在其他语言中,程序员需要显式的分配和释放内存,比如c或者c++
- Rust采用第三种方式
- 内存通过一个所有权系统来管理,其中包含一组编译器在编译时检查的规则
- 当程序运行时,所有权特性不会减慢程序的运行速度
栈内存stack和堆内存heap
- 在rust中使用stack和heap有很大的不同
- stack和heap都是可用的内存,但是结果很不相同
| 对比 | stack(栈) | heap(堆) | 备注 |
|---|---|---|---|
| 存储数据 | 按值得接收顺序存储,按照相反的顺序移除(后进先出,LIFO, last in first out) | ||
| 添加数据:入栈,移除数据:出栈 | |||
| 所有存储在stack上得数据必须拥有已知得固定的大小 | 编译时大小未知或者运行时大小可能发生变化的数据必须放在heap上 | ||
| 指针是已知固定大小,可以把指针存放在stack上 | |||
| heap内存组织性差一些,会请求一定数量的空间 | |||
| 把值压入stack上不叫分配 | 在heap上进行分配:操作系统在heap中找到一块足够大的空间,把它标记为再用,并返回一个指针,也就是这个空间的地址 | ||
| 把数据压到stack上比在heap上分配快的多 | 在stack上,操作系统不需要寻找用来存储新数据的空间,位置用于在stack的顶端 | 在heap上分配空间需要更多工作:操作系统首先需要找到一个足够大的空间来存放数据,然后要做好记录方便下次分配 | |
| 访问数据 | 访问heap的数据比stack的要慢,因为通过指针才能找到heap中的数据 | 对于现代的处理器,由于缓存的缘故,如果指令在内存中跳转的次数越少,速度越快 | |
| 处理速度 | 数据存放距离近,处理速度快->stack上 | 数据之间距离较远,处理速度慢->heap上 | heap上分配大量的空间也是需要时间的 |
| 函数调用 | 代码调用函数时,值被传入到函数(也包含指向heap的指针)。函数本地的变量被压到stack上,当函数结束时,这些值会从stack上弹出 |
所有权存在的原因
- 所有权解决的问题
- 跟踪代码的哪些部分在使用heap的哪些数据
- 最小化heap上的重复数据量
- 清理heap上未使用的数据以避免空间不足
- 一旦懂了所有权就不用经常想stack和heap了
- 管理heap数据是所有权存在的原因
所有权规则/内存与分配
所有权规则
- 每个值都有一个变量,这个变量是该值的所有者
- 每个值只能有一个所有者
- 当所有者超出作用域(scope)时,该值将被删除
变量作用域
String类型
-
基本概念
- 字符串类型,能够存储在编译时未知数量的文本
- 存储在堆上->用于研究String类型是如何回收的
- 字符串字面值:程序中手写的"ssss",他们是不可变的。因为两者的内存分配方式不同
-
如何创建String类型的值
- 使用from函数
rust// ::表示from是String类型下的函数 let mut s = String::from("hell0"); s.push_str(", world!"); -
变量与数据的交互方式
-
移动 Move
- 对于存在堆上的数据,String的移动如下:
这个s2=s1只是单纯的赋值了栈 上的ptr,len和capacity
rustlet s1 = String::from("hell0"); // 这个s2=s1只是单纯的赋值了栈上的ptr,len和capacity let s2 = s1; println!("{}",s2); // s1已经失效了,编译器会自动报错 println!("{}",s1);
- 类似浅拷贝
- rust不会自动创建数据的深拷贝
- 对于存在堆上的数据,String的移动如下:
-
克隆 Clone(针对heap上的数据)
- 如果真的像对heap上的String数据进行深度拷贝,可以使用Clone方法
rustfn main() { let s1 = String::from("hello"); let s2 = s1.clone(); println!("{}", s2); println!("{}", s1); }- clone是将stack和heap的数据都做了个copy ,比较消耗资源

-
复制(针对stack上的数据)
- 原因:x是整数类型,在编译期间就确认了大小,数据存储在stack中,复制操作非常快速
rustfn main() { let x = 2; let y = x; // 这里x和y都有值,都有效 println!("{}, {}", x,y); }- copy trait
- 如果一个类型实现了copy trait,旧的变量在复制后仍然可用
- 如果一个类型或者该类型的一部分实现了Drop trait,rust旧不允许它再去实现copy trait
- 一些用于copy trait的类型
- 任何简单的标量的组合类型都是可以copy的
- 所有整数,bool, char,浮点类型
- Tuple,如果所有字段都是可copy的
- (i32,i32)是
- (i32, String)不是
-
所有权与函数
-
将值传递给函数,要不会发生复制要不会发生移动
- 将String值传给函数也会发生Move,原始值也会报错

- 将String值传给函数也会发生Move,原始值也会报错
-
函数在返回值的过程中同样也会发生所有权的转移
s1只是为了获取到s的所有权,当s进去到函数并且函数执行完成后s就被销毁了

- 一个变量的所有权遵循的模式:
-
把一个值赋给其他变量是会发生 move/copy(heap相关的变量会move,stack相关的会copy)
-
当一个包含heap数据的变量离开作用域,它的值就会被drop函数清理(或者数据的所有权移动到另一个变量上)
-
看个例子
rust// 观察caculate_length1和caculate_length2有什么不同? fn caculate_length1(s: String) -> (String, usize) { // 这里只是借用,下面会提到 let len =s.len(); return (s, len); } fn caculate_length2(s: String) -> (String, usize) { // 会编译报错,显示s.len()会报错,s已经被move,所有s.len()没办法执行 return (s, s.len()); }
-
- 一个变量的所有权遵循的模式:
-
如何让函数使用某个值,但不获得其所有权(我的理解是函数用了某个值,但是这个值在函数结束后任然可以正常被调用)?
- 使用引用
引用与借用
引用
-
实现效果对比
- 没有使用引用
rustfn main() { let s1 = String::from("hello"); let (s2, length) = caculate_length(s1); println!("s2: {}, length: {}", s2, length); } fn caculate_length(s: String) -> (String, usize) { let len = s.len(); return (s, len); }- 使用引用
rustfn main() { let s1 = String::from("hello"); let length = caculate_length(&s1); println!("Length: {}", length); } fn caculate_length(s: &String) -> usize { return s.len(); }- & 表示引用:允许引用某些值而不取得其所有权
s = &s1
借用
- 把引用作为函数参数的行为叫借用???->&s1
- 是否可以修改借用的东西?
- 不行
- 引用默认情况下是不可变的 ->可以切换为可变引用

可变引用
-
变成可变引用后代码如下
rustfn main() { let mut s1 = String::from("hello"); let length = caculate_length(&mut s1); println!("Length: {}", length); } fn caculate_length(s: &mut String) -> usize { s.push_str(", world"); return s.len(); }
可变引用的限制
-
在特定作用域,对某一块数据,只能有一个可变的引用
- 编译时就可以防止数据竞争

- 以下三种情况会引发数据竞争
- 两个或者多个指针访问同一个数据
- 至少有一个指针用于写入数据
- 没有使用任何机制来同步对数据的访问
- 编译时就可以防止数据竞争
-
可以通过创建新的作用域,来允许非同时的创建多个可变引用

-
不可以同时拥有一个可变引用和一个不可变引用

-
多个不可变引用是可以的
悬空引用(Dangling References)
- 悬空指针:一个指针引用了内存的某个地址,而这块内催可能已经被释放或者分配给其他人使用了。
- 在rust中编译器可保证引用永远不是悬空引用

引用的规则
- 在任何给定时刻,只能满足下列条件之一:
- 一个可变的引用
- 任意数量的不可变引用
- 引用必须一直有效
切片
- 切片 slice :不持有所有权的数据类型
- 问题:编写一个函数
- 接收字符串作为参数
- 返回在这个字符串中找到的第一个单词
- 如果函数没有找到空格,那么整个字符串就被返回 // 找到空格位置或者整个字符串长度
rust
// 初级版本: // 找到空格位置或者整个字符串长度
fn main() {
let mut s1 = String::from("he llo ");
let index = get_first_word_index(&s1);
println!("the first word index is {}", index);
}
fn get_first_word_index(str: &String) -> usize{
// turn to byte
let bytes = str.as_bytes();
// bytes.iter().enumerate() 将bytes转化成turple类型
// i表示index,item表示具体的值
for (i, &item) in bytes.iter().enumerate(){
// b' ' 表示一个空字符的byte值
if item == b' '{
return i;
}
}
return str.len();
}
- 上述存在的问题,index 是不随s1的变化而变化的(我觉得很正常)
字符串切片
-
字符串切片是指向字符串中一部分内容的引用
-
形式:[开始索引 ... 结束索引] 表示[开始,结束)左开右闭
-
存储两个内容:指针(指向起始元素位置)和length
rustfn main() { let mut s1 = String::from("he llo "); let first = &s1[0..2]; let last = &s1[3..6]; // 输出he println!("the first word is {}", first); // 输出llo println!("the last word is {}", last); } -
语法糖:一些简写
语法糖 原 现 从初始值开始 &s1[0...2] &s1[...2] 到末尾值结束 &s1[3...s1.len()] &s1[3...] 整个字符串 &s1[0...s1.len()] &s1[...] -
范围索引必须发生在有效的UTF-8字符边界内。
rust
// 进化版本: // 返回第一个单词
fn main() {
let mut s1 = String::from("he llo ");
let index = get_first_word_index(&s1);
// 加这一行会报错,因为在上一行已经是不可变的借用,所以这里不能改成可变的借用
s1.clear();
println!("the first word index is {}", index);
}
// 切片的类型是&str, &str是不可变的
fn get_first_word_index(str: &String) -> &str{
// turn to byte
let bytes = str.as_bytes();
// bytes.iter().enumerate() 将bytes转化成turple类型
// i表示index,item表示具体的值
for (i, &item) in bytes.iter().enumerate(){
// b' ' 表示一个空字符的byte值
if item == b' '{
return &str[..i];
}
}
return &str[..];
}
-
将字符串切片作为参数传递
- 将
rustfn first_word(s: &String)->&str{}转化为
rustfn first_word(s: &str)->&str{}- 在使用String时,直接创建一个完整的String切片来调用该函数
其他类型的切片

2026/1/6