本篇博客我们来解释几个名词,栈
、堆
、作用域
、所有权
、所有权移动
栈
栈是在代码运行时,可供使用的一块内存。它的存取数据方式是先进后出
,或者说后进先出
。想象有一个箱子,你往里放本子,最先放入的本子,是在箱子底下,当你要使用本子时,总是从顶上取一个使用,也就是取最后放入的一个本子。
因为这种存取数据时总是在栈顶操作,而不需要去内存中寻找一个位置,所以栈的操作是时分迅速的。
还有一个点是,存在栈里的数据,都是以知的固定大小。这一点的意思是,例如要让用户输入一个名字,因为不知道用户会输入多少字符,所以这个数据就无法放在栈中,因为无法事先知道明确的大小。
堆
在编译时大小未知或大小可能变化的数据,要改为存储在堆上。堆是缺乏组织的:当向堆放入数据时,你要请求一定大小的空间。操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的 指针(pointer)
。这个过程称作 在堆上分配内存(allocating on the heap)
,有时简称为 分配(allocating)
。
作用域
作用域可以理解为一个东西在程序中的有效范围。对于Rust来说,当一个变量出了作用域后,对应的内存就会自动被释放掉,变量变为无效状态。
{ // s 在这里无效, 它尚未声明
let s = "hello"; // 从此处起,s 是有效的
// 使用 s
} // 此作用域已结束,s 不再有效
字符串类型 String
之前在数据类型一节,没有讲到 String,是因为牵扯到堆栈的问题,所以放在这里讲。
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) 请求其所需的内存。这在编程语言中是非常通用的。
所有权
- Rust 中每一个值都有一个被称为
所有者
的变量 - 值,有且只有一个所有者
- 当所有者(变量)离开作用域时,这个值被丢弃,内存被释放
移动
先看下面一段代码
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:10
和 name1: Fred, name2: Fred
再看下面这段代码
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
,bool
,char
等不会存在移动
的问题。但是对于存储在堆
上的数据,不管是String还是后面自定义的数据类型,这样的操作都会触发移动
有没有办法将指上堆内存的变量赋值给另一个变量不触发移动
呢?有!方法就是克隆,看下面的代码。
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()
方法。这个后面会讲到。
移动与函数
说完了移动
,就需要说一下移动和函数相关的东西。如果将一个值作为参数,去调用一个函数,如果这个值是在栈上,那么不会发生什么,但是如果这个值是分配在堆上,那么它会移动到函数内部。
看下面的代码(注意看代码的注释)
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);
}
下面的代码,函数在结束时将 所有权
返回
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
}