Аллокаторы: явное управление памятью

Понимаем главную идею 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 — интерфейс; функция работает с любой стратегией выделения.
Проверьте себя
1. Почему функция в Zig обязана принимать аллокатор, чтобы выделить память на куче?
AИз-за ограничений LLVM
BЭто принцип «нет скрытым аллокациям»: без аллокатора функция не может тронуть кучу
CЧтобы ускорить компиляцию
DАллокатор хранит сборщик мусора
2. Какова идиоматичная пара к alloc.alloc в Zig?
AНичего, память освобождается сама
BСразу defer alloc.free(...) рядом с выделением
CВызов gc()
DРучной вызов в самом конце main