Rust知识篇05-所有权和借用

Rust知识篇05-所有权和借用

Rust 万众瞩目就是因为其内存安全性

在以往,内存安全都是通过 GC 的方式实现,但 GC 会引来性能、内存占用以及 Stop the world 等问题,在高性能场景和系统编程上不可接受

Rust 采用了与 ( 不 ) 众 ( 咋 ) 不 ( 好 ) 同 ( 学 )的方式**所有权系统**


所有权系统

程序就必须和计算机内存打交道

从内存中申请空间来存放程序的运行内容在不需要的时候释放这些空间,成了重中之重,也是所有编程语言设计的难点。

目前大致分为三种流派

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

Rust属于第三种,**通过所有权来管理内存,**在编译期进行检查,程序运行期则不会有任何性能上的损失。可以说是一个妙笔。

👉 c悬空指针

🍎悬空问题

先看一段c语言之中的代码,存在内存安全问题,造成悬空指针(Dangling Pointer) 的问题

javascript 复制代码
int* foo() {
    int a;           // 变量a的作用域开始
    a = 100;
    char *c = "xyz"; // 变量c的作用域开始
    return &a;
}  // 变量a和c的作用域结束

上面的代码之中

&a 返回的是局部变量 a 的地址。由于 a 是局部变量,它的生命周期仅限于函数 foo 内部。当 foo 函数返回时,a 的内存空间会被销毁,因此返回的指针 &a 指向的是一个已经被释放的内存区域,指向一个已经不存在的内存区域。这就导致了一个问题------返回一个悬空指针(dangling pointer),指向的内存已经不再有效。

🍎解决方法

c之中如何解决这种问题呢,大致分为下面这三种

javascript 复制代码
//返回静态变量或全局变量
int* foo() {
    static int a;  // 静态变量a,生命周期延长至程序结束
    a = 100;
    return &a;  // 返回静态变量的地址是安全的
}

// 动态分配内存
int* foo() {
    int *a = malloc(sizeof(int));  // 动态分配内存
    *a = 100;
    return a;  // 返回动态分配的内存地址
}

// 传入外部指针
void foo(int *a) {
    *a = 100;  // 将值赋给外部传入的指针
}

栈(Stack)与堆(Heap)

对于 Rust 这样的系统编程语言,值是位于栈上还是堆上非常重要,直接影响程序的行为和性能

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

👉栈 (Stack)

后进先出

栈按照顺序存储值并以相反顺序取出值。

想象一下叠盘子:当增加更多盘子时,把它们放在盘子顶部,当需要盘子时,再从顶部拿走。

不能从中间也不能从底部增加或拿走盘子!

增加数据叫做进栈 ,移出数据则叫做出栈

所以,栈中的所有数据都必须占用已知且固定大小的内存空间,假设数据大小是未知的,那么在取出数据时,你将无法取到你想要的数据。

👉堆(Heap)

与栈不同,对于大小未知或者可能变化的数据,我们需要将它存储在堆上。

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

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

堆是一种缺乏组织的数据结构

想象一下去餐馆就座吃饭:进入餐馆,告知服务员有几个人,然后服务员找到一个够大的空桌子(堆上分配的内存空间)并领你们过去。如果有人来迟了,他们也可以通过桌号(栈上的指针)来找到你们坐在哪。

性能区别

在栈上分配内存比在堆上分配内存要快,因为入栈时操作系统无需进行函数调用(或更慢的系统调用)来分配新的空间,只需要将新数据放入栈顶即可。

相比之下,在堆上分配内存则需要更多的工作,操作系统必须首先找到一块足够存放数据的内存空间,接着做一些记录为下一次分配做准备,如果当前进程分配的内存也不足时,还需要进行系统调用来申请更多内存。 因此,处理器在栈上分配数据会比在堆上分配数据更加高效。

所有权与堆栈

当你的代码调用一个函数时,传递给函数的参数(包括可能指向堆上数据的指针和函数的局部变量)依次被压入栈中,当函数调用结束时,这些值将被从栈中按照相反的顺序依次移除。

堆上的数据缺乏组织,因此跟踪这些数据何时分配和释放是非常重要的,否则堆上的数据将产生内存泄漏 ------ 这些数据将永远无法被回收。

这就是Rust 所有权系统为我们提供的强大保障

在 Rust 中明白堆栈的原理必不可少

所有权原则

先看看所有权规则

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

作用域是一个变量在程序中有效的范围

javascript 复制代码
{   // s 在这里无效,它尚未声明
    let s = "hello";   // 从此处起,s 是有效的
    // 使用 s
}  // 此作用域已结束,s不再有效

变量绑定

转移所有权
javascript 复制代码
fn main() {
    // 由于 loop_forever() 永不返回,下面的代码将无法执行到
    let x = 5;
    let y = x;
    println!("x {} y!{}",x,y);
}

// 输出 5 5 

这段代码并没有发生所有权的转移

首先将 5 绑定到变量 x,接着拷贝 x 的值赋给 y,最终 xy 都等于 5,因为整数是 Rust 基本数据类型,是固定大小的简单值,因此这两个值都是通过自动拷贝的方式来赋值的,都被存在栈中,完全无需在堆上分配内存。

整个过程中的赋值都是通过值拷贝的方式完成(发生在栈中),因此并不需要所有权转移。

对于基本类型(存储在栈上),Rust 会自动拷贝,但是 String 不是基本类型,而且是存储在堆上的,因此不能自动拷贝。

🍎 二次释放问题

String 类型是一个复杂类型,由存储在栈中的堆指针字符串长度字符串容量共同组成

String 类型指向了一个堆上的空间,这里存储着它的真实数据

一个值只允许有一个所有者 ,但是现在这个值(堆上的真实字符串数据)有了两个所有者:s1s2

当变量离开作用域后,Rust 会自动调用 drop 函数并清理变量的堆内存。不过由于两个 String 变量指向了同一位置。这就有了一个问题:当 s1s2 离开作用域,它们都会尝试释放相同的内存。这是一个叫做 二次释放(double free) 的错误,也是之前提到过的内存安全性 BUG 之一。两次释放(相同)内存会导致内存污染,它可能会导致潜在的安全漏洞。

🍎Rust 如何解决这种问题呢

**s1** 被赋予 **s2** 后,Rust 认为 **s1** 不再有效,因此也无需在 **s1** 离开作用域后 **drop** 任何东西,这就是把所有权从 **s1** 转移给了 **s2** **s1**** 在被赋予 **s2** 后就马上失效了**。

javascript 复制代码
let s1 = String::from("hello");
let s2 = s1;
print!("{} ---{}", s1, s2);
javascript 复制代码
fn main() {
    let s1 = String::from("hello");
    let s2 = s1;
    print!("我是s2-{}", s2);
}

我们可以看到报错

javascript 复制代码
// 报错信息如下:

  --> src\main.rs:25:9
   |
25 |     let s2 = s1;
   |         ^^ help: if this is intentional, prefix it with an underscore: `_s2`
   |
   = note: `#[warn(unused_variables)]` on by default

error[E0382]: borrow of moved value: `s1`
  --> src\main.rs:26:23
   |
24 |     let s1 = String::from("hello");
   |         -- move occurs because `s1` has type `String`, 
     which does not implement the `Copy` trait
25 |     let s2 = s1;
   |              -- value moved here
26 |     print!("我是s1-{}", s1);
   |                         ^^ value borrowed here after move
   |
   = note: this error originates in the macro `$crate::format_args`
     which comes from the expansion of the macro `print` 
       (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
   |
25 |     let s2 = s1.clone();
   |                ++++++++

For more information about this error, try `rustc --explain E0382`.
warning: `nexusrust` (bin "nexusrust") generated 1 warning
error: could not compile `nexusrust` (bin "nexusrust") due to 1 previous error; 
1 warning emitted

因为这个时候s1已经被抛弃,有效的是s2。s2输出正常

javascript 复制代码
fn main() {
    let s1 = String::from("hello");
    let s2 = s1;
    print!("我是s2-{}", s2);
}

//正常输出
我是s2-hello

现在就可以理解为什么这个操作被称为 移动(move),而不是浅拷贝

s1 不再指向任何数据,只有 s2 是有效的,当 s2 离开作用域,它就会释放内存。

此刻我们就明白了为什么 Rust 称呼 let a = b变量绑定了,如图

拷贝(浅拷贝)

浅拷贝只发生在栈上,因此性能很高,在日常编程中,浅拷贝无处不在。

Rust 有一个叫做 Copy 的特征,可以用在类似整型这样在栈中存储的类型。如果一个类型拥有 Copy 特征,一个旧的变量在被赋值给其他变量后仍然可用,也就是赋值的过程即是拷贝的过程。

Rust定义了一些可以Copy的通用规则

任何基本类型的组合可以 **Copy** ,不需要分配内存或某种形式资源的类型是可以 **Copy** 。如下是一些 Copy 的类型:

  • 所有整数类型,比如 u32
  • 布尔类型,bool,它的值是 truefalse
  • 所有浮点数类型,比如 f64
  • 字符类型,char
  • 元组,当且仅当其包含的类型也都是 Copy 的时候。比如,(i32, i32)Copy 的,但 (i32, String) 就不是
  • 不可变引用 &T ,例如转移所有权中的最后一个例子,但是注意:可变引用 **&mut T** 是不可以 Copy的

函数传值与返回

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

下面的函数之中s迁移到了takes_ownership这个函数之中。值已经被迁移走,所以会报错。

javascript 复制代码
fn main() {
    let s = String::from("hello");  // s 进入作用域

    takes_ownership(s); // s 的值移动到函数里 ...
    // ... 所以到这里不再有效
    println!("{}---ssss2", s);  

} // 这里, x 先移出了作用域,然后是 s。但因为 s 的值已被移走,
  // 所以不会有特殊操作

fn takes_ownership(some_string: String) { // some_string 进入作用域
    println!("{}", some_string);
} // 这里,some_string 移出作用域并调用 `drop` 方法。占用的内存被释放

// 报错
 borrow of moved value: `s`
  --> src\main.rs:28:28
   |
24 | let s = String::from("hello");  // s 进入作用域
   |  - move occurs because `s` has type `String`,
     which does not implement the `Copy` trait
25 |
26 |takes_ownership(s); // s 的值移动到函数里 ...
   |       - value moved here
27 | // ... 所以到这里不再有效
28 | println!("{}---ssss2", s);
   |     ^ value borrowed here after move

下面i32Copy 的,可以继续使用

javascript 复制代码
fn main() {
    let s = String::from("hello");  // s 进入作用域

    takes_ownership(s); // s 的值移动到函数里 ...
    // ... 所以到这里不再有效
    println!("{}---ssss2", s);  


    let x: i32 = 5; // x 进入作用域
    makes_copy(x);// x 应该移动函数里,
    println!("{}---xxxx2", x);
    // 但 i32 是 Copy 的,所以在后面可继续使用 x

} // 这里, x 先移出了作用域,然后是 s。但因为 s 的值已被移走,
  // 所以不会有特殊操作

fn takes_ownership(some_string: String) { // some_string 进入作用域
    println!("{}", some_string);
} // 这里,some_string 移出作用域并调用 `drop` 方法。占用的内存被释放

fn makes_copy(some_integer: i32) { // some_integer 进入作用域
    println!("{}", some_integer);
} // 这里,some_integer 移出作用域。不会有特殊操作


// 输出
hello
5
5---xxxx2
相关推荐
程序视点32 分钟前
2023最新HitPaw免注册版下载:一键去除图片视频水印的终极教程
前端
fured1 小时前
[调试][实现][原理]用Golang实现建议断点调试器
开发语言·后端·golang
bobz9651 小时前
linux cpu CFS 调度器有使用 令牌桶么?
后端
bobz9652 小时前
linux CGROUP CPU 限制有使用令牌桶么?
后端
小只笨笨狗~2 小时前
el-dialog宽度根据内容撑开
前端·vue.js·elementui
weixin_490354342 小时前
Vue设计与实现
前端·javascript·vue.js
David爱编程2 小时前
多核 CPU 下的缓存一致性问题:隐藏的性能陷阱与解决方案
java·后端
追逐时光者2 小时前
一款基于 .NET 开源、功能全面的微信小程序商城系统
后端·.net
烛阴3 小时前
带你用TS彻底搞懂ECS架构模式
前端·javascript·typescript
绝无仅有3 小时前
Go 并发同步原语:sync.Mutex、sync.RWMutex 和 sync.Once
后端·面试·github