深入浅出 - Rust 所有权与内存管理机制

一、从变量说起

fn main() {
    // 基本数据类型
    let a = 5;
    let b = a;
    // 指针
    let ptr_a = &a;
    let ptr_b = &b;
    println!("a value = {}", a);
    println!("b value = {}", b);
    println!("ptr_a value = {:p}", ptr_a);
    println!("ptr_b value = {:p}", ptr_b);
}

// 输出
a value = 5
b value = 5
ptr_a value = 0x8bdecff6b0
ptr_b value = 0x8bdecff6b4

在这个例子中,我们定义了变量 a 的值为 5,并且将变量 a 的值赋值给变量 b,接着定义 ptr_a 、ptr_b 分别取变量 a、b 的地址,然后调用 prinltn 函数输出各个变量的值。

思考一个问题:如果不使用变量,直接在输出函数中把数值 5 作为参数、字符串作为参数是否也可以呢?

可以。但是这样的话只能做到静态输出,而不能动态输出。如果需要用户输入了某个数据,需要做加减运算,或者做字符拼接,那么就需要引入变量实现动态处理。

那变量又是如何跟某个数据绑定起来的呢?

程序运行时,数据保存都在内存中,如果我们直接通过内存地址 0x8bdecff6b0 获取到里面的数值 5,那就非常的不方便。因此我们给这个内存地址起了一个别名,称为变量 a,访问变量 a 就是访问内存地址里面的值,从而可以获取数值 5,这样就方便多了。有时候就是想知道数据的内存地址,那么我们可以通过取地址符 & 来获取变量的地址,&a 为取变量 a 的地址 0x8bdecff6b0,而这个地址赋值给了变量 ptr_a ,访问 ptr_a 就是访问变量 a 的地址 0x8bdecff6b0,而不是访问内存地址里面的值 5要想访问值,只能通过解地址符 * 来访问内存地址里面的值,访问 *ptr_a 就是访问变量 a 地址 0x8bdecff6b0 里面的值,即数值 5。

而事实上,这样的解释根本不严谨,因此我们结合栈和堆的概念再深入解析。

二、栈和堆

栈和堆的核心目标就是为程序在运行时提供可供使用的内存空间。

——Rust Course

1、栈 Stack

栈中的所有数据都必须占用已知且固定大小的内存空间,遵循先进后出原则,把变量 a 增加到栈里面称为进栈,把变量 ptr_b 从栈定移除称为出栈在执行函数 main 时,变量依次进栈,程序结束运行时,变量依次出栈,对应的内存释放,从而实现内存回收。

2、堆 Heap

与栈相反,堆中的数据大小空间都是未知的,甚至会发生变化。要想在堆上存储数据,必须向操作系统申请一块能容纳该数据的内存空间,由操作系统返回一个内存地址,该过程也称为在堆上分配内存最后该内存地址进栈,跟某个变量进行绑定

扫描二维码关注公众号,回复: 16975523 查看本文章

3、栈和堆的性能对比 

写入性能 快。入栈时操作系统无需分配新的空间,只需要将新数据放入栈顶即可。 慢。必须先向操作系统申请内存空间后才能存入数据。
读取性能 快。栈数据直接存储在 CPU 高速缓存中,不需要访问内存。 慢。堆数据只能存储在内存中,必须先访问栈再通过栈上的指针来访问内存。

可见,在栈上处理数据是最高效的。

4、拷贝

栈上的数据复制称为浅拷贝(Cpoy),堆上的数据复制称为深拷贝(Clone)。

三、所有权与栈堆

 所有的程序都必须和计算机内存打交道,如何从内存中申请空间来存放程序的运行内容,如何在不需要的时候释放这些空间,成了重中之重,也是所有编程语言设计的难点之一。在计算机语言不断演变过程中,出现了三种流派:

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

其中 Rust 选择了第三种,最妙的是,这种检查只发生在编译期,因此对于程序运行期,不会有任何性能上的损失。

——Rust Course

理解了变量与栈堆原理之后,我们正式步入今天的主题——Rust 所有权与内存管理机制,来看这样一段代码:

fn main() {
    // 复合数据类型
    let x = String::from("hello");
    let y = String::from("world");
    let ptr_x = &x;
    let ptr_y = &y;
    println!("x value = {}", x);
    println!("y value = {}", y);
    println!("ptr_x value = {:p}", ptr_x);
    println!("ptr_y value = {:p}", ptr_y);
}

// 输出
x value = hello
y value = world
ptr_x value = 0x3cddcffa78
ptr_y value = 0x3cddcffa90

根据堆栈知识,从堆上分配了内存空间来存储 hello 字符串,并返回内存地址 0x9bdecff001 ,由变量 x 保存该地址,另一个字符 world 同理。到这里程序能正常运行,我们稍微改动一下代码,把 x 的数据赋值给变量 y:

6 |     let x = String::from("hello");
  |         - move occurs because `x` has type `String`, which does not implement the `Copy` trait
7 |     let y = x;
  |             - value moved here
8 |     println!("x value = {}", x);
  |                              ^ value borrowed here after move

在将变量 x 的值赋给变量 y 后,输出变量 x 的值发生报错,提示在第 7 行代码发生了所有权转移,因为 String 类型的变量 x 没有实现 Copy 方法。我们不管这个 Copy 方法,先解释一下为什么发生了所有权转移。

Rust Course 介绍了所有权原则:

谨记以下所有权原则:

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

——Rust Course

因此,在栈堆中应该是这样的,

1、首先从堆上分配了内存空间来存储 hello 字符串,并返回内存地址 0x9bdecff001 ,由变量 x 保存该地址;

2、执行 let y = x; 后,变量 y 也绑定了变量 x 中的数据内存地址 0x9bdecff001

3、变量 x 不再指向数据内存地址 0x9bdecff001 ,把所有权转移给变量 y 。因此再次访问变量 x 时,因为数据内存地址无效,所以编译报错。

思考一下:为什么变量 x 要把所有权转给变量 y 呢?变量 y 在堆上再复制(Clone)一份 hello 不行吗?

前面提到,在栈上处理数据(Copy)是比堆上高效的,因此可以认为,再复制(Clone)一份 hello 不够高效而且浪费内存;而在回收变量内存时,是通过出栈的方式逐一释放,如果变量 x 跟 y 都指向同一个内存地址,则会导致一个内存地址释放两次,这就导致内存污染。

四、Rust 所有权与内存管理机制

ok,现在让我们来梳理一下以上内容:

1、Rust 通过所有权来管理内存,一个值只能被一个变量绑定拥有

// hello 与 x 绑定
let x = String::from("hello");
// x 将 hello 所有权转移给 y ,hello 与 与绑定
let y = x;

2、Rust 变量在执行时按顺序进栈,程式结束时,依次出栈来释放内存

// 1、x 进栈 4、x 出栈,释放 x
let x = String::from("hello");
// 2、y 进栈 3、y 出栈,释放 y
let y = x;

3、Rust 基本类型在栈上做 Copy ,不会发生所有权转移

// 1 与 x 绑定
let x = 1;
// 栈上 Copy 一份 1 与 y 绑定,此处没有所有权转移
let y = x;

4、Rust 复合类型默认不能在栈上做 Copy,会发生所有权转移

// hello 与 x 绑定
let x = String::from("hello");
// x 将 hello 所有权转移给 y ,hello 与 与绑定
let y = x;

5、Rust 复合类型可以通过在堆上做 Clone 后进栈,不会发生所有权转移

// hello 与 x 绑定
let x = String::from("hello");
// x 将 hello 在堆上 Clone 一份,返回另一份 hello 的内存地址 y 与绑定,此处没有发生所有权转移
let y = x.clone();

6、函数传值与返回同样遵守所有权与栈堆原则

fn main() {
    // hello 与 x 绑定
    let x = String::from("hello");
    // 调用函数时,变量、参数同样也是进栈操作,结束调用时依照出栈次序释放内存
    // 因此,x 是复合类型,不能在栈上做复制,只能将 hello 所有权转移到函数参数 s 
    // 函数返回又将 hello 所有权转移给 y
    let y = move_ownership(x);
    // 后续不能再调用 x

    // 基本类型在调用函数时,变量、参数进栈时都是 Copy ,因此不会发生所有权转移
    let a = 1;
    let b = plus_one(a);
    // 后续可以再调用 a
}

fn move_ownership(s: String) -> String {
    s
}

fn plus_one(x: i32) -> i32 {
    x + 1
}

五、借用与引用

ok,在上面我们已经梳理完 Rust 的所有权与内存管理机制,它贯穿于整个 Rust 语法当中,但是我们总将某个变量的所有权移来移去,非常麻烦,有没有一种机制可以借用某个变量的所有权而不是直接转移所有权呢?没错,这就是 Rust 借用与引用机制。

fn main() {
    // 从堆上分配内存存储 hello 并返回地址指针给 x
    // 实际上 x 为一个结构体,结构体字段为 ptr 指针、len 长度 、cap 容量
    // 变量绑定后,x 的属性为 ptr 指向 hello ,len 为 5 ,cap 为 5
    let x = String::from("hello");
    // 变量 y 借用了 变量 x 的所有属性
    let y = &x;
    // 输出变量 y 的内存地址,y 借用 x 的 len 字段属性
    println!("{:p} {:?}", &y, y.len());
    // 输出变量 x 的内存地址,x 自身持有的 len 字段属性
    println!("{:p} {:?}", &x, x.len());
}

// 输出
0x8a94f9f4f0 5
0x8a94f9f4d8 5

访问变量 y ,实际上是访问变量 x 的地址,从而可以借用到 x 的字段;而 *y 相当于读取变量 x 里面的值,即 ptr 指向的 hello 。

程序运行结束时,按出栈顺序释放内存,回收变量 y 、回收 hello、回收变量 x。

猜你喜欢

转载自blog.csdn.net/weixin_47560078/article/details/128049578