Ziglings 笔记 96: 内存分配器 (Allocators)
向操作系统借地盘
在 Ziglings 的第 96 个练习中,我们迎来了一个里程碑:动态内存分配。
在之前的练习中,数组的大小必须在编译期已知(如 [5]u8)。但如果我们要读取用户输入的字符串,或者处理一个未知大小的文件怎么办?栈内存(Stack)满足不了我们,我们需要堆内存(Heap)。
Zig 与 C 或 Go 不同,它没有全局的 malloc,也没有垃圾回收(GC)。它要求我们在需要内存时,必须显式地选择一个 Allocator(分配器)。
挑战:计算移动平均数
我们需要为一个长度未知的输入数组计算移动平均数。
因为 avg 数组的长度取决于输入 arr 的长度,我们无法在编译期确定它的大小,必须在运行时分配。
解决方案
我们使用 ArenaAllocator。这是一种非常省心的分配策略:只管申请,最后一次性全部释放。
const std = @import("std");
fn runningAverage(arr: []const f64, avg: []f64) void {
var sum: f64 = 0;
for (0.., arr) |index, val| {
sum += val;
const f_index: f64 = @floatFromInt(index + 1);
avg[index] = sum / f_index;
}
}
pub fn main() !void {
// 假设这是从用户输入读取的动态数据
const arr: []const f64 = &[_]f64{ 0.3, 0.2, 0.1, 0.1, 0.4 };
// 1. 初始化 Arena 分配器
// page_allocator 是直接找操作系统要内存的底层分配器
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
// 2. 确保在 main 函数退出时释放所有内存
// Arena 的好处就在这里:我们不需要 free(avg),只需要释放 arena 即可
defer arena.deinit();
// 3. 获取分配器接口
const allocator = arena.allocator();
// 4. 动态分配内存
// alloc 可能会失败(内存不足),所以需要 try
// 它返回一个切片 []f64
const avg: []f64 = try allocator.alloc(f64, arr.len);
runningAverage(arr, avg);
std.debug.print("Running Average: ", .{});
for (avg) |val| {
std.debug.print("{d:.2} ", .{val});
}
std.debug.print("\n", .{});
}
核心知识点总结
1. 为什么是 Arena?
ArenaAllocator 就像是把你所有的玩具都倒在一个大框里。
- 优点:分配速度极快(指针移动一下就行),清理极其方便(直接把框倒空)。
- 缺点:内存使用量只能增加,不能减少。你不能单独释放框里的某一个玩具。 对于生命周期较短的任务(如 HTTP 请求处理、CLI 工具),这是最佳选择。
2. defer 的至关重要性
defer arena.deinit() 是 Zig 内存安全的基石。它保证了无论函数通过什么路径退出(正常结束或发生错误),内存都会被清理。这避免了 C 语言中常见的内存泄漏问题。
3. alloc vs create
alloc(T, n): 分配数组。返回切片[]T。create(T): 分配单个对象。返回指针*T。
后续预告:我们学会了分配原始内存切片。但在实际开发中,我们需要更高级的数据结构,比如可以自动扩容的数组。下一篇,我们将学习 Zig 标准库中的 ArrayList。