所有权可以说是Rust中最为独特的一个功能了。正是所有权概念和相关工具的引入,Rust才能够在没有垃圾回收机制的前提下保障内存安全。因此,正确地了解所有权概念及其在Rust中的实现方式,对于所有Rust开发者来讲都是十分重要的。在本章中,我们会详细地讨论所有权及其相关功能:借用、切片,以及Rust在内存中布局数据的方式。
所有权就是指一个东西归属谁。Rust 中一个变量对应一个值,变量就称为这个值的所有者。
lex x = 5;
这句话的意思就是,5这个数字所在的内存块的所有者是 x。
所有权规则
- Rust 中的每一个值都有一个对应的变量作为它的所有者
- 在同一时间内,值有且仅有一个所有者
- 当所有者离开自己的作用域时,它持有的值就会被释放掉
变量和数据交互的方式:移动
把一个变量赋值给另一个变量
fn main() {
let x = 5;
let y = x;
println!("x = {}, y = {}", x, y);
let s1 = String::from("hello");
let s2 = s1;
println!("s1 = {}, s2 = {}", s1, s2);
}
这段代码的作用就是 把一个变量赋值给另一个变量。
整数是已知固定大小的简单值,x y 两个值 5 会同时被推入到当前的栈中。
然而,将 s1 赋值给 s2 时,便复制了一次 String 的数据,这意味着我们复制了它存储在栈上的指针、长度及容量字段,没有复制指针指向的堆数据。此时的内存布局应该是这样:

为了确保内存安全,避免复制分配的内存,Rust 在这种场景下回简单的将 s1 废弃,不再视其为一个有效的变量。

也就解释了这段代码执行为什么报错
error[E0382]: borrow of moved value: `s1`
--> src/main.rs:11:34
|
8 | let s1 = String::from("hello");
| -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
9 | let s2 = s1;
| -- value moved here
10 |
11 | println!("s1 = {}, s2 = {}", s1, s2);
| ^^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
总的来说 移动(move) 这种变量和数据交互方式,仅仅是将栈上的数据移动了,堆数据还是同一个,并且将移动前的变量 s1 设置为失效,意味着 s1 和堆数据之间的绑定关系已经不存在了(相当于一个野指针?)。所以移动后在访问 s1 是不被允许的。
变量和数据交互的方式:克隆
let mut s1 = String::from("hello");
// let s2 = s1;
let s2 = s1.clone();
s1.push_str(", world");
println!("s1 = {}, s2 = {}", s1, s2);
克隆 = 深度 copy 堆上的数据。s1、s2 是两个独立的变量,指向的堆数据也不是同一个,所以在 clone 后对 s1 进行修改,s2 还是不变的。
trait :Copy 、Drop
当一种类型拥有了 Copy 这种 trait,那么它的变量就可以在赋值给其他变量之后保持可用性。
如果一种类型实现了 Drop,那么 Rust 就不允许它再实现 Copy。
所有权与函数
将值传递给函数在语义上类似于对变量进行赋值,会触发移动或者复制,就像赋值语句一样。
fn main() {
let x = 5;
show(x);
println!("x = {}", x);
let s1 = String::from("hello");
show_str(s1);
println!("函数外部 s1 = {}", s1)
}
fn show(x: i32) {
println!("函数内部 x = {}", x)
}
fn show_str(s: String) {
println!("函数内部 s1 = {}", s)
}
string 类型变量当做参数传递给函数后,变量的所有权会移动到函数的作用域内,main 函数中就不在持有当前变量的所有权了,所以当调用 show_str 之后在访问 s1就会报错。
从另一个方面来理解,函数执行结束后会销毁变量对应的内存空间,然后在 main 函数中访问一个已经移除调的内存空间报错也是合情合理的。
赋值并不是唯一涉及移动的操作。值在作为参数传递或从函数返回时也会被移动。
error[E0382]: borrow of moved value: `s1`
--> src/main.rs:8:30
|
6 | let s1 = String::from("hello");
| -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
7 | show_str(s1);
| -- value moved here
8 | println!("函数内部 s1 = {}", s1)
| ^^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
引用与借用
如果我们想在函数结束后还想要使用变量,可以将变量的引用传递到参数中。
引用允许在不获取所有权的情况下使用值。
fn main() {
let x = 5;
show(x);
println!("x = {}", x);
let s1 = String::from("hello");
show_str(&s1);
println!("函数外部 s1 = {}", s1)
}
fn show(x: i32) {
println!("函数内部 x = {}", x)
}
fn show_str(s: &String) {
println!("函数内部 s1 = {}", s)
}
修改点
- show_str(&s1);传递参数,传的是 s1 的引用
- fn show_str(s: &String) 修改为 String 的引用类型

&s1,指在不转移所有权的前提下,创建一个指向 s1 值的引用,引用不持有 s1 的所有权,所以在函数结束时,原 s1 仍然可用。
函数签名中的 & 用来表名参数 s 的类型是一个引用。s 并不持有指向值的所有权,在函数结束时不会销毁指向值。
通过引用传递参数给函数的方法也被称为借用。
可变引用
fn main() {
let mut s1 = String::from("hello");
show_str(&mut s1);
println!("函数外部 s1 = {}", s1)
}
fn show_str(s: &mut String) {
println!("函数内部 s1 = {}", s);
s.push_str(", world");
}
mut 表示可变的,变量使用 mut,引用使用 &mut。
通过 mut 关键字将变量或者引用声明为可变的。
可以在函数内部修改其引用变量的值。
一个变量可以有多个不可变引用,但是只能有一个可变引用。
一个变量不能同时有不可变引用和可变引用。
悬垂引用
这类指针指向曾经存在某处内存地址,但该内存已经被释放掉甚至被重新分配另作他用了。
这不就是野指针?
rust 语言中 ,编译器会 确保引用永远不会进入这种悬垂状态。
假如当前持有某一个数据的引用,编译器保证这个数据不会在引用被销毁前离开自己的作用域。
创造一个悬垂引用的例子:
fn main() {
let x = dangle();
println!("x = {}", x)
}
fn dangle() -> &String {
let s = String::from("hello");
return &s;
}
dangle 函数,返回 String 的引用,函数内部,声明了一个字符串 s,返回给调用者 s 的引用,看着貌似没什么问题。
由于变量 s 创建在函数内,所以它会在函数执行完毕时随之释放,但是我们的代码依旧返回指向 s 的引用,这个引用指向的是一个无效的内存地址。
error[E0106]: missing lifetime specifier
--> src/main.rs:6:16
|
6 | fn dangle() -> &String {
| ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime
|
6 | fn dangle() -> &'static String {
| +++++++
切片
切片也是不持有所有权的数据类型。
切片:集合中某一段连续的元素序列,而不是整个集合。
字符串字面量就是切片。
总结
所有权是 Rust 区别于其他编程语言的一个重要部分。
变量称为其对应的内存块的所有者,变量拥有所有权。
所有权发生移动的几个方式
- 变量赋值给其他变量
- 变量当做参数传递到函数
- 函数返回值赋值给变量
作用域可以简单理解为{},大括号内部称为一个作用域。
当变量离开作用域时,变量会自动释放其所有权。
所有权只会发生在堆上分配的数据,栈上分配的数据没有所有权的概念。
