Memory Leak — Como Resolver em Zig

Memory Leak — Como Resolver em Zig

O Que Este Erro Significa

Um Memory Leak (vazamento de memória) ocorre quando o programa aloca memória dinamicamente mas nunca a libera. Com o tempo, a memória consumida cresce continuamente até causar OutOfMemory ou degradação de desempenho. Diferente de linguagens com garbage collector, em Zig toda alocação deve ser liberada manualmente.

O GeneralPurposeAllocator de Zig detecta vazamentos automaticamente no deinit():

error(gpa): memory address 0x7f1234560000 has not been freed
error(gpa): leaked allocation of 1024 bytes

O status retornado por gpa.deinit() indica se houve vazamento:

const status = gpa.deinit();
if (status == .leak) {
    // Vazamento detectado!
}

Causas Comuns

1. Esquecer de Chamar free

const std = @import("std");

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

    const dados = try allocator.alloc(u8, 1024);
    // LEAK: nunca chama allocator.free(dados)
    _ = dados;
}

2. Retorno Antecipado sem Liberar

const std = @import("std");

fn processar(allocator: std.mem.Allocator) !void {
    const buffer = try allocator.alloc(u8, 1024);

    const resultado = try fazerAlgo();
    if (resultado < 0) {
        return error.Invalido; // LEAK: buffer nunca é liberado!
    }

    allocator.free(buffer);
}

fn fazerAlgo() !i32 {
    return -1;
}

3. Sobrescrever Ponteiro sem Liberar o Anterior

const std = @import("std");

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

    var dados = try allocator.alloc(u8, 100);
    // LEAK: o primeiro buffer é perdido!
    dados = try allocator.alloc(u8, 200);
    // Só o segundo será acessível para free
    allocator.free(dados);
}

4. Esquecer deinit em Coleções

const std = @import("std");

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

    var lista = std.ArrayList(u8).init(allocator);
    try lista.append(1);
    try lista.append(2);
    try lista.append(3);
    // LEAK: nunca chama lista.deinit()
}

5. Leak em Estruturas Aninhadas

const std = @import("std");

const Registro = struct {
    nome: []u8,
    email: []u8,
};

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

    var lista = std.ArrayList(Registro).init(allocator);
    defer lista.deinit(); // Libera a lista, mas NÃO os campos internos!

    const nome = try allocator.alloc(u8, 10);
    @memcpy(nome, "Zig Brasil");
    const email = try allocator.alloc(u8, 15);
    @memcpy(email, "zig@brasil.dev.");

    try lista.append(.{ .nome = nome, .email = email });
    // LEAK: nome e email nunca são liberados!
}

Como Corrigir

Solucao 1: Usar defer Imediatamente Após Alocar

const std = @import("std");

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

    const dados = try allocator.alloc(u8, 1024);
    defer allocator.free(dados); // Garante free ao final do escopo

    // Usa dados normalmente
    dados[0] = 42;
}

Solucao 2: Usar errdefer para Caminhos de Erro

const std = @import("std");

fn processar(allocator: std.mem.Allocator) ![]u8 {
    const buffer = try allocator.alloc(u8, 1024);
    errdefer allocator.free(buffer); // Free SÓ se retornar erro

    try validar(buffer);

    return buffer; // Sucesso: transfere propriedade
}

fn validar(buf: []u8) !void {
    _ = buf;
}

Solucao 3: Liberar Antes de Sobrescrever

const std = @import("std");

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

    var dados = try allocator.alloc(u8, 100);

    // Libera o antigo antes de realocar
    allocator.free(dados);
    dados = try allocator.alloc(u8, 200);
    defer allocator.free(dados);
}

Solucao 4: Limpar Coleções com Dados Aninhados

const std = @import("std");

const Registro = struct {
    nome: []u8,
    email: []u8,
};

fn limparRegistros(allocator: std.mem.Allocator, lista: *std.ArrayList(Registro)) void {
    for (lista.items) |reg| {
        allocator.free(reg.nome);
        allocator.free(reg.email);
    }
    lista.deinit();
}

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

    var lista = std.ArrayList(Registro).init(allocator);
    defer limparRegistros(allocator, &lista);

    // Agora ao sair, tudo é liberado corretamente
}

Solucao 5: ArenaAllocator para Alocações com Mesmo Ciclo de Vida

const std = @import("std");

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

    var arena = std.heap.ArenaAllocator.init(gpa.allocator());
    defer arena.deinit(); // Libera TUDO de uma vez — impossível vazar

    const allocator = arena.allocator();

    _ = try allocator.alloc(u8, 100);
    _ = try allocator.alloc(u8, 200);
    _ = try allocator.alloc(u8, 300);
    // Sem free individual necessário — arena cuida de tudo
}

Detecção de Leaks

Usando GeneralPurposeAllocator

const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{
        .verbose_log = true, // Loga cada alocação e free
    }){};
    defer {
        const status = gpa.deinit();
        if (status == .leak) {
            @panic("Vazamento de memória detectado!");
        }
    }

    const allocator = gpa.allocator();
    const buf = try allocator.alloc(u8, 100);
    _ = buf;
    // Se esquecer de liberar, o deinit reporta o leak
}

Usando Valgrind

zig build-exe main.zig
valgrind --leak-check=full ./main

Regras de Ouro

  1. Para cada alloc, tenha um free correspondente.
  2. Use defer imediatamente após cada alocação.
  3. Use errdefer para liberar em caminhos de erro.
  4. Cuidado com dados aninhados — libere de dentro para fora.
  5. Use ArenaAllocator quando múltiplas alocações compartilham o ciclo de vida.
  6. Sempre teste com GeneralPurposeAllocator em Debug para detectar leaks cedo.

Erros Relacionados

Continue aprendendo Zig

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