@ -37,9 +37,9 @@ int* foo() {
#### 栈
栈按照顺序存储值并以相反顺序取出值,这也被称作 ** 后进先出**。想象一下一叠盘子:当增加更多盘子时,把它们放在盘子堆的顶部,当需要盘子时,再从顶部拿走。不能从中间也不能从底部增加或拿走盘子!
栈按照顺序存储值并以相反顺序取出值,这也被称作**后进先出**。想象一下一叠盘子:当增加更多盘子时,把它们放在盘子堆的顶部,当需要盘子时,再从顶部拿走。不能从中间也不能从底部增加或拿走盘子!
增加数据叫做 ** 进栈**,移出数据则叫做 ** 出栈**。
增加数据叫做**进栈**,移出数据则叫做**出栈**。
因为上述的实现方式,栈中的所有数据都必须占用已知且固定大小的内存空间,假设数据大小是未知的,那么在取出数据时,你将无法取到你想要的数据。
@ -48,9 +48,9 @@ int* foo() {
与栈不同,对于大小未知或者可能变化的数据,我们需要将它存储在堆上。
当向堆上放入数据时,需要请求一定大小的内存空间。操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的 ** 指针**, 该过程被称为 ** 在堆上分配内存**,有时简称为 “分配”( allocating) 。
当向堆上放入数据时,需要请求一定大小的内存空间。操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的**指针**, 该过程被称为**在堆上分配内存**,有时简称为 “分配”( allocating) 。
接着,该指针会被推入 ** 栈** 中,因为指针的大小是已知且固定的,在后续使用过程中,你将通过栈中的 ** 指针**,来获取数据在堆上的实际内存位置,进而访问该数据。
接着,该指针会被推入**栈**中,因为指针的大小是已知且固定的,在后续使用过程中,你将通过栈中的**指针**,来获取数据在堆上的实际内存位置,进而访问该数据。
由上可知,堆是一种缺乏组织的数据结构。想象一下去餐馆就座吃饭: 进入餐馆,告知服务员有几个人,然后服务员找到一个够大的空桌子(堆上分配的内存空间)并领你们过去。如果有人来迟了,他们也可以通过桌号(栈上的指针)来找到你们坐在哪。
@ -68,7 +68,7 @@ int* foo() {
因为堆上的数据缺乏组织,因此跟踪这些数据何时分配和释放是非常重要的,否则堆上的数据将产生内存泄漏 —— 这些数据将永远无法被回收。这就是 Rust 所有权系统为我们提供的强大保障。
对于其他很多编程语言,你确实无需理解堆栈的原理,但是 ** 在 Rust 中,明白堆栈的原理,对于我们理解所有权的工作原理会有很大的帮助**。
对于其他很多编程语言,你确实无需理解堆栈的原理,但是**在 Rust 中,明白堆栈的原理,对于我们理解所有权的工作原理会有很大的帮助**。
@ -146,20 +146,20 @@ let s2 = s1;
```
此时,可能某个大聪明(善意昵称)已经想到了:嗯,把 `s1` 的内容拷贝一份赋值给 `s2` ,实际上,并不是这样。之前也提到了,对于基本类型(存储在栈上), Rust 会自动拷贝,但是 `String` 不是基本类型,而且是存储在堆上的,因此不能自动拷贝。
实际上, `String` 类型是一个复杂类型,由 ** 存储在栈中的堆指针**、 ** 字符串长度**、 ** 字符串容量**共同组成,其中 ** 堆指针**是最重要的,它指向了真实存储字符串内容的堆内存,至于长度和容量,如果你有 Go 语言的经验,这里就很好理解:容量是堆内存分配空间的大小,长度是目前已经使用的大小。
实际上, `String` 类型是一个复杂类型,由**存储在栈中的堆指针**、**字符串长度**、**字符串容量**共同组成,其中**堆指针**是最重要的,它指向了真实存储字符串内容的堆内存,至于长度和容量,如果你有 Go 语言的经验,这里就很好理解:容量是堆内存分配空间的大小,长度是目前已经使用的大小。
总之 `String` 类型指向了一个堆上的空间,这里存储着它的真实数据, 下面对上面代码中的 `let s2 = s1` 分成两种情况讨论:
1. 拷贝 `String` 和存储在堆上的字节数组
如果该语句是拷贝所有数据(深拷贝),那么无论是 `String` 本身还是底层的堆上数据,都会被全部拷贝,这对于性能而言会造成非常大的影响
2. 只拷贝 `String` 本身
这样的拷贝非常快,因为在 64 位机器上就拷贝了 `8字节的指针` 、`8字节的长度`、`8字节的容量`,总计 24 字节,但是带来了新的问题,还记得我们之前提到的所有权规则吧?其中有一条就是: ** 一个值只允许有一个所有者**,而现在这个值(堆上的真实字符串数据)有了两个所有者:`s1` 和 `s2` 。
这样的拷贝非常快,因为在 64 位机器上就拷贝了 `8字节的指针` 、`8字节的长度`、`8字节的容量`,总计 24 字节,但是带来了新的问题,还记得我们之前提到的所有权规则吧?其中有一条就是:**一个值只允许有一个所有者**,而现在这个值(堆上的真实字符串数据)有了两个所有者:`s1` 和 `s2` 。
好吧,就假定一个值可以拥有两个所有者,会发生什么呢?
当变量离开作用域后, Rust 会自动调用 `drop` 函数并清理变量的堆内存。不过由于两个 `String` 变量指向了同一位置。这就有了一个问题:当 `s1` 和 `s2` 离开作用域,它们都会尝试释放相同的内存。这是一个叫做 ** 二次释放( double free) **的错误,也是之前提到过的内存安全性 BUG 之一。两次释放(相同)内存会导致内存污染,它可能会导致潜在的安全漏洞。
当变量离开作用域后, Rust 会自动调用 `drop` 函数并清理变量的堆内存。不过由于两个 `String` 变量指向了同一位置。这就有了一个问题:当 `s1` 和 `s2` 离开作用域,它们都会尝试释放相同的内存。这是一个叫做**二次释放( double free) **的错误,也是之前提到过的内存安全性 BUG 之一。两次释放(相同)内存会导致内存污染,它可能会导致潜在的安全漏洞。
因此, Rust 这样解决问题: ** 当 `s1` 赋予 `s2` 后, Rust 认为 `s1` 不再有效,因此也无需在 `s1` 离开作用域后 `drop` 任何东西,这就是把所有权从 `s1` 转移给了 `s2` , `s1` 在被赋予 `s2` 后就马上失效了**。
因此, Rust 这样解决问题:**当 `s1` 赋予 `s2` 后, Rust 认为 `s1` 不再有效,因此也无需在 `s1` 离开作用域后 `drop` 任何东西,这就是把所有权从 `s1` 转移给了 `s2` , `s1` 在被赋予 `s2` 后就马上失效了**。
再来看看,在所有权转移后再来使用旧的所有者,会发生什么:
```rust
@ -188,18 +188,18 @@ error[E0382]: use of moved value: `s1`
> 1. Rust 中每一个值都 `有且只有` 一个所有者(变量)
> 2. 当所有者(变量)离开作用域范围时,这个值将被丢弃(free)
如果你在其他语言中听说过术语 ** 浅拷贝( shallow copy) **和 ** 深拷贝( deep copy) **,那么拷贝指针、长度和容量而不拷贝数据听起来就像浅拷贝,但是又因为 Rust 同时使第一个变量 `s1` 无效了,因此这个操作被称为 ** 移动( move) **,而不是浅拷贝。上面的例子可以解读为 `s1` 被 ** 移动**到了 `s2` 中。那么具体发生了什么,用一张图简单说明:
如果你在其他语言中听说过术语**浅拷贝( shallow copy) **和**深拷贝( deep copy) **,那么拷贝指针、长度和容量而不拷贝数据听起来就像浅拷贝,但是又因为 Rust 同时使第一个变量 `s1` 无效了,因此这个操作被称为**移动( move) **,而不是浅拷贝。上面的例子可以解读为 `s1` 被**移动**到了 `s2` 中。那么具体发生了什么,用一张图简单说明:
< img alt = "s1 moved to s2" src = "/img/ownership01.svg" class = "center" style = "width: 50%;" / >
这样就解决了我们之前的问题,`s1` 不再指向任何数据,只有 `s2` 是有效的,当 `s2` 离开作用域,它就会释放内存。 相信此刻,你应该明白了,为什么 Rust 称呼 `let a = b` 为 ** 变量绑定**了吧?
这样就解决了我们之前的问题,`s1` 不再指向任何数据,只有 `s2` 是有效的,当 `s2` 离开作用域,它就会释放内存。 相信此刻,你应该明白了,为什么 Rust 称呼 `let a = b` 为**变量绑定**了吧?
#### 克隆(深拷贝)
首先,**Rust 永远也不会自动创建数据的 “深拷贝”**。因此,任何 ** 自动**的复制都不是深拷贝,可以被认为对运行时性能影响较小。
首先,**Rust 永远也不会自动创建数据的 “深拷贝”**。因此,任何**自动**的复制都不是深拷贝,可以被认为对运行时性能影响较小。
如果我们 ** 确实**需要深度复制 `String` 中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做 `clone` 的方法。
如果我们**确实**需要深度复制 `String` 中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做 `clone` 的方法。
```rust
let s1 = String::from("hello");
@ -230,7 +230,7 @@ println!("x = {}, y = {}", x, y);
Rust 有一个叫做 `Copy` 的特征,可以用在类似整型这样在栈中存储的类型。如果一个类型拥有 `Copy` 特征,一个旧的变量在被赋值给其他变量后仍然可用。
那么什么类型是可 `Copy` 的呢?可以查看给定类型的文档来确认,不过作为一个通用的规则: ** 任何基本类型的组合可以是 `Copy` 的,不需要分配内存或某种形式资源的类型是 `Copy` 的**。如下是一些 `Copy` 的类型:
那么什么类型是可 `Copy` 的呢?可以查看给定类型的文档来确认,不过作为一个通用的规则:**任何基本类型的组合可以是 `Copy` 的,不需要分配内存或某种形式资源的类型是 `Copy` 的**。如下是一些 `Copy` 的类型:
* 所有整数类型,比如 `u32` 。
* 布尔类型,`bool`,它的值是 `true` 和 `false` 。
@ -297,5 +297,5 @@ fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用
```
所有权很强大,避免了内存的不安全性,但是也带来了一个新麻烦: ** 总是把一个值传来传去来使用它**。 传入一个函数, 很可能还要从该函数传出去, 结果就是语言表达变得非常啰嗦, 幸运的是, Rust 提供了新功能解决这个问题。
所有权很强大,避免了内存的不安全性,但是也带来了一个新麻烦:**总是把一个值传来传去来使用它**。 传入一个函数, 很可能还要从该函数传出去, 结果就是语言表达变得非常啰嗦, 幸运的是, Rust 提供了新功能解决这个问题。