Você já sabe alocar e liberar memória. Mas saber a regra não impede de quebrá-la — e os erros de memória têm uma crueldade particular: eles se escondem. Um programa com um vazamento pode rodar perfeitamente por dias antes de engasgar. Um use-after-free pode devolver o valor certo em noventa e nove execuções e corromper tudo na centésima. Ler o código com atenção ajuda, mas não basta. Por isso, todo programador C sério trabalha ao lado de ferramentas que enxergam o invisível. Hoje você conhece as duas mais importantes.
O que é, exatamente, um vazamento
Um vazamento de memória (memory leak) acontece quando você aloca memória no heap e perde toda referência a ela sem tê-la liberado. A memória continua reservada — o sistema acha que você ainda a usa —, mas você já não tem nenhum ponteiro para alcançá-la. Ela fica presa, inútil, até o programa terminar:
#include <stdlib.h>
void vazar(void) {
int *p = malloc(100 * sizeof(int)); // aloca no heap
p[0] = 1;
// ...usa p...
} // p (a variável) morre aqui, mas os 400 bytes NÃO foram liberados: vazaram
int main(void) {
for (int i = 0; i < 1000000; i++) {
vazar(); // um milhão de vazamentos: a memória do programa só cresce
}
return 0;
}
O ponteiro p é uma variável local: quando vazar retorna, ele desaparece da pilha. Mas o bloco de 400 bytes que ele apontava vive no heap e não some com o retorno da função — e agora ninguém tem seu endereço. Multiplicado por um milhão de chamadas, o programa consome memória sem parar. Em um programa curto, um vazamento passa despercebido (o sistema recupera tudo ao final). Em um servidor que roda por meses, o mesmo vazamento derruba a máquina.
Por que os olhos não bastam
O exemplo acima é óbvio porque foi feito para ensinar. Na vida real, os vazamentos se escondem em caminhos que você não percebe:
char *processar(const char *entrada) {
char *buffer = malloc(256);
if (buffer == NULL) return NULL;
if (entrada[0] == '\0') {
return NULL; // VAZAMENTO: saímos sem liberar buffer!
}
// ...processa...
return buffer;
}
Aqui, o caminho normal aloca e devolve buffer corretamente. Mas o caminho de erro (entrada vazia) retorna NULL sem liberar buffer — a memória vaza toda vez que a entrada é vazia. Esse tipo de vazamento "em caminho de exceção" é insidioso: o código parece certo, funciona nos testes felizes, e só vaza em condições específicas. Nenhuma leitura casual pega todos os casos. É aqui que as ferramentas entram.
Valgrind: o detetive da memória
O Valgrind é uma ferramenta que executa seu programa dentro de uma máquina virtual instrumentada, vigiando cada alocação e cada acesso à memória. Ao final, ele relata exatamente o que vazou, onde foi alocado, e qualquer acesso inválido que tenha ocorrido. É a ferramenta mais valiosa do arsenal de um programador C, e usá-la é simples.
Primeiro, compile com informação de depuração (a opção -g), que permite ao Valgrind apontar as linhas exatas:
gcc -g programa.c -o programa
Depois, rode o programa através do Valgrind:
valgrind --leak-check=full ./programa
Para um programa com o vazamento da função vazar, o Valgrind imprime um relatório como este (resumido):
==12345== HEAP SUMMARY:
==12345== in use at exit: 400 bytes in 1 blocks
==12345== total heap usage: 1 allocs, 0 frees, 400 bytes allocated
==12345==
==12345== 400 bytes in 1 blocks are definitely lost in loss record 1 of 1
==12345== at 0x...: malloc
==12345== by 0x...: vazar (programa.c:5)
==12345== by 0x...: main (programa.c:12)
==12345==
==12345== LEAK SUMMARY:
==12345== definitely lost: 400 bytes in 1 blocks
Leia o que ele te entrega: quanto vazou (400 bytes), em quantos blocos (1), e — o mais precioso — onde aquela memória foi alocada (vazar, linha 5, chamada por main, linha 12). O Valgrind não adivinha onde você esqueceu o free, mas aponta onde a memória perdida nasceu, e isso quase sempre basta para encontrar a correção. A frase que você quer ver ao final de um programa saudável é: All heap blocks were freed -- no leaks are possible.
O Valgrind também pega os outros pecados
O Valgrind não se limita a vazamentos. Ele detecta também os acessos inválidos que estudamos: leitura ou escrita fora dos limites, uso de memória não inicializada, use-after-free e double-free. Para este código com um use-after-free:
int *p = malloc(sizeof(int));
*p = 5;
free(p);
printf("%d\n", *p); // uso após liberação
O Valgrind reporta um Invalid read of size 4, apontando que você leu de um bloco já liberado, e informando onde o bloco tinha sido liberado. Ele transforma o comportamento indefinido — que poderia passar despercebido — em uma mensagem clara e localizada.
Os sanitizers: rápidos e integrados
Há uma alternativa complementar embutida no próprio compilador: os sanitizers. O AddressSanitizer (ASan), que já mencionamos na aula de vetores, é ativado por uma flag de compilação e instrumenta seu programa para detectar erros de memória em tempo de execução — com desempenho muito melhor que o Valgrind:
gcc -g -fsanitize=address programa.c -o programa
./programa
Ao encontrar um erro, o ASan interrompe o programa na hora e imprime um relatório detalhado, com a pilha de chamadas do momento exato do erro. Para detectar vazamentos, o LeakSanitizer costuma vir junto do ASan e reporta a memória não liberada ao final. Há ainda o UndefinedBehaviorSanitizer (-fsanitize=undefined), que flagra comportamentos indefinidos como estouro de inteiros com sinal.
A diferença prática entre as duas abordagens: o Valgrind não exige recompilação e encontra mais categorias de erro, mas deixa o programa muito mais lento; os sanitizers exigem recompilar com a flag, são bem mais rápidos e integram bem em testes automatizados. Programadores experientes usam ambos — os sanitizers no dia a dia e nos testes, o Valgrind para uma auditoria profunda.
Uma disciplina que previne mais do que remedia
Ferramentas caçam vazamentos, mas o melhor vazamento é o que nunca acontece. Alguns hábitos reduzem drasticamente os erros de memória. Pense na liberação no momento em que aloca: assim que escreve um malloc, escreva mentalmente onde estará o free correspondente. Prefira um único ponto de saída em funções que alocam, para não esquecer o free em caminhos de erro (o padrão goto cleanup, comum em código C robusto e que veremos adiante, existe justamente para isso). E atribua NULL após liberar, como vimos, para transformar erros silenciosos em falhas visíveis. Nenhum desses hábitos substitui as ferramentas, mas juntos eles fazem com que você raramente precise delas em pânico.
Fechando a Fase 2
Com esta aula, encerramos a fase mais transformadora do curso. Você entrou nela sabendo declarar variáveis; sai dela entendendo endereços de memória, ponteiros, a diferença entre pilha e heap, a alocação dinâmica manual, e como caçar os erros que ela inevitavelmente produz. Essa base é o que torna possível tudo o que vem a seguir. Na próxima fase, vamos usar esse domínio para criar nossos próprios tipos de dados — começando pelas structs, que nos permitirão agrupar informações relacionadas e, mais adiante, construir as estruturas de dados que só existem graças aos ponteiros.
Fontes e leituras recomendadas
- Valgrind — Quick Start Guide e manual do Memcheck — https://valgrind.org/docs/manual/quick-start.html
- AddressSanitizer — documentação e exemplos — https://github.com/google/sanitizers/wiki/AddressSanitizer
- LeakSanitizer — detecção de vazamentos — https://github.com/google/sanitizers/wiki/AddressSanitizerLeakSanitizer
- The C Programming Language (K&R), Kernighan & Ritchie — Cap. 7 e 8, sobre uso responsável de memória
- CERT C — MEM31-C (liberar memória dinâmica quando não for mais necessária) — https://wiki.sei.cmu.edu/confluence/display/c
Exercícios
Exercício 1
Escreva um programa que aloque um vetor de 10 inteiros com malloc, use-o, e propositalmente esqueça o free. Compile com -g, rode sob valgrind --leak-check=full e leia o relatório. Quantos bytes o Valgrind diz que vazaram, e em que linha a memória foi alocada?
Ver resposta
✓ Resposta:
#include <stdlib.h>
int main(void) {
int *v = malloc(10 * sizeof(int));
for (int i = 0; i < 10; i++) v[i] = i;
// free(v) propositalmente omitido
return 0;
}
Rodando sob o Valgrind, o relatório indicará 40 bytes in 1 blocks are definitely lost (10 inteiros × 4 bytes), apontando a linha do malloc (a linha do int *v = malloc(...)) como o local da alocação perdida. O número exato de bytes depende do sizeof(int) da sua máquina — 40 no caso comum de int com 4 bytes.
Exercício 2
Corrija o programa do exercício 1 adicionando o free. Rode de novo sob o Valgrind e confirme que aparece a mensagem de que nenhum vazamento é possível.
Ver resposta
✓ Resposta: Basta adicionar free(v); antes do return 0;. Rodando sob o Valgrind novamente, você verá All heap blocks were freed -- no leaks are possible, e o resumo mostrará total heap usage: 1 allocs, 1 frees. O par alocação/liberação está equilibrado.
Exercício 3
Na função processar mostrada no artigo (que vaza no caminho da entrada vazia), reescreva-a de forma que buffer seja liberado em todos os caminhos de saída antes de retornar NULL.
Ver resposta
✓ Resposta: A forma mais limpa garante o free antes de qualquer retorno que abandone o buffer:
char *processar(const char *entrada) {
char *buffer = malloc(256);
if (buffer == NULL) return NULL;
if (entrada[0] == '\0') {
free(buffer); // libera antes de sair pelo caminho de erro
return NULL;
}
// ...processa e usa buffer...
return buffer; // caminho de sucesso: quem recebe é quem libera
}
A regra: qualquer caminho que não entregue o buffer ao chamador precisa liberá-lo antes de retornar. Em funções maiores, o idioma goto cleanup centraliza essa liberação num único ponto.
Exercício 4
Compile este trecho com gcc -g -fsanitize=address e rode. O que o AddressSanitizer relata, e em qual das duas linhas de free ele aponta o problema?
int *p = malloc(sizeof(int));
free(p);
free(p);
Ver resposta
✓ Resposta: O AddressSanitizer relata um erro attempting double-free (ou attempting free on address which was already freed), interrompendo o programa na segunda linha de free(p) — a que tenta liberar um bloco já liberado. Ele também mostra onde o bloco foi alocado e onde foi liberado da primeira vez, deixando a sequência do erro clara. O primeiro free é legítimo; o segundo é o double-free.
Exercício 5
Liste três hábitos de disciplina que reduzem a chance de vazamentos, e explique brevemente por que cada um ajuda.
Ver resposta
✓ Resposta: Três hábitos: (a) Escrever o free junto com o malloc — ao alocar, já decidir e anotar onde a memória será liberada evita que a liberação seja esquecida no calor do desenvolvimento. (b) Usar um único ponto de saída (ou goto cleanup) em funções que alocam — assim, todos os caminhos de retorno passam pela mesma liberação, incluindo os caminhos de erro, que são os que mais vazam. (c) Atribuir NULL ao ponteiro após liberar — não previne o vazamento em si, mas torna use-after-free e double-free imediatamente visíveis (o programa trava em vez de corromper silenciosamente), o que ajuda a manter a higiene de memória e a confiar nas verificações if (p != NULL).
Este é um tópico técnico; as ferramentas aqui descritas servem ao desenvolvimento de software seguro e confiável.