Аллокаторы: явное управление памятью
Понимаем главную идею Zig — память выделяется только через явный аллокатор.
Аллокатор (allocator) — объект, отвечающий за выделение и освобождение динамической памяти; в Zig любая функция, работающая с кучей, обязана принять аллокатор параметром.
Это центральный урок всего курса. В большинстве языков выделение памяти спрятано: new, конструкторы, рост контейнеров. Zig делает обратное — память на куче выделяется только через объект-аллокатор, который вы передаёте явно. Это и есть знаменитое «нет скрытым аллокациям».
Зачем такая строгость
Явный аллокатор даёт три выгоды. Во-первых, видно, какой код вообще трогает кучу: функция без аллокатора физически не может выделить динамическую память. Во-вторых, вызывающий выбирает стратегию выделения под задачу — для теста один аллокатор, для встраиваемой системы другой. В-третьих, легко ловить утечки: специальный отладочный аллокатор сообщит о неосвобождённой памяти.
Выделение и освобождение
const std = @import("std");
fn useMemory(alloc: std.mem.Allocator) !void {
// выделяем массив из 5 целых
const buf = try alloc.alloc(i32, 5);
defer alloc.free(buf); // освободим при выходе из функции
buf[0] = 42;
std.debug.print("buf[0]={d} len={d}\n", .{ buf[0], buf.len });
}
Метод alloc.alloc(i32, 5) выделяет срез из пяти i32 и возвращает его (или ошибку — память может кончиться, отсюда try). Сразу после выделения ставят defer alloc.free(buf): освобождение видно рядом с выделением и гарантированно случится при любом выходе из функции. Это идиома Zig: выделил — тут же написал defer на освобождение.
Аллокатор передаётся по цепочке
// аллокатор «протекает» через всю программу явными параметрами
fn buildReport(alloc: std.mem.Allocator) !Report {
const items = try alloc.alloc(Item, 10);
errdefer alloc.free(items); // откат при ошибке ниже
// ...
}
Поскольку выделять память может только обладатель аллокатора, он передаётся вниз по вызовам как обычный параметр. Это делает «дерево владения памятью» видимым прямо в сигнатурах функций.
Как работает под капотом
std.mem.Allocator — это интерфейс из указателя на реализацию и таблицы функций alloc/free/resize. Любой конкретный аллокатор реализует эту таблицу. Поэтому ваша функция, принимающая Allocator, работает с любой стратегией выделения — арена, страничный, отладочный — не зная деталей. Это полиморфизм без наследования и без скрытых вызовов.
Частые ошибки
Первая — забыть defer alloc.free(...) и получить утечку. Вторая — освободить чужой памятью аллокатор не тем аллокатором, которым выделяли: освобождать нужно тем же. Третья — ждать, что контейнер из std выделит память «сам»: почти все они требуют аллокатор.
Итог
- В Zig куча доступна только через явный аллокатор-параметр.
- Идиома:
const x = try alloc.alloc(...); defer alloc.free(x);. - Аллокатор передаётся вниз по вызовам, делая владение памятью видимым.
std.mem.Allocator— интерфейс; функция работает с любой стратегией выделения.