所有权
所有的程序都必须和计算机内存打交道,每一个变量都要在内存中存储,在不需要用到的适合,我们需要把这些变量从内存中删除掉,若不及时释放内存,就像一个房子在不断的进人,迟早要被挤爆
在释放内存这方面,编程语言出现了这三种流派:
- 垃圾回收机制(GC),在程序运行时不断寻找不再使用的内存,典型代表:Java、Go
- 手动管理内存的分配和释放, 在程序中,通过函数调用的方式来申请和释放内存,典型代表:C++
- 通过所有权来管理内存,编译器在编译时会根据一系列规则进行检查
rust则是选择的第三种:所有权。
在学习所有权之前,先学习数据的存储位置
1.栈与堆
栈与堆是不同的内存空间,当程序运行时提供存储的地方
1.1 栈 Stack
栈的存储方式类似于一个坑,先进去的就会在坑低,后进去的在坑顶。那么出来的时候只能后进去的先出,先进去的后出
增加数据时称为进栈,移出数据时称为出栈
栈中的所有数据都必须占用已知且固定大小的内存空间,例如整型、浮点型、布尔型,这些基本数据类型都是存储在栈上面
1.2 堆 Heap
对于大小未知,或者可能变化的数据,需要把他们存储在堆上,比如字符串String
当向堆上放入数据时,需要请求一定大小的内存空间。操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的指针,该过程被称为在堆上分配内存,有时简称为 “分配”(allocating)。
接着,该指针会被推入栈中,因为指针的大小是已知且固定的,在后续使用过程中,需要通过栈中的指针,来获取数据在堆上的实际内存位置,进而访问该数据。
2.所有权规则
Rust中所有权遵守以下三个规则
- Rust 中每一个值都被一个变量所拥有,该变量被称为值的所有者
- 一个值同时只能被一个变量所拥有,或者说一个值只能拥有一个所有者
- 当所有者(变量)离开作用域范围时,这个值将被丢弃(drop)
s 变量从声明的点开始直到当前作用域的结束都是有效的,当变量离开作用域后,Rust 会自动调用 drop 函数并清理变量的堆内存。
例如,rust的起点是一个main函数,我们会这样声明一个变量
fn main(){
let num = 123;
}那么此时,在从声明这个变量,再到main函数的},num都是有效的
但是一旦离开了},这个变量就不再有效
我们也可以使用{}单独写一个块作用域
fn main() {
{
let num = 123;
println!("{}", num); // 123
}
// println!("{}", num); // 无效,此处已不是num所处的作用域
}3.转移所有权
3.1 复制 Copy
rust中有个重要的概念是Trait,主要是为类型定义哪些行为
有一个Copy Trait,实现了这个Trait的类型,在发生赋值时,会自动进行按位复制,原变量依然有效。
实现了Copy trait的有以下类型
- 整型、浮点型、布尔型、字符型
- 不可变引用、裸指针
- 元组:当且仅当其包含的所有字段类型都实现
Copy时 - 数组:当且仅当其元素类型实现
Copy时 - 字符串切片
let num = 123;
let num1 = num;
println!("{},{}", num, num1); // 123,123
let b = true;
let b1 = b;
println!("{},{}", b, b1); // true,true
let c = 'A';
let c1 = c;
println!("{},{}", c, c1); // A,A
let t = (11, true, 'B');
let t1 = t;
println!("{:?},{:?}", t, t1); // (11, true, 'B'),(11, true, 'B')3.2 移动 Move
对于在堆上分配的数据类型,如果直接将一个变量赋值给另一个变量,会发生移动。这并非复制数据,而是所有权被转移给了新的变量。之后,原变量将失效,不能再被使用。这样做避免了二次释放的错误。
会发生移动的数据类型有如下:
- 字符串类型:最典型的
String - 集合类型:
Vec、HashMap - 智能指针:
Box<T>(除非T实现了Copy,否则移动其所有权),以及Rc<T>和Arc<T>(它们通过引用计数提供多重所有权,其克隆操作使用clone方法增加计数,而非Copytrait)。 - 自定义结构体或枚举:默认是移动,不可Copy
- 如果其包含的任意一个字段都实现了
Copy且实现了#[derive(Copy, Clone)]特征,那么这个结构体或枚举的赋值是复制 - 只要有一个字段是非Copy类型,例如String,则该结构体就无法实现Copy trait
- 如果其包含的任意一个字段都实现了
let s1 = String::from("hello");
let s2 = s1; // 所有权从 s1 移动到 s2
println!("{}", s2); // hello
// println!("{}", s1); // 值的所有权已被移动,不可再使用3.3 克隆
如果需要深度复制堆上的数据而不仅仅是栈上的指针(胖指针),可以使用clone方法。这会生成数据的完整副本,因此原变量和新变量都持有有效数据
let s1 = String::from("hello");
let s2 = s1.clone(); // 堆上数据被深度复制
println!("s1 = {}, s2 = {}", s1, s2); // 两者都有效4.函数中的所有权
4.1 参数的所有权
将值传递给函数,一样会发生 移动 或者 复制,就跟 let 语句一样,如下面函数的调用
fn get_something(num: u8, str: String) {
println!("{},{}", num, str);
}
fn main() {
let n = 123;
let s = String::from("你好");
get_sonething(n, s);
}上面第9行函数的调用就相当于以下let语句
let num = n;
let str = s;根据上方转移所有权所说,num是整型,赋值时会发生复制,而str是String,赋值时会发生移动,所以在这个函数调用之后在打印n和s试试
fn main() {
let n = 123;
let s = String::from("你好");
get_something(num, str);
println!("{}", n);// 123
println!("{}", s); // s已经被移动,程序会在此处发生错误
}4.2 返回值的所有权
同样的,函数返回值也有所有权,例如:
fn create_hello() -> String {
String::from("hello")
}
fn main() {
let s = create_hello(); // 相当于 let s = String::from("hello");
println!("{}", s); // hello
let s1 = s;
println!("{}", s1); // hello
println!("{}", s); // 此时s已被移动,程序会出错
}那么利用这个返回值,当我们把一个变量传入函数中,仍然想在后面使用这个变量时,就可以在函数中再把这个值返回出去
fn add_world(mut str: String) -> String {
str.push_str("world");
str
}
fn main() {
let s: String = String::from("hello");
let s1 = add_world(s);
println!("{}", s1); // helloworld
}所有权很强大,避免了内存的不安全性,但是也带来了一个新麻烦: 总是把一个值传来传去来使用它。 传入一个函数,很可能还要从该函数传出去,结果就是语言表达变得非常啰嗦,幸运的是,Rust 提供了新功能解决这个问题。就是接下来的借用
