Skip to content
This repository has been archived by the owner on Aug 24, 2024. It is now read-only.

Commit

Permalink
Update 07-heap-memory-and-allocator.md (#28)
Browse files Browse the repository at this point in the history
fix typo
  • Loading branch information
hshq authored Jan 12, 2024
1 parent 24c33a0 commit 7299657
Showing 1 changed file with 10 additions and 9 deletions.
19 changes: 10 additions & 9 deletions 07-heap-memory-and-allocator.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

堆是我们可以使用的第三个也是最后一个内存区域。与全局数据和调用栈相比,堆有点像蛮荒之地:什么都可以使用。具体来说,在堆中,我们可以在运行时创建大小已知的内存,并完全控制其生命周期。

调用堆栈之所以令人惊叹,是因为它管理数据的方式简单且可预测(通过推送和弹出堆栈帧)。这一优点同时也是缺点:数据的生命周期与它在调用堆栈中的位置息息相关。堆则恰恰相反。它没有内置的生命周期,因此我们的数据可长可短。这个优点也是它的缺点:它没有内置的生命周期,所以如果我们不释放数据,就没有人会释放。
调用堆栈之所以令人惊叹,是因为它管理数据的方式简单且可预测(通过压入和弹出堆栈帧)。这一优点同时也是缺点:数据的生命周期与它在调用堆栈中的位置息息相关。堆则恰恰相反。它没有内置的生命周期,因此我们的数据可长可短。这个优点也是它的缺点:它没有内置的生命周期,所以如果我们不释放数据,就没有人会释放。

让我们来看一个例子:

Expand Down Expand Up @@ -89,7 +89,7 @@ pub const Game = struct {

这段代码主要突显两件事:

1. `errdefer` 的作用。在正常情况下,`player``init` 分配,在 `deinit` 释放。但有一种边缘情况,即历史初始化失败。在这种情况下,我们需要撤销玩家的分配
1. `errdefer` 的作用。在正常情况下,`player``init` 分配,在 `deinit` 释放。但有一种边缘情况,`history` 初始化失败。在这种情况下,我们需要撤销 `players` 的分配
2. 我们动态分配的两个切片(`players``history`)的生命周期是基于我们的应用程序逻辑的。没有任何规则规定何时必须调用 `deinit` 或由谁调用。这是件好事,因为它为我们提供了任意的生命周期,但也存在缺点,就是如果从未调用 `deinit` 或调用 `deinit` 超过一次,就会出现混乱和错误。

> `init``deinit` 的名字并不特殊。它们只是 Zig 标准库使用的,也是社区采纳的名称。在某些情况下,包括在标准库中,会使用 `open``close`,或其他更适当的名称。
Expand All @@ -98,7 +98,7 @@ pub const Game = struct {

上面提到过,没有规则规定什么时候必须释放什么东西。但事实并非如此,还是有一些重要规则,只是它们不是强制的,需要你自己格外小心。

第一条规则是不能释放同一内存两次
第一条规则是不可释放同一内存两次

```zig
const std = @import("std");
Expand All @@ -117,7 +117,7 @@ pub fn main() !void {

可以预见到,最后一行代码不会被打印出来。这是因为我们释放了相同的内存两次。这被称为双重释放,是无效的。要避免这种情况似乎很简单,但在具有复杂生命周期的大型项目中,却很难发现。

第二条规则是,不能释放没有引用的内存。这听起来似乎很明显,但谁负责释放内存并不总是很清楚。下面的代码声明了一个转小写的函数:
第二条规则是,无法释放没有引用的内存。这听起来似乎很明显,但谁负责释放内存并不总是很清楚。下面的代码声明了一个转小写的函数:

```zig
const std = @import("std");
Expand All @@ -137,7 +137,7 @@ fn allocLower(allocator: Allocator, str: []const u8) ![]const u8 {
}
```

上面的代码正常,但下面的则会报错:
上面的代码没问题。但以下用法不是:

```zig
// For this specific code, we should have used std.ascii.eqlIgnoreCase
Expand All @@ -147,9 +147,9 @@ fn isSpecial(allocator: Allocator, name: [] const u8) !bool {
}
```

这是内存泄漏。创建的内存 `allocLower` 永远不会被释放。不仅如此,一旦 `isSpecial` 返回,这块内存就永远无法释放。在有垃圾收集器的语言中,当数据变得无法访问时,垃圾收集器最终会释放无用的内存。
这是内存泄漏。`allocLower` 中创建的内存永远不会被释放。不仅如此,一旦 `isSpecial` 返回,这块内存就永远无法释放。在有垃圾收集器的语言中,当数据变得无法访问时,垃圾收集器最终会释放无用的内存。

但在上面的代码中,一旦 `isSpecial` 返回,我们就失去了对已分配内存的唯一引用,即 `lower` 变量。而直到我们的进程退出后,这块内存块才会释放。我们的函数可能只会泄漏几个字节,但如果它是一个长时间运行的进程,并且重复调用该函数,无法泄漏的内存块就会逐渐累积起来,最终会耗尽所有内存。
但在上面的代码中,一旦 `isSpecial` 返回,我们就失去了对已分配内存的唯一引用,即 `lower` 变量。而直到我们的进程退出后,这块内存块才会释放。我们的函数可能只会泄漏几个字节,但如果它是一个长时间运行的进程,并且重复调用该函数,未被释放的内存块就会逐渐累积起来,最终会耗尽所有内存。

至少在双重释放的情况下,我们的程序会遭遇严重崩溃。内存泄漏可能很隐蔽。不仅仅是根本原因难以确定。真正的小泄漏或不常执行的代码中的泄漏甚至很难被发现。这是一个很常见的问题,Zig 提供了帮助,我们将在讨论分配器时看到。

Expand Down Expand Up @@ -338,7 +338,7 @@ pub const IntList = struct {
};
```

有趣的部分发生在 `add` 函数里,当 `pos == len`时,表明我们已经填充了当前数组,并且需要创建一个更大的数组。我们可以像这样使用`IntList`
有趣的部分发生在 `add` 函数里,当 `pos == len`时,表明我们已经填满了当前数组,并且需要创建一个更大的数组。我们可以像这样使用`IntList`

```zig
const std = @import("std");
Expand Down Expand Up @@ -522,6 +522,7 @@ pub fn main() !void {
.above = true,
.last_param = "are options",
}, .{.whitespace = .indent_2});
defer allocator.free(json);
std.debug.print("{s}\n", .{json});
}
Expand All @@ -539,7 +540,7 @@ pub fn main() !void {

但如果将 `buf` 更改为 `[120]u8`,将得到一个内存不足的错误。

固定缓冲区分配器(FixedBufferAllocators)的常见模式是重置并重复使用,竞技场分配器(ArenaAllocators)也是如此。这将释放所有先前的分配,并允许重新使用分配器。
固定缓冲区分配器(FixedBufferAllocators)的常见模式是 `reset` 并重复使用,竞技场分配器(ArenaAllocators)也是如此。这将释放所有先前的分配,并允许重新使用分配器。

---

Expand Down

0 comments on commit 7299657

Please sign in to comment.