Trilha 06 · Memória e execução

Heap e alocação dinâmica

A stack é automática mas limitada. O heap é manual mas ilimitado. malloc, new, free, delete — e o problema da fragmentação que surge quando você aloca e libera muitas vezes.

① Intuição

Quando a stack não é suficiente

A stack é conveniente: você declara uma variável e ela some sozinha quando a função retorna. Mas tem dois limites sérios. Primeiro, o tamanho precisa ser conhecido em tempo de compilação — você não pode declarar int arr[n] se n só é conhecido em runtime. Segundo, a vida útil está amarrada ao frame — o dado desaparece quando a função retorna, mesmo que outros trechos do programa ainda precisem dele.

O heap resolve os dois: você aloca qualquer tamanho em runtime e o bloco persiste até você explicitamente liberá-lo. O preço é responsabilidade: você precisa lembrar de chamar free(). Esquecer = memory leak. Liberar duas vezes = heap corrompido. Usar após liberar = undefined behavior.

Fragmentação: ao alternar alocações e liberações de tamanhos variados, o heap pode ficar cheio de "buracos" pequenos que, individualmente, não servem para nenhuma nova alocação maior — mesmo que a soma do espaço livre seja suficiente. É como um estacionamento com muitas vagas pequenas espalhadas mas nenhuma sequência grande para um caminhão.
② Visualização interativa

Aloque, libere, observe a fragmentação

Cada célula do grid representa 4 bytes. Aloque blocos de tamanhos diferentes e depois libere-os em ordem alternada para ver a fragmentação emergir.

malloc:
Total: 256 BUsado: 0 BLivre: 256 B
Clique num bloco alocado (ou na legenda) para selecionar → depois em "free" para liberar. Veja a fragmentação aparecer após múltiplos alloc/free.
③ Explicação técnica

malloc, calloc, realloc, free

// Alocação dinâmica em C
int* arr = malloc(n * sizeof(int));  // aloca n inteiros no heap
if (arr == NULL) { perror("malloc"); exit(1); }  // sempre checar!

arr[0] = 42;
arr[n - 1] = 99;

free(arr);      // devolve ao alocador
arr = NULL;     // evita dangling pointer

// calloc: aloca E zera
int* zeros = calloc(n, sizeof(int));

// realloc: redimensiona (pode mover o bloco!)
arr = realloc(arr, 2 * n * sizeof(int));
// atenção: o ponteiro antigo pode ser invalidado

Por dentro do alocador

// O que o alocador de memória faz por dentro

// O heap é uma lista de blocos livres e usados:
// [USADO·16B][LIVRE·32B][USADO·8B][LIVRE·64B][LIVRE·16B]...

// malloc(20): busca first-fit ou best-fit na lista de livres
//  → acha o bloco de 32B
//  → divide: [USADO·20B][LIVRE·12B]
//  → retorna ponteiro para os 20B

// free(ptr):
//  → marca o bloco como livre
//  → tenta fundir com blocos livres adjacentes (coalescência)
//  [LIVRE·20B][LIVRE·12B] → [LIVRE·32B]  ← evita fragmentação

// Fragmentação externa: muitos buracos pequenos, nenhum grande
// o suficiente para a próxima alocação — mesmo que a soma seja ok
// [LIVRE·8B][USADO·20B][LIVRE·8B] → malloc(16) falha!
jemalloc, tcmalloc, mimalloc: aplicações de alta performance (Firefox, Chrome, Redis) substituem o alocador padrão da libc por implementações especializadas que reduzem fragmentação, melhoram localidade de cache e escalam melhor em programas multi-thread — usando arenas por thread para evitar contenção de lock na lista global de livres.
④ Projeto para programar

Construindo um alocador simples

Mini projeto: implemente um my_malloc e my_free em C usando um array estático de 4096 bytes como "heap simulado". Mantenha uma lista encadeada de blocos livres e use first-fit para alocar. Imprima o estado da lista após cada operação.

Projeto principal: escreva um alocador de pool (pool allocator): pré-aloca um bloco grande e entrega sempre blocos de tamanho fixo (ex: 64 bytes). Compare latência de alocação com malloc padrão em 1 milhão de alocações/liberações. Por que o pool é mais rápido? Quando não é a escolha certa?

Desafio extra: adicione ao seu alocador detecção de double-free: ao liberar, verifique se o bloco já está marcado como livre. Adicione também um "canary value" (valor mágico) no final de cada bloco alocado e verifique na hora do free se o canary foi sobrescrito — indicando buffer overflow.

⑤ Exercícios rápidos

Teste sua intuição

Por que usamos malloc para criar um array de tamanho n sendo n lido do usuário?
O que acontece com um bloco alocado com malloc se você nunca chamar free()?
Quando a fragmentação do heap se torna um problema?
⑥ Aplicações no mundo real

Onde você encontra isso

🗄️

Bancos de dados

Sistemas como PostgreSQL gerenciam seu próprio pool de memória (palloc/pfree) em vez de usar malloc diretamente. Isso permite liberar toda a memória de uma transação de uma só vez com uma chamada — sem rastrear cada alocação individual — e evita fragmentação do heap do alocador padrão.

🧠

Arenas em compiladores

LLVM e GCC usam "arenas" (bump allocators): alocam um bloco grande de uma vez e distribuem objetos sequencialmente com um simples incremento de ponteiro. Liberar todos os objetos de uma fase de compilação é uma única operação. Alocação é O(1) e fragmentação zero — ao custo de não poder liberar objetos individualmente.

🔧

C++ smart pointers

unique_ptr libera automaticamente ao sair de escopo (sem GC). shared_ptr usa contagem de referências para múltiplos donos. weak_ptr observa sem segurar. Em código moderno de C++, malloc e free manual são raros — os smart pointers encapsulam o ciclo de vida do heap com a segurança de tipos em tempo de compilação.

← Anterior: A pilha de chamadas Próxima: Ponteiros e referências →