Memory leaks e como evitá-los com valgrind

1. O que são Memory Leaks em C

Memory leaks (vazamentos de memória) ocorrem quando um programa aloca memória dinamicamente durante sua execução, mas nunca a libera de volta ao sistema operacional. Em C, isso acontece tipicamente quando usamos funções como malloc, calloc ou realloc sem o correspondente free.

As consequências são graves: o programa consome cada vez mais memória ao longo do tempo, causando degradação de desempenho, estouro de memória (out-of-memory) e, eventualmente, crashes. Em sistemas embarcados ou servidores que rodam por meses, um memory leak pode ser catastrófico.

Exemplo simples de um memory leak:

#include <stdlib.h>

void cria_vazamento() {
    int *ptr = (int*) malloc(sizeof(int) * 100);
    // Nunca liberamos ptr!
}

int main() {
    cria_vazamento();
    // 400 bytes (100 ints * 4 bytes) estão perdidos para sempre
    return 0;
}

2. Como a memória dinâmica funciona na prática

Em C, a memória é dividida em stack (pilha) e heap (monte). Variáveis locais e parâmetros de função vivem na stack, que é gerenciada automaticamente. Já a memória alocada com malloc, calloc e realloc vive no heap, e sua liberação é responsabilidade do programador.

Ciclo de vida esperado da memória dinâmica:

  1. Alocar: malloc(n) ou calloc(n, tamanho) reserva um bloco contíguo no heap
  2. Usar: acessar e modificar a memória através do ponteiro retornado
  3. Liberar: free(ptr) devolve o bloco ao heap para reutilização

Exemplo correto:

#include <stdlib.h>

int main() {
    int *dados = (int*) malloc(10 * sizeof(int));
    if (dados == NULL) return 1;

    for (int i = 0; i < 10; i++) dados[i] = i * 2;

    free(dados);  // Liberação obrigatória
    return 0;
}

3. Identificando Memory Leaks manualmente

Sintomas comuns de vazamento de memória:
- O programa consome mais memória com o passar do tempo
- O uso de memória nunca diminui, mesmo após operações que deveriam liberar recursos
- O sistema fica lento ou trava após execução prolongada

Técnicas básicas de rastreamento manual:
- Para cada malloc, identifique visualmente onde o free correspondente deve estar
- Garanta que todos os caminhos de execução (incluindo tratamento de erros) liberem a memória
- Mantenha uma contagem manual de alocações e liberações

A principal armadilha são os ponteiros perdidos: quando você sobrescreve o único ponteiro que aponta para um bloco de memória antes de liberá-lo, esse bloco fica inacessível para sempre.

int *ptr = malloc(100);
ptr = malloc(200);  // O primeiro bloco (100 bytes) está perdido!
free(ptr);          // Só libera o segundo bloco

4. Introdução ao Valgrind

Valgrind é uma ferramenta poderosa de análise de memória para programas em C e C++ no Linux. Seu módulo memcheck detecta automaticamente memory leaks, acesso a memória não inicializada, uso após free, e muito mais.

Instalação (Ubuntu/Debian):

sudo apt update
sudo apt install valgrind

Uso básico:

valgrind --leak-check=yes ./meu_programa

Vamos testar com um programa que tem vazamento:

// vazamento.c
#include <stdlib.h>
#include <stdio.h>

int main() {
    char *buffer = (char*) malloc(50);
    sprintf(buffer, "Hello, Valgrind!");
    printf("%s\n", buffer);
    // Esquecemos de free(buffer)
    return 0;
}

5. Interpretando a saída do Valgrind

Ao executar valgrind --leak-check=yes ./vazamento, você verá algo como:

==12345== HEAP SUMMARY:
==12345==     in use at exit: 50 bytes in 1 blocks
==12345==   total heap usage: 1 allocs, 0 frees, 50 bytes allocated
==12345== 
==12345== 50 bytes in 1 blocks are definitely lost in loss record 1 of 1
==12345==    at 0x4C2B0E0: malloc (vg_replace_malloc.c:309)
==12345==    by 0x4005E7: main (vazamento.c:6)
==12345== 
==12345== LEAK SUMMARY:
==12345==    definitely lost: 50 bytes in 1 blocks
==12345==    indirectly lost: 0 bytes in 0 blocks
==12345==    possibly lost: 0 bytes in 0 blocks
==12345==    still reachable: 0 bytes in 0 blocks

Principais categorias de vazamento:

  • definitely lost: vazamento real e confirmado. A memória foi alocada e nunca liberada, e não há mais ponteiros para ela. Corrigir imediatamente.
  • indirectly lost: memória perdida indiretamente porque o ponteiro que a apontava também está perdido (ex: nós de uma lista encadeada).
  • possibly lost: vazamento possível, mas o Valgrind não tem certeza. Geralmente ocorre com ponteiros encadeados de forma complexa.
  • still reachable: a memória ainda tem um ponteiro válido, mas nunca foi liberada. Pode ser aceitável em programas que terminam rapidamente, mas é má prática.

6. Estratégias para evitar Memory Leaks

Padrão de alocação e liberação pareadas: todo malloc deve ter um free correspondente. Uma boa prática é escrever o free imediatamente após o malloc, antes mesmo de preencher o código intermediário.

int *dados = malloc(100 * sizeof(int));
// ... processamento ...
free(dados);

Funções auxiliares para gerenciamento:

void libera_tudo(void **ptr1, void **ptr2) {
    if (*ptr1) { free(*ptr1); *ptr1 = NULL; }
    if (*ptr2) { free(*ptr2); *ptr2 = NULL; }
}

int main() {
    int *a = malloc(10);
    int *b = malloc(20);
    // ... uso ...
    libera_tudo((void**)&a, (void**)&b);
    return 0;
}

Boas práticas:
- Inicialize ponteiros com NULL após a declaração
- Após free, atribua NULL ao ponteiro para evitar uso acidental
- Sempre verifique o retorno de malloc (pode ser NULL se a alocação falhar)

7. Casos comuns que geram vazamentos

Retorno antecipado sem liberação:

int processa() {
    int *dados = malloc(1000);
    if (algum_erro) return -1;  // Vazamento! dados não foi liberado
    free(dados);
    return 0;
}

Correção: libere antes de retornar.

Vazamentos em estruturas encadeadas (listas, árvores):

typedef struct No {
    int valor;
    struct No *prox;
} No;

void libera_lista(No *inicio) {
    No *atual = inicio;
    while (atual != NULL) {
        No *prox = atual->prox;
        free(atual);  // Só libera o nó, e os dados internos?
        atual = prox;
    }
}

Se cada nó contiver um ponteiro para memória alocada separadamente, é necessário liberar também essa memória antes de liberar o nó.

Alocação aninhada incompleta:

typedef struct {
    char *nome;
    int *notas;
} Aluno;

Aluno* cria_aluno() {
    Aluno *a = malloc(sizeof(Aluno));
    a->nome = malloc(50);
    a->notas = malloc(10 * sizeof(int));
    return a;
}

void libera_aluno(Aluno *a) {
    free(a->nome);
    free(a->notas);
    free(a);  // Agora sim, liberação completa
}

8. Valgrind como aliado no desenvolvimento

Integre o Valgrind ao seu fluxo de desenvolvimento:

# Compile com informações de debug
gcc -g -o meu_prog meu_prog.c

# Execute com Valgrind
valgrind --leak-check=yes --show-reachable=yes ./meu_prog

Para testes automatizados, use a flag --error-exitcode=1 para que o Valgrind retorne código de erro se houver vazamentos:

valgrind --leak-check=yes --error-exitcode=1 ./meu_prog

Limitações do Valgrind:
- Não detecta todos os problemas (ex: uso após free pode passar despercebido em alguns casos)
- O programa roda 10 a 30 vezes mais devagar
- Funciona apenas em Linux (existem alternativas como AddressSanitizer para outros sistemas)

Dica final: rode o Valgrind regularmente durante o desenvolvimento, não apenas no final. Corrigir memory leaks logo que aparecem é muito mais fácil do que depurar um programa inteiro com dezenas de vazamentos.

Referências