Um vazamento de memória não é memória que sumiu. É memória que ainda é sua, ainda está reservada, e da qual você perdeu a capacidade de devolver - porque o último ponteiro para ela se foi. Nada trava. O programa continua rodando, um pouco mais pesado a cada vez, até que algo acabe falhando em um lugar não relacionado.
Esta página cobre como vazamentos surgem, a disciplina que previne a maioria deles e as duas ferramentas que acham o resto.
Como é um vazamento
Cada iteração sobrescreve block com um ponteiro novo. O bloco anterior continua alocado; nenhuma variável guarda seu endereço; ele nunca poderá ser liberado. Três iterações perdem doze kilobytes. Um servidor fazendo isso uma vez por requisição perde para sempre, na taxa em que as requisições chegarem.
A correção é uma linha - free(block); no fim do corpo - mas reconhecer onde ela cabe é a habilidade de verdade.
Como vazamentos acontecem
1. O ponteiro perdido
Qualquer atribuição a um ponteiro que ainda guarda a única referência para um bloco vivo o vaza.
char *name = malloc(32);
name = malloc(64); /* os primeiros 32 bytes agora são inalcançáveis */
O laço acima é o mesmo bug vestido de laço. O mesmo vale para reatribuir um campo de struct, e o mesmo vale para o atalho de realloc de calloc e realloc:
p = realloc(p, n); /* em caso de falha: p vira NULL, bloco antigo órfão */
2. O retorno antecipado
Todo caminho de saída de uma função tem que liberar o que a função já tomou. O que é esquecido é sempre um caminho de erro.
O caminho feliz está correto e o caminho de erro vaza, e é por isso que isso sobrevive aos testes: o ramo de falha quase nunca roda durante o desenvolvimento. A correção é uma única seção de limpeza para a qual cada caminho salta:
Este é o único uso de goto que programadores C experientes recomendam ativamente. Funciona porque todo ponteiro começa em NULL e free(NULL) não faz nada, então um único bloco de saída está correto não importa até onde a função tenha chegado.
3. Posse indefinida
Os vazamentos mais sutis não são erros de codificação - são duas funções discordando sobre de quem era o trabalho.
char *build_message(void); /* quem chama libera isso? */
void store(char *text); /* store assume a posse? */
Se build_message devolve memória alocada e store a copia, quem chama tem que liberar. Se store guarda o ponteiro, quem chama não deve liberar. Nada no código diz qual dos dois, então uma das duas suposições é feita duas vezes - e você ganha ou um vazamento ou um free duplo.
O remédio é uma convenção, declarada em um comentário ao lado de toda função que aloca:
/* Devolve uma string recém-alocada; quem chama deve liberá-la. */
char *build_message(void);
/* Assume a posse de 'text'; ele será liberado por store_free(). */
void store(char *text);
Escreva a regra na função, não em um documento de projeto. É o hábito de maior valor em gerenciamento de memória em C.
A disciplina de posse
Quatro regras cobrem quase tudo:
- Toda alocação tem exatamente um dono - um trecho de código responsável por liberá-la.
- Pareie cada função que aloca com uma que libera.
vec_init/vec_free,config_load/config_free. A simetria torna visível uma chamada faltando. - Libere na mesma camada que alocou, a menos que o comentário da função transfira a posse explicitamente.
- Ponha o ponteiro em
NULLdepois de liberá-lo, para que um uso acidental posterior trave no ponto da falha em vez de corromper o heap em silêncio.
Encontrando vazamentos: valgrind
No Linux, o valgrind não precisa de recompilação, embora símbolos de depuração deixem o relatório legível:
gcc -g -O0 program.c -o program
valgrind --leak-check=full --show-leak-kinds=all ./program
Para o laço que vaza no topo desta página, o relatório termina com algo assim:
==12345== HEAP SUMMARY:
==12345== in use at exit: 12,000 bytes in 3 blocks
==12345== total heap usage: 3 allocs, 0 frees, 12,000 bytes allocated
==12345==
==12345== 12,000 bytes in 3 blocks are definitely lost in loss record 1 of 1
==12345== at 0x4C2FB0F: malloc (vg_replace_malloc.c:299)
==12345== by 0x108671: main (program.c:6)
==12345==
==12345== LEAK SUMMARY:
==12345== definitely lost: 12,000 bytes in 3 blocks
Leia de baixo para cima. "definitely lost" significa que nenhum ponteiro para o bloco existia na saída - um vazamento de verdade. O rastro de pilha nomeia a linha do malloc que o criou, não a linha onde ele foi perdido, o que geralmente basta para achar o free que falta.
Aparecem outras duas categorias:
- indirectly lost - blocos alcançáveis apenas por um bloco que ele mesmo foi perdido, como os elementos de uma lista encadeada vazada. Conserte o "definitely lost" e estes somem.
- still reachable - alocados na saída mas com um ponteiro vivo, tipicamente um cache global. Não é um vazamento no sentido perigoso, mas vale liberar para que o relatório fique vazio.
O valgrind também pega leituras de memória não inicializada e escritas além do fim de um bloco, que muitas vezes é como você descobre o bug por trás do vazamento.
Encontrando vazamentos: AddressSanitizer
O AddressSanitizer é embutido no GCC e no Clang, roda muito mais rápido que o valgrind e funciona onde o valgrind não funciona (incluindo o macOS atual):
gcc -g -fsanitize=address -fno-omit-frame-pointer program.c -o program
./program
O relatório de vazamento é impresso automaticamente no encerramento:
=================================================================
==12345==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 12000 byte(s) in 3 object(s) allocated from:
#0 0x7f... in malloc
#1 0x1086... in main program.c:6
SUMMARY: AddressSanitizer: 12000 byte(s) leaked in 3 allocation(s).
O ASan também transforma use-after-free e estouros de buffer no heap em abortos imediatos e claramente rotulados, em vez de corrupções misteriosas mais tarde. Construa suas rodadas de teste com ele ligado; construa as releases com ele desligado, já que ele custa memória e velocidade.
Se a detecção de vazamentos não disparar na sua plataforma, defina ASAN_OPTIONS=detect_leaks=1 no ambiente antes de rodar.
Consertando um programa que vaza, passo a passo
Aqui está um pequeno programa com três vazamentos separados:
O valgrind relata três registros "definitely lost" com três números de linha diferentes. Consertados um de cada vez:
A correção 1 é o comentário de posse tornado real: shout aloca, main libera. A correção 2 remove a alocação dobrada por completo em vez de liberar a primeira - o código mais simples também é o código correto. A correção 3 acrescenta o free que faltava no retorno antecipado; com mais alocações em jogo, o rótulo único cleanup: mostrado antes escala melhor do que repetir os frees.
Hábitos que previnem vazamentos
- Escreva o
freeimediatamente depois de escrever omalloc, e só então preencha o código no meio. - Dê a cada função que aloca uma função de liberação correspondente.
- Declare a posse em um comentário em qualquer função que devolve ou recebe um ponteiro que ela alocou.
- Use um único bloco de saída
cleanup:em funções que guardam várias alocações. - Rode seus testes com
-fsanitize=addresspor hábito, não só quando algo parece errado. - Trate "definitely lost: 0 bytes" como parte de uma rodada de testes aprovada.
Perguntas frequentes
O que é um vazamento de memória em C?
Memória que você alocou com malloc e que não consegue mais liberar, porque nada no programa ainda aponta para ela. O bloco fica reservado por toda a vida do processo. Não é uma falha - o programa continua funcionando, só usando mais memória a cada passagem até acabar ficando sem.
Como encontro vazamentos de memória em C?
Rode o programa sob o valgrind: valgrind --leak-check=full ./program. Ele relata cada bloco ainda alocado na saída, com o rastro de pilha do malloc que o criou. No macOS ou onde o valgrind não está disponível, compile com -fsanitize=address e o mesmo relatório sai no encerramento.
O que causa vazamentos de memória em C?
Três padrões cobrem quase todos: sobrescrever o único ponteiro para um bloco (incluindo p = realloc(p, n) em caso de falha), retornar cedo de uma função que já alocou, e posse indefinida - duas funções cada uma supondo que a outra libera, então nenhuma libera.
Vazamentos de memória importam se o programa termina de qualquer jeito?
Para um programa que roda uma vez e termina, o sistema operacional recupera tudo, então o impacto prático é nulo. Importa para qualquer coisa de longa duração - um servidor, um laço de jogo, um daemon - onde um vazamento por requisição cresce sem limite. Libere de forma consistente mesmo assim: um relatório de vazamento é ruído que esconde os que realmente importam.