---
title: "Stack vs Heap em Zig: Fundamentos de Gerenciamento de Memória"
url: "https://ziglang.com.br/tutoriais/stack-vs-heap-em-zig-fundamentos-de-gerenciamento-de-mem%C3%B3ria/"
markdown_url: "https://ziglang.com.br/tutoriais/stack-vs-heap-em-zig-fundamentos-de-gerenciamento-de-mem%C3%B3ria.MD"
description: "Aprenda a diferença entre stack e heap em Zig com exemplos práticos. Entenda alocação de memória, lifetime de variáveis e como Zig torna tudo explícito."
date: "2026-02-21"
author: "Zig Brasil"
---

# Stack vs Heap em Zig: Fundamentos de Gerenciamento de Memória

Aprenda a diferença entre stack e heap em Zig com exemplos práticos. Entenda alocação de memória, lifetime de variáveis e como Zig torna tudo explícito.


Antes de escrever código eficiente em qualquer linguagem de sistemas, você precisa entender como a memória funciona. Neste primeiro artigo da série **Masterclass de Memória**, vamos explorar os fundamentos de stack e heap em Zig — e por que Zig torna essas escolhas mais claras do que qualquer outra linguagem.

## O Modelo de Memória de um Programa

Quando um programa é executado, o sistema operacional reserva diferentes regiões de memória para ele. As duas mais importantes para nós são:

| Região | Características | Velocidade | Gerenciamento |
|--------|----------------|------------|---------------|
| **Stack** | Tamanho fixo, LIFO | Extremamente rápida | Automático |
| **Heap** | Tamanho dinâmico | Mais lenta | Manual em Zig |

### A Stack: Memória Automática

A stack (pilha) é uma região de memória que funciona como uma pilha de pratos — o último item colocado é o primeiro a ser removido (LIFO — Last In, First Out). Cada vez que uma função é chamada, um **stack frame** é criado; quando a função retorna, o frame é destruído automaticamente.

```zig
const std = @import("std");

fn calcularArea(largura: f64, altura: f64) f64 {
    // 'resultado' vive na stack desta função
    // Quando calcularArea() retorna, 'resultado' é destruído automaticamente
    const resultado = largura * altura;
    return resultado;
}

fn exemploStack() void {
    // Estas variáveis vivem na stack de exemploStack()
    const x: i32 = 42;
    const y: f64 = 3.14;
    var buffer: [256]u8 = undefined; // Array de 256 bytes na stack

    const area = calcularArea(10.0, 20.0);

    std.debug.print("x = {d}, y = {d:.2}, area = {d:.1}\n", .{ x, y, area });
    std.debug.print("buffer tem {d} bytes na stack\n", .{buffer.len});
}

pub fn main() void {
    exemploStack();
}
```

Neste exemplo, todas as variáveis (`x`, `y`, `buffer`, `resultado`) vivem na stack. Elas são criadas quando a função é chamada e destruídas quando a função retorna. Você não precisa se preocupar em "liberar" essa memória.

**Vantagens da stack:**
- Alocação instantânea (apenas mover o stack pointer)
- Desalocação automática ao sair do escopo
- Excelente localidade de cache (dados contíguos)

**Limitações da stack:**
- Tamanho limitado (geralmente 1-8 MB por thread)
- Dados não sobrevivem além do escopo da função
- Tamanho deve ser conhecido em tempo de compilação

### A Heap: Memória Dinâmica

A heap é uma região de memória muito maior, onde você pode alocar dados de tamanho variável que persistem além do escopo de uma função. Em Zig, toda alocação de heap é feita através de **allocators** — nunca há uma chamada oculta a `malloc`.

```zig
const std = @import("std");

pub fn main() !void {
    // O allocator é explícito — você sempre sabe quando está alocando
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer {
        const status = gpa.deinit();
        if (status == .leak) {
            std.debug.print("ALERTA: Memory leak detectado!\n", .{});
        }
    }
    const allocator = gpa.allocator();

    // Alocação na heap — tamanho determinado em runtime
    const tamanho: usize = 1000;
    const dados = try allocator.alloc(u8, tamanho);
    defer allocator.free(dados); // Liberar quando sair do escopo

    // Preencher os dados
    for (dados, 0..) |*byte, i| {
        byte.* = @intCast(i % 256);
    }

    std.debug.print("Alocados {d} bytes na heap\n", .{dados.len});
    std.debug.print("Primeiros 5 bytes: ", .{});
    for (dados[0..5]) |byte| {
        std.debug.print("{d} ", .{byte});
    }
    std.debug.print("\n", .{});
}
```

Observe como em Zig:
1. O allocator é passado **explicitamente** — sem `malloc` global oculto
2. O `defer` garante que a memória será liberada ao sair do escopo
3. O `GeneralPurposeAllocator` detecta memory leaks automaticamente

## Stack vs Heap: Quando Usar Cada Uma

Escolher entre stack e heap é uma decisão fundamental. Aqui está um guia prático:

### Use a Stack Quando:

```zig
const std = @import("std");

// 1. Dados com tamanho conhecido em compilação
fn processarCoords() void {
    var ponto = [3]f64{ 1.0, 2.0, 3.0 }; // 24 bytes na stack
    ponto[0] = 10.0;
    std.debug.print("Ponto: ({d}, {d}, {d})\n", .{ ponto[0], ponto[1], ponto[2] });
}

// 2. Buffers temporários pequenos
fn formatarMensagem(nome: []const u8) void {
    var buf: [512]u8 = undefined;
    const msg = std.fmt.bufPrint(&buf, "Olá, {s}!", .{nome}) catch "erro";
    std.debug.print("{s}\n", .{msg});
}

// 3. Structs pequenas
const Vetor2D = struct {
    x: f64,
    y: f64,

    fn magnitude(self: Vetor2D) f64 {
        return @sqrt(self.x * self.x + self.y * self.y);
    }
};

pub fn main() void {
    processarCoords();
    formatarMensagem("Zig Brasil");

    const v = Vetor2D{ .x = 3.0, .y = 4.0 };
    std.debug.print("Magnitude: {d:.1}\n", .{v.magnitude()});
}
```

### Use a Heap Quando:

```zig
const std = @import("std");

const Node = struct {
    valor: i32,
    proximo: ?*Node,
};

fn criarLista(allocator: std.mem.Allocator, valores: []const i32) !?*Node {
    var cabeca: ?*Node = null;
    var atual: ?*Node = null;

    for (valores) |val| {
        const novo = try allocator.create(Node);
        novo.* = .{ .valor = val, .proximo = null };

        if (cabeca == null) {
            cabeca = novo;
        } else {
            atual.?.proximo = novo;
        }
        atual = novo;
    }
    return cabeca;
}

fn imprimirLista(lista: ?*Node) void {
    var atual = lista;
    while (atual) |node| {
        std.debug.print("{d} -> ", .{node.valor});
        atual = node.proximo;
    }
    std.debug.print("null\n", .{});
}

fn destruirLista(allocator: std.mem.Allocator, lista: ?*Node) void {
    var atual = lista;
    while (atual) |node| {
        const proximo = node.proximo;
        allocator.destroy(node);
        atual = proximo;
    }
}

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

    // Lista ligada — tamanho dinâmico, dados na heap
    const valores = [_]i32{ 10, 20, 30, 40, 50 };
    const lista = try criarLista(allocator, &valores);
    defer destruirLista(allocator, lista);

    imprimirLista(lista);
}
```

## O Padrão `defer` para Segurança de Memória

Um dos recursos mais poderosos de Zig para gerenciamento de memória é o `defer`. Ele garante que um trecho de código será executado quando o escopo atual terminar, independentemente de como isso aconteça (retorno normal ou erro).

```zig
const std = @import("std");

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

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

    fn deinit(self: *Recurso) void {
        self.allocator.free(self.dados);
        self.dados = &.{};
    }

    fn processar(self: *Recurso) !void {
        // Simulação de processamento
        for (self.dados, 0..) |*byte, i| {
            byte.* = @intCast(i % 256);
        }
        std.debug.print("Processados {d} bytes\n", .{self.dados.len});
    }
};

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

    // 'defer' garante que deinit() SEMPRE será chamado
    var recurso = try Recurso.init(allocator, 1024);
    defer recurso.deinit();

    // Mesmo que processar() falhe, deinit() será chamado
    try recurso.processar();

    std.debug.print("Recurso processado com sucesso!\n", .{});
}
```

O padrão é simples: **para cada `init`, tenha um `defer deinit`**. Isso é muito mais seguro do que o modelo de C, onde você precisa lembrar de chamar `free` em cada caminho de retorno.

## Sentinelas e Valores Opcionais

Zig usa ponteiros opcionais (`?*T`) para representar ponteiros que podem ser nulos. Isso elimina uma classe inteira de bugs:

```zig
const std = @import("std");

fn buscarValor(allocator: std.mem.Allocator, chave: []const u8) !?[]u8 {
    // Simula uma busca que pode não encontrar o valor
    if (std.mem.eql(u8, chave, "existente")) {
        const resultado = try allocator.alloc(u8, 5);
        @memcpy(resultado, "achei");
        return resultado;
    }
    return null; // Sem alocação, sem problema
}

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

    // Busca com resultado existente
    if (try buscarValor(allocator, "existente")) |valor| {
        defer allocator.free(valor);
        std.debug.print("Encontrado: {s}\n", .{valor});
    } else {
        std.debug.print("Não encontrado\n", .{});
    }

    // Busca sem resultado — nenhuma memória para liberar
    if (try buscarValor(allocator, "inexistente")) |valor| {
        defer allocator.free(valor);
        std.debug.print("Encontrado: {s}\n", .{valor});
    } else {
        std.debug.print("Não encontrado\n", .{});
    }
}
```

## Visualizando a Memória

Para entender melhor o layout de memória, podemos inspecionar endereços:

```zig
const std = @import("std");

var global: i32 = 100; // Segmento de dados

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

    const stack_var: i32 = 42; // Stack
    const heap_var = try allocator.create(i32); // Heap
    defer allocator.destroy(heap_var);
    heap_var.* = 99;

    std.debug.print("Endereços de memória:\n", .{});
    std.debug.print("  Global (dados): {*}\n", .{&global});
    std.debug.print("  Stack:          {*}\n", .{&stack_var});
    std.debug.print("  Heap:           {*}\n", .{heap_var});
    std.debug.print("\nValores:\n", .{});
    std.debug.print("  Global: {d}\n", .{global});
    std.debug.print("  Stack:  {d}\n", .{stack_var});
    std.debug.print("  Heap:   {d}\n", .{heap_var.*});
}
```

Ao rodar esse programa, você verá que os endereços estão em regiões completamente diferentes, refletindo as diferentes áreas de memória do processo.

## Resumo e Próximos Passos

Neste artigo, cobrimos:

- **Stack**: memória automática, rápida, de tamanho fixo
- **Heap**: memória dinâmica, flexível, gerenciada manualmente
- **`defer`**: o mecanismo de Zig para garantir limpeza de memória
- **Ponteiros opcionais**: eliminando null pointer bugs
- **Inspeção de endereços**: visualizando o layout de memória

No próximo artigo, vamos mergulhar nos **[tipos de allocators em Zig](/tutoriais/zig-memoria-masterclass/artigo-2-allocators-tipos/)** — entendendo cada allocator da biblioteca padrão e quando usar cada um.

## Leitura Complementar

- [Builtins de memória](/builtins/) — Funções built-in relacionadas a memória
- [Zig para Iniciantes](/tutoriais/zig-para-iniciantes/) — Se precisa revisar a sintaxe básica
- [Receitas: Trabalhando com Slices](/receitas/) — Exemplos práticos com slices
