本篇博客我们来解释几个名词,作用域所有权所有权移动

栈是在代码运行时,可供使用的一块内存。它的存取数据方式是先进后出,或者说后进先出。想象有一个箱子,你往里放本子,最先放入的本子,是在箱子底下,当你要使用本子时,总是从顶上取一个使用,也就是取最后放入的一个本子。
因为这种存取数据时总是在栈顶操作,而不需要去内存中寻找一个位置,所以栈的操作是时分迅速的。
还有一个点是,存在栈里的数据,都是以知的固定大小。这一点的意思是,例如要让用户输入一个名字,因为不知道用户会输入多少字符,所以这个数据就无法放在栈中,因为无法事先知道明确的大小。

在编译时大小未知或大小可能变化的数据,要改为存储在堆上。堆是缺乏组织的:当向堆放入数据时,你要请求一定大小的空间。操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的 指针(pointer)。这个过程称作 在堆上分配内存(allocating on the heap),有时简称为 分配(allocating)

作用域

作用域可以理解为一个东西在程序中的有效范围。对于Rust来说,当一个变量出了作用域后,对应的内存就会自动被释放掉,变量变为无效状态。

1
2
3
4
{                      // s 在这里无效, 它尚未声明
let s = "hello"; // 从此处起,s 是有效的
// 使用 s
} // 此作用域已结束,s 不再有效

字符串类型 String

之前在数据类型一节,没有讲到 String,是因为牵扯到堆栈的问题,所以放在这里讲。

1
2
3
4
5
6
7
8
9
10
11
fn main() {
// 像这种直接硬编码在代码里的字符串,是放在栈上的,并且不可改变
let name = "Jack";

// 使用String::from创建的,是在堆上分配内存,并且是可以改变的
let mut my_name = String::from("Jack");
my_name.push_str(", My name is Jack");

// 输出 Jack, My name is Jack
println!("{}", my_name);
}

当调用 String::from 时,它的实现 (implementation) 请求其所需的内存。这在编程语言中是非常通用的。

所有权

  1. Rust 中每一个值都有一个被称为所有者的变量
  2. 值,有且只有一个所有者
  3. 当所有者(变量)离开作用域时,这个值被丢弃,内存被释放

移动

先看下面一段代码

1
2
3
4
5
6
7
8
9
fn main() {
let x = 10;
let y = x;
println!("x: {}, y:{}", x, y);

let name1 = "Fred";
let name2 = name1;
println!("name1: {}, name2: {}", name1, name2);
}

很正常,最后输出了 x: 10, y:10name1: Fred, name2: Fred

再看下面这段代码

1
2
3
4
5
6
7
8
9
10
fn main() {
let name1 = String::from("Fred");
println!("name1: {}", name1);

let name2 = name1;
println!("name2: {}", name2);

// 编译出错,这句会出错
println!("name1 again: {}", name1);
}

为什么加了最后一句会编译出错呢,这里涉及到一个概念,移动。首先 name1 指向的值是分配在堆上的。当将 name1 赋值 给 name2后,在有一些编程语言,两个变量会指向同一块堆内存区域,但是对于Rust来说,不是这样的,Rust在这里会直接让 name1 失效,避免两个指针指向同一块堆内存。因为 Rust 会自动释放内存,这样可以避免当两个变量超出作用域时,导致重复的内存释放问题。将 name1 赋值给 name2,这个操作叫做移动,name1移动到了name2,移动后,name1自动失效,所以最后一句访问 name1 会编译出错。

更详细的内容 官方文档

这里要记住,对于那些固定大小的数据类型,i32, f32, boolchar 等不会存在移动的问题。但是对于存储在上的数据,不管是String还是后面自定义的数据类型,这样的操作都会触发移动

有没有办法将指上堆内存的变量赋值给另一个变量不触发移动呢?有!方法就是克隆,看下面的代码。

1
2
3
4
5
6
7
8
9
fn main() {
let name1 = String::from("Fred");
println!("name1: {}", name1);

let name2 = name1.clone();
println!("name2: {}", name2);

println!("name1 again: {}", name1);
}

和之前的代码只有第5行变了,当调用了 clone()函数后,会导致 name1 指向的堆上的内存复制一份。所以这里就没有移动。String内部实现了 clone(),当我们自定义数据结构时,如果要有克隆功能,需要自己实现 clone()方法。这个后面会讲到。

移动与函数

说完了移动,就需要说一下移动和函数相关的东西。如果将一个值作为参数,去调用一个函数,如果这个值是在栈上,那么不会发生什么,但是如果这个值是分配在堆上,那么它会移动到函数内部。

看下面的代码(注意看代码的注释)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fn main() {
let name1 = String::from("Fred");
println!("name1: {}", name1);

// name1 的值移动了函数里
takes_ownership((name1));

// name1 已经无效,这里再使用就会编译出错
// println!("name1 again: {}", name1);

}

fn takes_ownership(str: String) {
println!("i have ownership: {}", str);
}

下面的代码,函数在结束时将 所有权 返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn main() {
let name1 = String::from("Fred");
println!("name1: {}", name1);

// 因为name1不是mut的,所以这里的name1相当于创建了一个
// 新的变量name1, 本质上并不是之前的
let name1 = takes_and_gives_back(name1);
println!("name1 again: {}", name1);
}

fn takes_and_gives_back(str: String) -> String {
println!("i have ownership: {}", str);

// 这里将值返回,所有权移出函数
str
}

使用函数时每次都要转移所有权很繁琐,所下一节将介绍引用,不用每次将所有权转来转去