Skip to content

所有权

所有的程序都必须和计算机内存打交道,每一个变量都要在内存中存储,在不需要用到的适合,我们需要把这些变量从内存中删除掉,若不及时释放内存,就像一个房子在不断的进人,迟早要被挤爆

在释放内存这方面,编程语言出现了这三种流派:

  • 垃圾回收机制(GC),在程序运行时不断寻找不再使用的内存,典型代表:Java、Go
  • 手动管理内存的分配和释放, 在程序中,通过函数调用的方式来申请和释放内存,典型代表:C++
  • 通过所有权来管理内存,编译器在编译时会根据一系列规则进行检查

rust则是选择的第三种:所有权。

在学习所有权之前,先学习数据的存储位置

1.栈与堆

栈与堆是不同的内存空间,当程序运行时提供存储的地方

1.1 栈 Stack

栈的存储方式类似于一个坑,先进去的就会在坑低,后进去的在坑顶。那么出来的时候只能后进去的先出,先进去的后出

增加数据时称为进栈,移出数据时称为出栈

栈中的所有数据都必须占用已知且固定大小的内存空间,例如整型、浮点型、布尔型,这些基本数据类型都是存储在栈上面

1.2 堆 Heap

对于大小未知,或者可能变化的数据,需要把他们存储在堆上,比如字符串String

当向堆上放入数据时,需要请求一定大小的内存空间。操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的指针,该过程被称为在堆上分配内存,有时简称为 “分配”(allocating)。

接着,该指针会被推入中,因为指针的大小是已知且固定的,在后续使用过程中,需要通过栈中的指针,来获取数据在堆上的实际内存位置,进而访问该数据。

2.所有权规则

Rust中所有权遵守以下三个规则

  1. Rust 中每一个值都被一个变量所拥有,该变量被称为值的所有者
  2. 一个值同时只能被一个变量所拥有,或者说一个值只能拥有一个所有者
  3. 当所有者(变量)离开作用域范围时,这个值将被丢弃(drop)

s 变量从声明的点开始直到当前作用域的结束都是有效的,当变量离开作用域后,Rust 会自动调用 drop 函数并清理变量的堆内存。

例如,rust的起点是一个main函数,我们会这样声明一个变量

rust
fn main(){
    let num = 123;
}

那么此时,在从声明这个变量,再到main函数的},num都是有效的

但是一旦离开了},这个变量就不再有效

我们也可以使用{}单独写一个块作用域

rust
fn main() {
    {
        let num = 123;
        println!("{}", num);        // 123
    }
    // println!("{}", num);  // 无效,此处已不是num所处的作用域
}

3.转移所有权

3.1 复制 Copy

rust中有个重要的概念是Trait,主要是为类型定义哪些行为

有一个Copy Trait,实现了这个Trait的类型,在发生赋值时,会自动进行按位复制,原变量依然有效。

实现了Copy trait的有以下类型

  • 整型、浮点型、布尔型、字符型
  • 不可变引用、裸指针
  • 元组:当且仅当其包含的所有字段类型都实现 Copy
  • 数组:当且仅当其元素类型实现 Copy
  • 字符串切片
rust
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
  • 集合类型:VecHashMap
  • 智能指针:Box<T>(除非 T实现了 Copy,否则移动其所有权),以及 Rc<T>Arc<T>(它们通过引用计数提供多重所有权,其克隆操作使用 clone方法增加计数,而非 Copytrait)。
  • 自定义结构体或枚举:默认是移动,不可Copy
    • 如果其包含的任意一个字段都实现了 Copy且实现了#[derive(Copy, Clone)]特征,那么这个结构体或枚举的赋值是复制
    • 只要有一个字段是非Copy类型,例如String,则该结构体就无法实现Copy trait
rust
let s1 = String::from("hello");
let s2 = s1; // 所有权从 s1 移动到 s2

println!("{}", s2); // hello

// println!("{}", s1); // 值的所有权已被移动,不可再使用

3.3 克隆

如果需要深度复制堆上的数据而不仅仅是栈上的指针(胖指针),可以使用clone方法。这会生成数据的完整副本,因此原变量和新变量都持有有效数据

rust
let s1 = String::from("hello");
let s2 = s1.clone(); // 堆上数据被深度复制
println!("s1 = {}, s2 = {}", s1, s2); // 两者都有效

4.函数中的所有权

4.1 参数的所有权

将值传递给函数,一样会发生 移动 或者 复制,就跟 let 语句一样,如下面函数的调用

rust
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语句

rust
let num = n;
let str = s;

根据上方转移所有权所说,num是整型,赋值时会发生复制,而str是String,赋值时会发生移动,所以在这个函数调用之后在打印n和s试试

rust
fn main() {
    let n = 123;
    let s = String::from("你好");

    get_something(num, str);
    println!("{}", n);// 123
    println!("{}", s); // s已经被移动,程序会在此处发生错误
}

4.2 返回值的所有权

同样的,函数返回值也有所有权,例如:

rust
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已被移动,程序会出错
}

那么利用这个返回值,当我们把一个变量传入函数中,仍然想在后面使用这个变量时,就可以在函数中再把这个值返回出去

rust
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 提供了新功能解决这个问题。就是接下来的借用

MIT Licensed