作为一个java开发者,我所知道的关于内存分配的所有内容都是由一些称为垃圾收集的process处理的,这是JVM的问题,而不是我的问题。
因此,当我打开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)!
后续补充......
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。