什么是所有权

Rust 的所有权,是 Rust 语言的一个核心概念。可以简单理解为,一种内存管理的方式。用现实中的东西举例,当你从图书管借了一本书时,这本书的所有权暂时归你所有,而当你把书给你的朋友时,此刻,书的所有权归你的朋友所有,而当你的朋友把书归还图书馆时,此时没有人拥有书的所有权,相当于内存释放。

在使用有自动垃圾回收(GC)的编程语言时,我们并不需要考虑内存的释放问题,因为GC会帮我们释放。Rust是无GC的语言,一个变量占用的内存什么时候释放,由它的所有权决定,简单来说,当所有权所在的作用域结束时,内存将被释放。

什么是作用域

Rust 的作用域和其他编程语言中的作用域概念是一样的,我们使用下面的代码说明

1
2
3
4
5
6
7
8
9
10
11
12
13
// 整个 main 函数是一个作用域
fn main()
{
let a = 10;

// 下面的花括号内,也是一个作用域
{
let s = "hello";
println!("{}", s);
}
// 下面这句再次打印s,会编译出错,因为s所在的作用域已经结束,s 被释放掉了
//println!("s again: {}", s);
}

关于堆内存和栈内存

我们知道,内存分为堆和栈。存在栈上的数据,必须是已知固定大小的数据。而存在堆上的数据,都是在编译时不知道大小的数据,例如用户自己输入的数据。栈比堆的访问快很多,这是因为栈的存取结构,都是操作栈顶,不需要去内存中找数据。而要将数据存在堆上,则需要向操作系统申请,由操作系统在内容中找到一块能够容纳你要存的数据大小的内存空间,然后将内存空间的指针返回给你。访问堆内存上的数据,都是要通过指针,找到指向的内存,然后再读取内存中的数据。

哪些数据是存在栈上,哪些是存在堆上

1
2
3
4
let x = 10;
let y = "hello";
let c = 'A';
let x2 = x;

像上面这些简单的数据类型,都是存在于栈上,对于 Rust 而言,整型,浮点型,布尔型,字符型等,都是存在于栈上。而对于Rust的 String,这种可变大小的数据类型,是存在于堆上的。看下面的代码

1
2
let s1 = String::from("hello");
let s2 = "hello".to_string();

上面的代码 from 就是从 &str 创建一个 String 类型,两行代码是两种方式实现同样的效果。一个 String 在内存中的形式如下图

p003001_string-pointer

指针 s1 是存在于栈上的,而它指向的内存,也就是存储 “hello” 的内存空间,则是在堆上的。

所有权和所有权转移

当我们给一个变量赋值,也就是将一个值,绑定到一个变量上,那这个变量,就拥有了这个值的所有权

当我们把一个变量赋值给另一个变量时,就像上面代码中的 let x2 = x;,因为 x 上的值是固定大小,存在于栈上的,所以相当于 x 的值直接拷贝了一份赋给了 x2。但是对于存在于堆上的数据,这样的赋值,在 Rust 中将会导致所有权转移。看下面的代码

1
2
let s1 = String::from("hello");
let s2 = s1;

我们知道,s1 和 s2 都是指针,指向了堆上的一块内存。对于像C/C++这类编程语言,这样无非是两个指针指向同一块内存,并不会有什么问题。但是对于Rust而方,”hello” 的所有权,会移动到 s2,在这之后,s1 将无效,也就是在 let s2 = s1 后再次访问 s1,将会编译出错。

下面代码会编译出错

1
2
3
let s1 = string::from("hello");
let s2 = s1;
println!("{}", s1); // 这一句导致编译出错

我们来看一下将 s1 赋值给 s2 后,内存情况。在赋值给 s2 后,s1 变为无效状态,不能再访问。

p003002_string-pointer

对于将一个变量作为参数传给一个函数时,也会触发所有权转移,看下面的代码

1
2
3
4
5
6
7
8
9
10
11
fn main(){
let my_name = String::from("fred");
print_name(my_name);

// 下面这句会编译出错
//println!("{}", my_name);
}

fn print_name(name: String) {
println!("the name is : {}", name);
}

上面的代码在调用了函数 print_name 后,”fred” 的所有权,就会移动到参数 name 上,my_name 变为无效状态,所以在函数调用之后,再次访问 my_name 会编译出错。而当函数结束时,name 的作用域结束,它所指向的内存,就会被释放。