07_rust的所有权

所有权

所有权是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);
}

Copy trait

stack上的数据:复制,无需考虑深拷贝、浅拷贝的问题,也就没有移动失效的问题。

fn main() {
    let x = 5;
    let y = x;
    println!("test {} {}", x, y);
}

Copy trait:trait可简单理解为接口,可用于像整数这些完全存放在stack上的数据类型,如果一个数据类型实现了Copy trait,则旧的变量在赋值后仍然可用。
如果一个类型或者该类型的一部分实现了Drop trait,则Rust不允许它再实现Copy trait。

一些拥有Copy trait的类型

  • 任何简单标量的组合类型都可Copy
  • 任何需要分配内存或某种资源的都不可Copy

一些拥有Copy trait的类型:

  • 所有的整数类型,如u32
  • bool
  • char
  • 所有浮点类型,如f64
  • Tuple(元组,如果其所有字段都是Copy的),如(i32, i32)是,(i32, String)则不是

所有权和函数

  • 参数
    在语义上,将值传递给函数和把值赋给变量是类似的,将值传递给函数将发生移动或复制。
    如果参数类型是String等类型,则Move操作,如果是i32类型,则Copy操作。

  • 返回值
    在返回值的过程中也同样会发生所有权的转移。

一个变量的所有权总是遵循同样的模式:

  • 赋值过程发生Move/Copy。
  • 当包含heap数据的变量离开作用域时,值会被drop函数清理,除非数据的所有权移动到了另一个变量上。
    如果想保留所有权,则可使用引用的方式。
posted @ 2023-10-13 17:35  00lab  阅读(7)  评论(0编辑  收藏  举报