Cheatsheet: Allocators em Zig

Cheatsheet: Allocators em Zig

O sistema de alocadores é um dos pilares fundamentais de Zig. Em vez de um alocador global implícito como em C (malloc/free) ou um garbage collector como em Go/Java, Zig exige que você passe explicitamente um alocador para qualquer função que precise alocar memória. Isso dá controle total e facilita testes, depuração e otimização.

Conceito Fundamental

Em Zig, std.mem.Allocator é uma interface (implementada via ponteiro de função). Qualquer código que precisa alocar memória recebe um Allocator como parâmetro:

const std = @import("std");

fn criarLista(allocator: std.mem.Allocator) !std.ArrayList(u8) {
    return std.ArrayList(u8).init(allocator);
}

Por que isso importa:

  • Sem estado global oculto — cada alocação é explícita
  • Fácil trocar alocadores para testes ou otimização
  • Detecção automática de vazamento de memória em modo debug
  • Permite alocadores especializados para cada caso de uso

Tipos de Alocadores

GeneralPurposeAllocator (GPA)

O alocador mais usado no dia a dia. Detecta vazamentos de memória e uso indevido (use-after-free, double-free) em modo debug.

const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer {
        const status = gpa.deinit();
        if (status == .leak) @panic("Vazamento de memória detectado!");
    }
    const allocator = gpa.allocator();

    // Usar o allocator normalmente
    const dados = try allocator.alloc(u8, 100);
    defer allocator.free(dados);

    // Alocar um único item
    const ptr = try allocator.create(u32);
    defer allocator.destroy(ptr);
    ptr.* = 42;
}

Opções de configuração:

var gpa = std.heap.GeneralPurposeAllocator(.{
    .stack_trace_frames = 8,       // frames no stack trace de erros
    .enable_memory_limit = true,   // habilitar limite de memória
    .thread_safe = true,           // seguro para múltiplas threads
}){};
gpa.setRequestedMemoryLimit(1024 * 1024); // limite de 1MB

ArenaAllocator

Aloca tudo de uma vez e libera tudo junto. Excelente para operações com tempo de vida bem definido.

const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    // Arena usa outro alocador como backend
    var arena = std.heap.ArenaAllocator.init(gpa.allocator());
    defer arena.deinit(); // libera TUDO de uma vez

    const allocator = arena.allocator();

    // Não precisa de free individual!
    const buf1 = try allocator.alloc(u8, 256);
    const buf2 = try allocator.alloc(u8, 512);
    const buf3 = try allocator.alloc(u8, 1024);

    // Usar buf1, buf2, buf3...
    _ = buf1;
    _ = buf2;
    _ = buf3;

    // Resetar arena sem liberar a memória do SO
    _ = arena.reset(.retain_capacity);
    // Agora a memória pode ser reutilizada
}

Quando usar Arena:

  • Processamento de requisições (alocar tudo, processar, liberar tudo)
  • Parsing de dados temporários
  • Operações batch que produzem resultado final

FixedBufferAllocator

Aloca memória a partir de um buffer fixo na stack ou pré-alocado. Zero alocações no heap.

const std = @import("std");

pub fn main() !void {
    var buffer: [4096]u8 = undefined;
    var fba = std.heap.FixedBufferAllocator.init(&buffer);
    const allocator = fba.allocator();

    // Aloca do buffer estático
    const dados = try allocator.alloc(u8, 100);
    _ = dados;

    // Se ultrapassar 4096 bytes, retorna error.OutOfMemory
    // Resetar para reusar
    fba.reset();
}

Quando usar FixedBuffer:

  • Sistemas embarcados sem heap
  • Operações com tamanho máximo conhecido
  • Quando não pode falhar na alocação (pré-aloca tudo)

page_allocator

Alocador que usa diretamente as chamadas de sistema do SO (mmap/VirtualAlloc). Cada alocação pede pelo menos uma página (geralmente 4KB).

const std = @import("std");

pub fn main() !void {
    const allocator = std.heap.page_allocator;

    // Cada alocação é no mínimo uma página do SO
    const dados = try allocator.alloc(u8, 1024);
    defer allocator.free(dados);

    // Bom para alocações grandes
    const grande = try allocator.alloc(u8, 1024 * 1024); // 1MB
    defer allocator.free(grande);
}

c_allocator

Wrapper sobre malloc/free do C. Útil para interop com bibliotecas C.

const std = @import("std");

pub fn main() !void {
    const allocator = std.heap.c_allocator;

    const dados = try allocator.alloc(u8, 100);
    defer allocator.free(dados);
}

Nota: Requer linkagem com libc (-lc ou const std_options = .{ .link_libc = true } no build.zig).

Operações do Allocator

Tabela de Referência Rápida

OperaçãoDescriçãoExemplo
alloc(T, n)Alocar array de n elementostry alloc.alloc(u8, 100)
free(slice)Liberar slice alocadoalloc.free(dados)
create(T)Alocar um único itemtry alloc.create(Struct)
destroy(ptr)Liberar item únicoalloc.destroy(ptr)
realloc(slice, n)Redimensionar alocaçãotry alloc.realloc(buf, 200)
dupe(T, slice)Duplicar slicetry alloc.dupe(u8, "cópia")

Exemplos Detalhados

const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const alloc = gpa.allocator();

    // alloc e free
    const nums = try alloc.alloc(i32, 10);
    defer alloc.free(nums);
    for (nums, 0..) |*n, i| {
        n.* = @intCast(i);
    }

    // create e destroy
    const Ponto = struct { x: f32, y: f32 };
    const p = try alloc.create(Ponto);
    defer alloc.destroy(p);
    p.* = .{ .x = 1.0, .y = 2.0 };

    // dupe — cria cópia independente
    const original = "texto original";
    const copia = try alloc.dupe(u8, original);
    defer alloc.free(copia);

    // realloc — redimensionar
    var buf = try alloc.alloc(u8, 10);
    buf = try alloc.realloc(buf, 50); // agora tem 50 bytes
    defer alloc.free(buf);
}

Padrão: Alocador como Parâmetro

A convenção em Zig é sempre receber o alocador como parâmetro:

const std = @import("std");

const MeuBuffer = struct {
    dados: []u8,
    allocator: std.mem.Allocator,

    pub fn init(allocator: std.mem.Allocator, tamanho: usize) !MeuBuffer {
        return .{
            .dados = try allocator.alloc(u8, tamanho),
            .allocator = allocator,
        };
    }

    pub fn deinit(self: *MeuBuffer) void {
        self.allocator.free(self.dados);
    }
};

Padrão: Defer para Limpeza

Sempre use defer para garantir a liberação:

fn processarDados(allocator: std.mem.Allocator) ![]u8 {
    var temp = try allocator.alloc(u8, 1024);
    defer allocator.free(temp); // liberado mesmo se houver erro

    // ... processar usando temp ...

    // Resultado separado que será retornado (caller faz free)
    const resultado = try allocator.dupe(u8, temp[0..tamanho_real]);
    return resultado;
}

Comparativo dos Alocadores

AlocadorVelocidadeSegurança DebugUso Ideal
GeneralPurposeAllocatorMédioAlta (detecta leaks)Desenvolvimento geral
ArenaAllocatorRápidoMédiaOperações batch
FixedBufferAllocatorMuito rápidoBaixaEmbarcados, stack
page_allocatorLentoNenhumaAlocações grandes
c_allocatorMédioNenhumaInterop com C

Erros Comuns

// ERRO: esquecer de liberar memória
fn ruim(allocator: std.mem.Allocator) !void {
    const dados = try allocator.alloc(u8, 100);
    // Sem free! GPA vai detectar o vazamento
}

// CORRETO: usar defer
fn bom(allocator: std.mem.Allocator) !void {
    const dados = try allocator.alloc(u8, 100);
    defer allocator.free(dados);
    // ...
}

// ERRO: usar memória após free (use-after-free)
fn perigoso(allocator: std.mem.Allocator) !void {
    const dados = try allocator.alloc(u8, 100);
    allocator.free(dados);
    dados[0] = 42; // Comportamento indefinido!
}

Veja Também

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.