作为一个 Ruby 开发者,我所知道的关于内存分配的所有内容都是由一些称为垃圾收集的 process 处理的,这是 Aaron Patterson 的问题,而不是我的问题。
因此,当我打开 Rust Book 并看到 Rust 没有垃圾收集时,我变得有点担心。处理记忆管理的责任是否应该堆积在我身上?显然,对于 C 这样的系统编程语言来说,处理内存分配是一件大事,如果做得不好就会产生重大影响。
1、Stack(栈) & Heap(堆)
栈和堆是在运行时管理内存的方法。
栈被认为是快速高效的,因为它有序地存储和访问数据。栈顶是在栈中添加、删除数据的唯一位置。这被称为 LIFO,后进先出,意味着我们在释放内存时只需知道栈顶的地址。
栈快速的另一个原因,栈所需的内存空间在编译时是已知的。这意味着我们可以在将数据存储到其中之前分配一块固定大小的内存。
例如,如果你知道有四个人参加您的晚宴,你可以提前决定每个人的座位,准备多少食物,并在他们到达之前练习他们的名字。这是超级高效的!如果你不能提前确切地知道有多少人参加您的晚宴,你可以使用堆。使用堆意味着需要准备足够多但未知数目的椅子并向到来的人发出名称标签(name 字段)。
当在运行时期间需要存储未知大小的数据时,计算机会在堆上搜索内存,对其进行标记并返回一个指针,该指针指向内存中的位置。这称为分配内存。然后,您可以在栈上使用该指针,但是,当你要检索真实数据时,需要读取指针指向的内存位置的数据。
当我不断深入了解栈和堆时,管理堆中的数据会很困难。例如,你需要确保在完成使用一块内存后允许计算机重新分配这块内存。但是,如果其中一个代码块释放了内存中的某个位置,而另一个代码块仍然持有该内存的指针,则会出现悬空指针(dangling pointer)。
跟踪哪部分代码正在使用堆上的哪些数据,最小化堆上的数据拷贝,以及清理堆上未被使用的数据使其不会耗尽内存空间,这是所有权解决的所有问题。
Rust Book
2、Ownership(所有权) & Scope(作用域)
Rust 关于所有权的三个规则:
Rust 中的每个值都有一个称为其所有者的变量名。(let name = “xxx”)
同一时间只能有一个所有者。
当所有者超出作用域时,该值将被删除。
2.1 所有权的最简单的例子
所有权的最简单的例子是关于变量的作用域:
一旦当前函数作用域结束,由}表示,变量 hello 超出作用域会被删除。这一点和大多数编程语言的本地变量是一致的。但这不是所有权的全部,当我们需要在传递值,并将字符串常量 (暂时认为数据存储在栈中,其实在永久的常量池中) 切换到 String 类型 (数据存储在堆上的) 时,事情会变得更有趣。
2.2 所有权发生改变
当使用字符串常量时,正如我们所期望的那样,Rust 将 hello 的值复制到 hello1 中。但是当使用 String 类型时,Rust 会移交该值的所有值。编译时会抛出错误:error[E0382]: use of moved value: ‘hello’。
看起来在使用字符串常量时,Rust 会将该一个变量的值复制到另一个变量中,但是当我们使用 String 类型时,它会移交所有权。关于这个话题在论坛里有相关讨论,请阅读此贴子 The Copy trait – what does it actually copy?.
以下是我对讨论后的总结:
字符串常量“Hello,World!”存储在只读内存中的某个位置(既不在栈中也不在堆中),并且指向该字符串的指针存储在栈中。这里的指针通常是称为引用,这意味着我们使用指向存储在永久内存中的字符串常量的引用(参见 Ownership in Rust, Part 2 中有关引用的更多信息),并保证它在整个程序的运行时间里是有效的(它有一个静态的生命周期)。
变量 hello 和 hello1 存储在栈。当我们使用 = 运算符时,Rust 会将存储在 hello 中的指针值的副本绑定到变量 hello1。在作用域的最后,Rust 会调用 drop 方法从栈中删除变量以释放内存。这些变量可以存储并轻松地在栈中进行复制,因为它们的大小在编译时是已知的。
在堆上,字符串类型的值为“hello,world!”使用 string:from 方法绑定到变量 hello。但是,与字符串常量不同,绑定到变量 hello 的是数据本身而不仅仅是指针,并且这些数据的大小可以在运行时更改。= 运算符将变量 hello 指向的数据绑定到新变量 hello1,有效地将数据的所有权从一个变量移交给另一个变量。变量 hello 现在是无效的,根据所有权规则 2:“同一时间只能有一个所有者。”
2.3 为什么不总是 copy 数据?
但为什么这样呢?为什么 Rust 不始终复制数据并将其绑定到新变量?
回想一下栈和堆之间的差异,堆上存储的数据大小在编译时是不可知的,这意味着我们需要在运行时进行一些内存分配步骤。这可能会代价很高。根据我们的数据量,如果我们整天都在 copy 数据,可能会很快耗尽内存。除此之外,Rust 的默认行为会保护我们免受内存问题的影响(可能在其他语言中遇到)。
将数据存储在堆上并在栈上存储指向该数据的指针。但是,与使用指针指向只读内存 (存储字符串常量) 不同,堆上的数据可能会发生变化。指针值 << DATA >> 绑定到存储 String 类型的 hello 变量。如果我们将相同的指针值绑定到两个不同的变量,看起来像这样:
我们有两个变量 hello 和 hello1,它们共享相同值的所有权。这违反了规则 2:“同一时间只能有一个所有者”,但让我们继续。
在变量 hello 和 hello1 的作用域结束时,我们必须将他们在堆上的内存释放。
首先,我们将 hello1 指向的堆上内存数据释放,现在当我们释放 hello 时会发生什么?
这称为双重释放错误(double free error),我认为在这个 StackOverflow 答案中有最好的总结:https://stackoverflow.com/a/2…
A double free in C, technically speaking, leads to undefined behavior. This means that the program can behave completely arbitrarily and all bets are off about what happens. That’s certainly a bad thing to have happen! In practice, double-freeing a block of memory will corrupt the state of the memory manager, which might cause existing blocks of memory to get corrupted or for future allocations to fail in bizarre ways (for example, the same memory getting handed out on two different successive calls of malloc).
Double frees can happen in all sorts of cases. A fairly common one is when multiple different objects all have pointers to one another and start getting cleaned up by calls to free. When this happens, if you aren’t careful, you might free the same pointer multiple times when cleaning up the objects. There are lots of other cases as well, though.
— templatetypedef
Rust 就是要避免犯这类错误。通过使 hello 无效,编译器知道只在 hello1 上发出一个释放内存的调用(drop)。
2.4 深度拷贝
这一切都很好,但有些情况下我们确实想要 copy 存储在堆中的数据。Rust 中可以使用 clone()方法 实现。
请记住,调用 clone()的代价可能会很高,这就是 Rust 默认阻止这类“deep copy”的原因。
3、参考
Rust Book
Rust Language Form Post about The Copy Trait
未完待续
显然,Rust 的所有权涉及的知识还有很多: 借用 (borrowing),引用(referencing) 和切片 (slicing)!
后续补充 ……