前提:Rust 所有权规则是针对堆内存数据而言的。
一、栈内存(Stack) & 堆内存(Heap)
在栈上使用内存存取数据一般说成“压栈出栈”。
分配内存一般是指在堆内存上查找一块足够大的空间标记为已用,来存放数据。
使用变量保存一个堆内存的数据,底层是变量本身存放在栈上,变量的一个内部属性(ptr)的值指向数据在堆内存上的地址。
如下图所示:
1.为什么说 Rust 所有权是针对堆内存数据
栈内存的数据在变量赋值的过程中是直接深拷贝整个值,底层是是闲了 Copy trait 的。实现了 Copy trait 的数据类型一般有如下这些:
- 所有整型类型,如
u32
- 所有浮点类型数据,如
f64
- 布尔类型,值两种
true
&false
- 字符类型,
char
- 元组,当且仅当其包含的类型都是 Copy trait 类型的时候,如:(u32, f64, false) ,但是(i32, String) 就不是,String 数据是存放在堆上的,不是 Copy trait 类型。
2.Copy trait 类型数据的赋值
let x = 5;
let y = x;
数字 5 默认是 i32 类型,实现了 Copy trait 类型。把 x 赋值给 y 的时候就是直接 x 的值 5 重新深考本一份赋值给 y,这个时候栈上就有两个 5。
3.非 Copy trait 类型数据的赋值
let s1 = String::from("hello");
let s2 = s1;
s1 的数据类型是 String 类型,未实现 Copy trait,并且 s1 的值是放在堆内存上,把 s1 赋值给 s2 的时候只是把变量 s1 置成未初始化状态,并把 s2 的的指针指向“hello”字符串在堆内存上的地址。
变量 s1 的内存结构:
变量 s2 的内存结构:
这里灰色的 **s1 **变量已经不可用了。
二、所有权转移的时机
所有的权的转移发生在以下地方:
- 变量赋值语句
- 给函数参数赋值
- 函数返回值
三、所有权规则
- Rust 中的每一个值都有一个被称为其所有者(owner)的变量。
- 值在任一时刻有且只有一个所有者。
- 当所有者(变量)离开作用域,这个值将被丢弃。
let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1); // 这里会发生编译报错
当所有者超出作用域时,该值将被删除(即s1的所有权通过变量赋值语句后转给了s2,s1将被销毁不可访问)。
s1 分配给 s2 之后,s1 已经释放掉了(这里 s1 是存在于栈上的一个指向堆上的内存地址),示意图如下:
四、引用与借用
1.引用
引用的写法就是在变量前面添加一个&
符号,只获取变量的指针而不获取变量的所有权。
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("‘{}’ 字符串的长度是:{}", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
上面代码中包含的变量 s、**s1 **以及字符串“hello”的内存结构图:
函数参数 **s **指向另一个变量 s1 上的指针,s1 上的指针指向的是字符串“hello”在堆内存上的地址。
从这个示意图上就可以知道函数参数 s 指向的是变量 s1 的指针,并不具备变量 s1 的所有权。
2.解引用
解引用就是引用的相反操作,它使用解引用的运算符*****
3.引用规则
- 在任意给定时间,要么 只能有一个可变引用,要么 只能有多个不可变引用。
- 引用必须总是有效的。
4.不可变引用 & 可变引用
引用的规则用于解决数据竞争和悬垂引用。
悬垂引用就是堆上的数据已经被销毁了,还保留着这个堆内存上的地址。
- 不可变引用
就是只能读取数据,不能操作数据,不具备所有权。
fn main() {
let s = String::from("hello");
let s1 = &s;
let s2 = &s;
println!("{}", s);
println!("{}", s1);
println!("{}", s2);
}
// 这里不能给 s1 或者 s2 使用 push_str() 方法修改字符串数据,否则报错
- 可变引用
可变引用就是能修改源数据,但是还是不具备所有权。
在变量前面添加关键字mut
标记为一个可变变量,同时创建可变引用的地方使用&mut
标记创建另一个可变引用。
fn main() {
let mut s = String::from("hello");
change(&mut s);
println!("{}", s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world.");
}
1. 注意声明了一个变量的可变引用后,就不能再访问这个变量之前定义的其它引用(可变和非可变引用),否则报错。
2. 声明同一个变量的可变引用后不能再声明这个变量的不可变引用,否则也报错。
可变引用规则:
(1)借用者的作用域不能超过所有者的作用域。
(2)不可变引用的数量不受限制,可变引用只能存在一个。
(3)所有者可以拥有可变引用或不可变引用,但不能同时拥有两者。
(4)所有引用必须有效(不能为空)。
5.借用(borrowing)
借用就是给函数参数传递引用的过程。
也就是说函数的参数是一个引用类型的,在调用这个函数的时候把一个引用的变量传递给这个函数参数。
fn main() {
let s = String::from("hello");
// 函数的参数 some_string 借用变量 s,得到是一个 s 的引用
let len = get_string_length(&s);
println("{len}");
}
fn get_string_length(some_string: &String) -> usize {
some_string.len()
}
五、slice 切片
slice 就是集合的切片
slice 就是一个切面,就是集合中的一段连续的片段子集。
fn main() {
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
}
上面这段代码对应的内存结构示意图:
[m…n] 获取集合的切片
下面中括号的语法就是指定一个范围的 slice。
- [m…n] 左闭右开
- [m…=n] 包含两个端点值
- […] 即从0到最大索引,整个集合元素的片段
- [m…] 从 m 开始到集合结尾的片段
- […m] 从0开始到 m -1 索引序号为止的集合元素片段
这里面也可以使用变量,如:
fn main() {
let s = String::from("hello world");
let len = s.len();
let world = s[6..len];
}
注意:索引必须位于有效的 UTF-8 字符边界内,如果尝试从一个多字节字符的中间位置创建字符串 slice,则程序将会因错误而退出,多字节的字符如表情字符:🥳。
字符串字面量
字符串字面量就是 **slice**
,字符串字面量的类型就是**&str**
类型。
程序中直接创建一个字符串字面量的数据是直接编码在二进制代码中,在 Rust 中,字符串字面量(例如 “hello world”)是存储在静态内存区域,而不是栈或堆上。静态内存区域是在程序编译时分配的,并且在整个程序的生命周期内都存在。