fix(ownership): Bold

pull/348/head
Allan Downey 3 years ago
parent a3277b6a8f
commit 44b7ab8cc1

@ -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 提供了新功能解决这个问题。

Loading…
Cancel
Save