|
|
|
@ -23,17 +23,17 @@ int* foo() {
|
|
|
|
|
} // 变量a和c的作用域结束
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
这段代码虽然可以编译通过,但是其实非常糟糕,变量 `a` 和 `c` 都是局部变量,函数结束后将局部变量 `a` 的地址返回,但局部变量 `a` 存在栈中,在离开作用域后,`a` 所申请的栈上内存都会被系统回收,从而造成了 `悬空指针(Dangling Pointer)` 的问题。这是一个非常典型的内存安全问题,虽然编译可以通过,但是运行的时候会出现错误, 很多编程语言都存在。
|
|
|
|
|
这段代码虽然可以编译通过,但是其实非常糟糕,变量 `a` 和 `c` 都是局部变量,函数结束后将局部变量 `a` 的地址返回,但局部变量 `a` 存在栈中,在离开作用域后,`a` 所申请的栈上内存都会被系统回收,从而造成了 `悬空指针(Dangling Pointer)` 的问题。这是一个非常典型的内存安全问题,虽然编译可以通过,但是运行的时候会出现错误,很多编程语言都存在。
|
|
|
|
|
|
|
|
|
|
再来看变量 `c`,`c` 的值是常量字符串,存储于常量区,可能这个函数我们只调用了一次,也可能我们不再会使用这个字符串,但 `"xyz"` 只有当整个程序结束后系统才能回收这片内存。
|
|
|
|
|
|
|
|
|
|
所以内存安全问题,一直都是程序员非常头疼的问题,好在, 在 Rust 中这些问题即将成为历史,因为 Rust 在编译的时候就可以帮助我们发现内存不安全的问题,那 Rust 如何做到这一点呢?
|
|
|
|
|
所以内存安全问题,一直都是程序员非常头疼的问题,好在,在 Rust 中这些问题即将成为历史,因为 Rust 在编译的时候就可以帮助我们发现内存不安全的问题,那 Rust 如何做到这一点呢?
|
|
|
|
|
|
|
|
|
|
在正式进入主题前,先来一个预热知识。
|
|
|
|
|
|
|
|
|
|
## 栈(Stack)与堆(Heap)
|
|
|
|
|
|
|
|
|
|
栈和堆是编程语言最核心的数据结构,但是在很多语言中,你并不需要深入了解栈与堆。 但对于 Rust 这样的系统编程语言,值是位于栈上还是堆上非常重要, 因为这会影响程序的行为和性能。
|
|
|
|
|
栈和堆是编程语言最核心的数据结构,但是在很多语言中,你并不需要深入了解栈与堆。 但对于 Rust 这样的系统编程语言,值是位于栈上还是堆上非常重要,因为这会影响程序的行为和性能。
|
|
|
|
|
|
|
|
|
|
栈和堆的核心目标就是为程序在运行时提供可供使用的内存空间。
|
|
|
|
|
|
|
|
|
@ -49,11 +49,11 @@ int* foo() {
|
|
|
|
|
|
|
|
|
|
与栈不同,对于大小未知或者可能变化的数据,我们需要将它存储在堆上。
|
|
|
|
|
|
|
|
|
|
当向堆上放入数据时,需要请求一定大小的内存空间。操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的**指针**, 该过程被称为**在堆上分配内存**,有时简称为 “分配”(allocating)。
|
|
|
|
|
当向堆上放入数据时,需要请求一定大小的内存空间。操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的**指针**,该过程被称为**在堆上分配内存**,有时简称为 “分配”(allocating)。
|
|
|
|
|
|
|
|
|
|
接着,该指针会被推入**栈**中,因为指针的大小是已知且固定的,在后续使用过程中,你将通过栈中的**指针**,来获取数据在堆上的实际内存位置,进而访问该数据。
|
|
|
|
|
|
|
|
|
|
由上可知,堆是一种缺乏组织的数据结构。想象一下去餐馆就座吃饭: 进入餐馆,告知服务员有几个人,然后服务员找到一个够大的空桌子(堆上分配的内存空间)并领你们过去。如果有人来迟了,他们也可以通过桌号(栈上的指针)来找到你们坐在哪。
|
|
|
|
|
由上可知,堆是一种缺乏组织的数据结构。想象一下去餐馆就座吃饭:进入餐馆,告知服务员有几个人,然后服务员找到一个够大的空桌子(堆上分配的内存空间)并领你们过去。如果有人来迟了,他们也可以通过桌号(栈上的指针)来找到你们坐在哪。
|
|
|
|
|
|
|
|
|
|
#### 性能区别
|
|
|
|
|
|
|
|
|
@ -74,12 +74,12 @@ int* foo() {
|
|
|
|
|
|
|
|
|
|
> 1. Rust 中每一个值都被一个变量所拥有,该变量被称为值的所有者
|
|
|
|
|
> 2. 一个值同时只能被一个变量所拥有,或者说一个值只能拥有一个所有者
|
|
|
|
|
> 3. 当所有者(变量)离开作用域范围时,这个值将被丢弃(drop)
|
|
|
|
|
> 3. 当所有者(变量)离开作用域范围时,这个值将被丢弃(drop)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#### 变量作用域
|
|
|
|
|
|
|
|
|
|
作用域是一个变量在程序中有效的范围, 假如有这样一个变量:
|
|
|
|
|
作用域是一个变量在程序中有效的范围,假如有这样一个变量:
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
let s = "hello";
|
|
|
|
@ -106,7 +106,7 @@ let s = "hello";
|
|
|
|
|
- **字符串字面值是不可变的**,因为被硬编码到程序代码中
|
|
|
|
|
- 并非所有字符串的值都能在编写代码时得知
|
|
|
|
|
|
|
|
|
|
例如,字符串是需要程序运行时,通过用户动态输入然后存储在内存中的,这种情况,字符串字面值就完全无用武之地。 为此,Rust 为我们提供动态字符串类型: `String`, 该类型被分配到堆上,因此可以动态伸缩,也就能存储在编译时大小未知的文本。
|
|
|
|
|
例如,字符串是需要程序运行时,通过用户动态输入然后存储在内存中的,这种情况,字符串字面值就完全无用武之地。 为此,Rust 为我们提供动态字符串类型: `String`,该类型被分配到堆上,因此可以动态伸缩,也就能存储在编译时大小未知的文本。
|
|
|
|
|
|
|
|
|
|
可以使用下面的方法基于字符串字面量来创建 `String` 类型:
|
|
|
|
|
|
|
|
|
@ -139,7 +139,7 @@ let y = x;
|
|
|
|
|
|
|
|
|
|
这段代码并没有发生所有权的转移,原因很简单: 代码首先将 `5` 绑定到变量 `x`,接着**拷贝** `x` 的值赋给 `y`,最终 `x` 和 `y` 都等于 `5`,因为整数是 Rust 基本数据类型,是固定大小的简单值,因此这两个值都是通过**自动拷贝**的方式来赋值的,都被存在栈中,完全无需在堆上分配内存。
|
|
|
|
|
|
|
|
|
|
整个过程中的赋值都是通过值拷贝的方式完成(发生在栈中),因此并不需要所有权转移。
|
|
|
|
|
整个过程中的赋值都是通过值拷贝的方式完成(发生在栈中),因此并不需要所有权转移。
|
|
|
|
|
|
|
|
|
|
> 可能有同学会有疑问:这种拷贝不消耗性能吗?实际上,这种栈上的数据足够简单,而且拷贝非常非常快,只需要复制一个整数大小(`i32`,4 个字节)的内存即可,因此在这种情况下,拷贝的速度远比在堆上创建内存来得快的多。实际上,上一章我们讲到的 Rust 基本类型都是通过自动拷贝的方式来赋值的,就像上面代码一样。
|
|
|
|
|
|
|
|
|
@ -204,7 +204,7 @@ For more information about this error, try `rustc --explain E0382`.
|
|
|
|
|
|
|
|
|
|
> 1. Rust 中每一个值都被一个变量所拥有,该变量被称为值的所有者
|
|
|
|
|
> 2. 一个值同时只能被一个变量所拥有,或者说一个值只能拥有一个所有者
|
|
|
|
|
> 3. 当所有者(变量)离开作用域范围时,这个值将被丢弃(drop)
|
|
|
|
|
> 3. 当所有者(变量)离开作用域范围时,这个值将被丢弃(drop)
|
|
|
|
|
|
|
|
|
|
如果你在其他语言中听说过术语 **浅拷贝(shallow copy)** 和 **深拷贝(deep copy)**,那么拷贝指针、长度和容量而不拷贝数据听起来就像浅拷贝,但是又因为 Rust 同时使第一个变量 `s1` 无效了,因此这个操作被称为 **移动(move)**,而不是浅拷贝。上面的例子可以解读为 `s1` 被**移动**到了 `s2` 中。那么具体发生了什么,用一张图简单说明:
|
|
|
|
|
|
|
|
|
@ -243,7 +243,7 @@ println!("s1 = {}, s2 = {}", s1, s2);
|
|
|
|
|
|
|
|
|
|
这段代码能够正常运行,说明 `s2` 确实完整的复制了 `s1` 的数据。
|
|
|
|
|
|
|
|
|
|
如果代码性能无关紧要,例如初始化程序时或者在某段时间只会执行寥寥数次时,你可以使用 `clone` 来简化编程。但是对于执行较为频繁的代码(热点路径),使用 `clone` 会极大的降低程序性能,需要小心使用!
|
|
|
|
|
如果代码性能无关紧要,例如初始化程序时或者在某段时间只会执行寥寥数次时,你可以使用 `clone` 来简化编程。但是对于执行较为频繁的代码(热点路径),使用 `clone` 会极大的降低程序性能,需要小心使用!
|
|
|
|
|
|
|
|
|
|
#### 拷贝(浅拷贝)
|
|
|
|
|
|
|
|
|
@ -271,7 +271,7 @@ Rust 有一个叫做 `Copy` 的特征,可以用在类似整型这样在栈中
|
|
|
|
|
- 所有浮点数类型,比如 `f64`
|
|
|
|
|
- 字符类型,`char`
|
|
|
|
|
- 元组,当且仅当其包含的类型也都是 `Copy` 的时候。比如,`(i32, i32)` 是 `Copy` 的,但 `(i32, String)` 就不是
|
|
|
|
|
- 不可变引用 `&T` ,例如[转移所有权](#转移所有权)中的最后一个例子,**但是注意: 可变引用 `&mut T` 是不可以 Copy的**
|
|
|
|
|
- 不可变引用 `&T` ,例如[转移所有权](#转移所有权)中的最后一个例子,**但是注意:可变引用 `&mut T` 是不可以 Copy的**
|
|
|
|
|
|
|
|
|
|
## 函数传值与返回
|
|
|
|
|
|
|
|
|
|