07_rust的所有权

发布时间 2023-10-13 17:45:46作者: UFOFCZ

所有权

所有权是rust最独特、核心的特性,使得rust无GC也可保证内存安全。
其他语言都有在运行时管理自身内存的机制,比如GC,或者程序员手动申请和释放。
rust则采用了第三种方式:所有权

  • 内存通过一所有权系统来管理,含一组编译时用于检测的规则。
  • 当程序运行时,所有权不会影响程序速度。

stack(栈)和heap(堆)内存

stack按值的接收顺序来存储,按相反的顺序移除(先进先出,LIFO),分别是压栈和弹栈。所有存入stack的数据必须已知固定大小。
heap用于存储编译时大小未知运行时才确定大小、及可能发生变化的数据,需代码中手动申请,然后系统找到一块适合大小的空间并返回指针(这个过程叫分配内存),然后存入数据。
所有权解决的问题:

  • 跟踪代码哪些地方在使用heap的哪些数据
  • 清理heap上未使用的数据,避免空间不足
  • 最小化heap上的重复数据

所有权规则

  • 每个值都有一个变量,每个变量是该值的所有者
  • 每个值同时只能有一个所有者
  • 当所有者超出作用域(scope)时,该值将被删除

以string类型为例了解所有权

String类型定义和试用

fn main() {
    let mut s = String::from("Hello"); // 在运行时申请分配相应大小的内存
    s.push_str(" world");
    println("{}", s);
}

rust采用的回收方式:对于某个值,当拥有它的变量离开作用范围是,内存会立即自动交还给操作系统。会自动调用drop函数。

变量和数据的交互方式:移动(Move)

多个变量可与同一个数据使用move方式来交互,如

fn main() {
    let s1 = String::from("Hello");
    let s2 = s1;
}

一个String由3部分组成:一个指向存放字符串内容的内存指针,一个长度,一个容量。
比如s1

string s1: # 以下三个字段构成一个String对象,存放在stack上
  ptr: 0x1234 # 内存首地址
  len: 5 # 存放字符串内容所需的字节数
  capacity: 5 # 存放string从操作系统里获得的总字节数

# 以下是heap上开辟的内存
0x1234:
hello

而执行let s2 = s1时,只会复制一份stack的数据,ptr的值不变,指向heap里的同一个地址。

s1 ==> s2
string s2:
  ptr: 0x1234 # 也指向heap上的hello
  len: 5
  capacity: 5

当离开作用域时,rust会自动调用drop函数,并将变量使用的heap内存释放,当s1、s2离开作用域时,都会尝试释放相同的内存。
在c中,这种现象称之为二次释放(double free)错误。
在rust中为了保证内存安全,执行let s2 = s1后,rust会让s1失效,这样在离开s1作用域时不需再释放ptr指向的内存。
如果还继续使用s1则会编译报错:

fn main() {
    let s1 = String::from("Hello");
    let s2 = s1;
    println!("test {}", s1);
}
/*
编译报错
25 |     let s1 = String::from("Hello");
   |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
26 |     let s2 = s1;
   |              -- value moved here
27 |     println!("test {}", s1);
   |                         ^^ value borrowed here after move

*/

之前有浅拷贝(shallow copy)和深拷贝(deep copy)的概念,
而rust做的一种移动操作,相当于让s2替代了s1,s1失效。

隐含的一个设计原则:rust不会自动创建数据的深拷贝。
如果想深拷贝,实现heap数据拷贝,则使用克隆(Clone)方法。

fn main() {
    let s1 = String::from("Hello");
    let s2 = s1.clone();
    println!("test {}", s1);
}