C Vazamentos de Memória
Um vazamento de memória (memory leak) acontece quando o programa reserva memória com malloc, calloc ou realloc e nunca a devolve com free. Pior: quase sempre o programa já perdeu o endereço do bloco e nem conseguiria devolvê-lo. Um vazamento não dá erro, não dá aviso e não muda a saída: o programa só vai ficando mais pesado, até o computador ficar lento ou a memória acabar. Nesta aula você vai ver um vazamento acontecer, medir o tamanho dele, achar a linha culpada com duas ferramentas e conhecer os quatro jeitos mais comuns de vazar.
- O que é um vazamento e por que ele passa despercebido.
- A medir a memória de um programa com
/usr/bin/time. - A achar a linha do
mallocque vazou com o Valgrind e com o LeakSanitizer (-fsanitize=address). - Os jeitos clássicos de vazar: o
freeesquecido, o ponteiro trocado, a saída no meio da função e a struct com um ponteiro dentro. - Os hábitos que evitam vazamentos desde o começo.
Esta aula usa o que você viu em malloc e free e calloc e realloc. As ferramentas têm aulas próprias, com todos os detalhes dos relatórios: Valgrind e Sanitizers; aqui o foco é o vazamento em si. Todos os programas foram compilados com o gcc 15.2 na máquina fictícia da Ana (usuária ana, computador notebook), na pasta ~/c/vaza, com o comando padrão do guia mais -g (para as ferramentas mostrarem as linhas): gcc -std=c23 -Wall -Wextra -g arquivo.c -o programa.
A ideia: as chaves perdidas do guarda-volumes
No guarda-volumes da aula malloc e free, cada armário só volta a ficar livre quando alguém devolve a chave. Imagine um cliente distraído que pega um armário, guarda as compras, vai embora e perde a chave no caminho. O armário fica ocupado para sempre: o cliente não sabe mais qual era, e o atendente não pode dá-lo a outra pessoa. Um cliente assim por dia não faz diferença. Mas se ele vem todo dia, um dia o guarda-volumes lota e ninguém mais consegue guardar nada.
É exatamente isso que um vazamento faz. Uma única reserva esquecida num programa que roda por 1 segundo não incomoda ninguém, porque quando o programa termina o sistema operacional recolhe toda a memória dele. O problema é o vazamento que se repete (dentro de um laço, numa função chamada muitas vezes) num programa que roda por muito tempo: um servidor, um jogo, um editor de texto, o navegador.
1. Um vazamento que ninguém vê
A lojinha da Ana processa os pedidos do dia. Para cada pedido, a função processar monta um recibo num bloco de 1 MB (um milhão de bytes):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define TAMANHO 1000000
int processar(int numero) {
char *recibo = malloc(TAMANHO);
if (recibo == NULL) {
return -1;
}
memset(recibo, ' ', TAMANHO);
int escritos = snprintf(recibo, TAMANHO, "Recibo do pedido %d", numero);
return escritos;
}
int main(void) {
for (int i = 1; i <= 1000; i++) {
if (processar(i) < 0) {
printf("Faltou memória no pedido %d!\n", i);
return 1;
}
}
printf("1000 pedidos processados.\n");
return 0;
}
- char *recibo = malloc(TAMANHO);
- Reserva 1 MB no heap. O endereço fica em
recibo, uma variável local: ela mora na pilha e some quandoprocessartermina (aula Pilha e Heap). - memset(recibo, ' ', TAMANHO);
- Enche o bloco de espaços, como se o recibo estivesse sendo montado. (Isso também obriga o sistema a entregar a memória de verdade; um bloco nunca tocado às vezes nem chega a ocupar espaço.)
- snprintf(recibo, TAMANHO, …)
- Escreve o texto do recibo no começo do bloco (aula Strings Dinâmicas) e devolve quantas letras escreveu.
- return escritos;
- Aqui está o vazamento: a função termina sem
free(recibo). A variávelrecibosome, e com ela o único lugar onde o endereço do bloco estava guardado. O bloco continua reservado, e ninguém mais sabe onde ele está.
$ gcc -std=c23 -Wall -Wextra -g pedidos.c -o pedidos « nenhum aviso $ ./pedidos 1000 pedidos processados.
Nenhum aviso na compilação, nenhum erro ao rodar, a saída certa. Para ver o problema, é preciso medir. O comando /usr/bin/time (o programa time, não a palavra do bash; por isso o caminho completo) roda outro programa e, com -f "%M", mostra a memória máxima que ele usou, em kilobytes:
$ /usr/bin/time -f "Memória máxima: %M KB" ./pedidos 1000 pedidos processados. Memória máxima: 981752 KB « quase 1 GB!
Quase 1 GB para um programa que, a cada momento, só precisa de 1 recibo de 1 MB. Com um free(recibo); antes do return (no arquivo pedidos2.c), cada bloco é devolvido e reaproveitado pelo pedido seguinte:
$ /usr/bin/time -f "Memória máxima: %M KB" ./pedidos2 1000 pedidos processados. Memória máxima: 2644 KB « 2,6 MB: o programa todo, mais um recibo
Num servidor que atende um milhão de pedidos por dia, a primeira versão precisaria de 1 TB de memória. Muito antes disso, o sistema começaria a ficar lento e, no fim, o Linux mataria o programa por falta de memória.
O editor dá a cada programa no máximo 256 MB. Com o vazamento, o malloc começa a devolver NULL no meio do caminho, e o mesmo pedidos.c mostra Faltou memória no pedido 261!, com código de saída 1. É um dos poucos jeitos de ver um vazamento no editor: ele não tem Valgrind nem sanitizers, e um vazamento que não esgota os 256 MB passa calado.
Missão: rode e veja a memória do editor acabar no pedido 261. Conserte o vazamento: devolva o bloco do recibo em processar, depois do snprintf e antes do return. A saída deve ser 1000 pedidos processados.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define TAMANHO 1000000
int processar(int numero) {
char *recibo = malloc(TAMANHO);
if (recibo == NULL) {
return -1;
}
memset(recibo, ' ', TAMANHO);
int escritos = snprintf(recibo, TAMANHO, "Recibo do pedido %d", numero);
return escritos;
}
int main(void) {
for (int i = 1; i <= 1000; i++) {
if (processar(i) < 0) {
printf("Faltou memória no pedido %d!\n", i);
return 1;
}
}
printf("1000 pedidos processados.\n");
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define TAMANHO 1000000
int processar(int numero) {
char *recibo = malloc(TAMANHO);
if (recibo == NULL) {
return -1;
}
memset(recibo, ' ', TAMANHO);
int escritos = snprintf(recibo, TAMANHO, "Recibo do pedido %d", numero);
free(recibo);
return escritos;
}
int main(void) {
for (int i = 1; i <= 1000; i++) {
if (processar(i) < 0) {
printf("Faltou memória no pedido %d!\n", i);
return 1;
}
}
printf("1000 pedidos processados.\n");
return 0;
}
2. Achar a linha: o LeakSanitizer e o Valgrind
Medir mostra que há vazamento, mas não onde. Num programa de verdade, com centenas de malloc, você precisa da linha. As duas ferramentas do Linux dão a linha do malloc que nunca teve free.
LeakSanitizer: -fsanitize=address
O AddressSanitizer (aula Sanitizers) traz junto o LeakSanitizer, que confere a memória quando o programa termina. Basta recompilar:
$ gcc -std=c23 -Wall -Wextra -g -fsanitize=address pedidos.c -o pedidos-asan
$ ./pedidos-asan
1000 pedidos processados.
=================================================================
==439612==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 1000000000 byte(s) in 1000 object(s) allocated from:
#0 0x7f665c92b60f in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:67
#1 0x5645d743a261 in processar /home/ana/c/vaza/pedidos.c:8
#2 0x5645d743a2d5 in main /home/ana/c/vaza/pedidos.c:19
#3 0x7f665c42a600 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:59
#4 0x7f665c42a717 in __libc_start_main_impl ../csu/libc-start.c:360
#5 0x5645d743a184 in _start (/home/ana/c/vaza/pedidos-asan+0x1184) (BuildId: 5615ba55c0c0970f16ca54ed19c67e78d5fc947a)
SUMMARY: AddressSanitizer: 1000000000 byte(s) leaked in 1000 allocation(s).
$ echo $?
1
- 1000 pedidos processados.
- O programa rodou inteiro; o relatório vem depois, na saída.
- ERROR: LeakSanitizer: detected memory leaks
- "Vazamentos de memória detectados".
- Direct leak of 1000000000 byte(s) in 1000 object(s)
- "Vazamento direto de 1 bilhão de bytes em 1000 blocos". Direto quer dizer que nenhum ponteiro aponta mais para esses blocos. Os 1000 blocos saíram do mesmo lugar, por isso viram uma linha só no relatório.
- #1 … in processar /home/ana/c/vaza/pedidos.c:8
- A pilha de chamadas de onde o bloco foi reservado, do mais recente para o mais antigo. A linha
#0é o própriomalloc; a primeira linha com um arquivo seu (#1) é a pista: omalloc(TAMANHO)da linha 8. As linhas#3a#5são o começo de todo programa, antes domain; ignore. - echo $? → 1
- Mesmo com a saída certa, o código de saída vira 1. Num teste automático (aula Testes), o vazamento faz o teste falhar.
O relatório diz onde o bloco nasceu, não onde ele deveria ter sido liberado: isso é com você. Pergunte "até quando este bloco precisa existir?" e ponha o free ali. Aqui, até o fim de processar.
-fsanitize=leak
Se você só quer saber de vazamentos, -fsanitize=leak liga o LeakSanitizer sozinho, sem o resto do ASan. O programa fica quase tão rápido quanto o normal, e o relatório é o mesmo (o resumo diz SUMMARY: LeakSanitizer: …). No dia a dia, prefira -fsanitize=address, que pega também os outros erros de memória.
Valgrind: sem recompilar
O Valgrind roda o programa como ele está (com -g, para mostrar as linhas) e confere tudo no fim. A opção que mostra onde cada bloco vazado nasceu é --leak-check=full. Ele aparece na próxima seção, em cada um dos jeitos de vazar.
LeakSanitizer (-fsanitize=address ou leak) | Valgrind (--leak-check=full) | |
|---|---|---|
| Precisa recompilar? | Sim | Não (só o -g, para as linhas) |
| Velocidade | Quase normal | 10 a 50 vezes mais lento |
| O que diz | Direct leak / Indirect leak, com a pilha do malloc | definitely lost / indirectly lost / still reachable, com a pilha do malloc |
| Código de saída | 1 se vazou | O do programa (ou o que você pedir com --error-exitcode=1) |
| No editor do guia | Nenhum dos dois funciona. Só o limite de 256 MB denuncia um vazamento grande. | |
3. Os jeitos clássicos de vazar
O free esquecido no fim da função você já viu. Há outros três jeitos, mais traiçoeiros, porque o free está no código, só que não alcança todos os blocos.
a) Trocar o ponteiro sem liberar o bloco antigo
A Ana reaproveitou a variável cliente para o segundo cliente, com um malloc novo:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void) {
char *cliente = malloc(20);
if (cliente == NULL) {
return 1;
}
strcpy(cliente, "Ana");
printf("Atendendo %s\n", cliente);
cliente = malloc(20);
if (cliente == NULL) {
return 1;
}
strcpy(cliente, "Bia");
printf("Atendendo %s\n", cliente);
free(cliente);
return 0;
}
$ gcc -std=c23 -Wall -Wextra -g trocou.c -o trocou $ valgrind --leak-check=full ./trocou ==438763== Memcheck, a memory error detector ==438763== Copyright (C) 2002-2024, and GNU GPL'd, by Julian Seward et al. ==438763== Using Valgrind-3.26.0 and LibVEX; rerun with -h for copyright info ==438763== Command: ./trocou ==438763== Atendendo Ana Atendendo Bia ==438763== ==438763== HEAP SUMMARY: ==438763== in use at exit: 20 bytes in 1 blocks ==438763== total heap usage: 3 allocs, 2 frees, 4,136 bytes allocated ==438763== ==438763== 20 bytes in 1 blocks are definitely lost in loss record 1 of 1 ==438763== at 0x4850858: malloc (vg_replace_malloc.c:447) ==438763== by 0x400119E: main (trocou.c:6) ==438763== ==438763== LEAK SUMMARY: ==438763== definitely lost: 20 bytes in 1 blocks ==438763== indirectly lost: 0 bytes in 0 blocks ==438763== possibly lost: 0 bytes in 0 blocks ==438763== still reachable: 0 bytes in 0 blocks ==438763== suppressed: 0 bytes in 0 blocks ==438763== ==438763== For lists of detected and suppressed errors, rerun with: -s ==438763== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
- in use at exit: 20 bytes in 1 blocks
- Quando o programa terminou, 1 bloco de 20 bytes ainda estava reservado.
- total heap usage: 3 allocs, 2 frees
- 3 reservas (os dois
malloc(20)e o buffer que oprintfreserva para ele mesmo) e 2 liberações. Falta uma. - 20 bytes in 1 blocks are definitely lost … main (trocou.c:6)
- O bloco perdido é o da linha 6, o da Ana. O
freeda linha 20 liberou o da Bia, que era o único endereço ainda guardado emcliente. A linha 13 escreveu o endereço novo por cima do antigo, e o bloco da Ana ficou sem ninguém apontando para ele.
O conserto: antes de reaproveitar o ponteiro, devolva o que ele aponta (free(cliente); antes da linha 13). Ou, melhor ainda neste caso, nem reserve de novo: o bloco de 20 bytes serve para a Bia também (strcpy(cliente, "Bia");). A regra geral é: antes de escrever um endereço novo num ponteiro que é dono de um bloco, libere o bloco. O p = realloc(p, …) que falha é este mesmo erro, com o NULL por cima do endereço antigo (aula Array que Cresce).
b) Sair no meio da função
A função media tem um free, lá no fim. Mas ela tem uma saída antecipada, para nota inválida, que pula o fim:
#include <stdio.h>
#include <stdlib.h>
/* Lê n notas e devolve a média; devolve -1 se alguma nota for inválida. */
double media(int n) {
double *notas = malloc(n * sizeof *notas);
if (notas == NULL) {
return -1;
}
for (int i = 0; i < n; i++) {
if (scanf("%lf", ¬as[i]) != 1 || notas[i] < 0 || notas[i] > 10) {
return -1;
}
}
double soma = 0;
for (int i = 0; i < n; i++) {
soma += notas[i];
}
free(notas);
return soma / n;
}
int main(void) {
double m = media(3);
if (m < 0) {
printf("Nota inválida.\n");
return 1;
}
printf("Média: %.2f\n", m);
return 0;
}
Com notas boas, nada vaza. Com uma nota 11, a função sai pelo return -1 do laço, e o bloco fica para trás. O printf … | manda as três notas para o programa como se fossem digitadas:
$ gcc -std=c23 -Wall -Wextra -g saida.c -o saida $ printf '7 11 8\n' | valgrind --leak-check=full ./saida «… o cabeçalho …» Nota inválida. ==438807== ==438807== HEAP SUMMARY: ==438807== in use at exit: 24 bytes in 1 blocks ==438807== total heap usage: 3 allocs, 2 frees, 8,216 bytes allocated ==438807== ==438807== 24 bytes in 1 blocks are definitely lost in loss record 1 of 1 ==438807== at 0x4850858: malloc (vg_replace_malloc.c:447) ==438807== by 0x40011E8: media (saida.c:6) ==438807== by 0x4001314: main (saida.c:24) ==438807== «… o LEAK SUMMARY: definitely lost: 24 bytes in 1 blocks …»
Os 24 bytes são as 3 notas (3 × 8 bytes) do malloc da linha 6. Repare que este vazamento só aparece com a entrada errada: rodar o Valgrind só com notas boas não acharia nada. Teste os caminhos de erro também.
Há duas formas de consertar. A mais simples: free(notas); antes de cada return que vem depois do malloc. A outra, comum em funções com muitas saídas (o núcleo do Linux é cheio delas), é ter uma saída só, com o free lá, e pular para ela com goto:
double media(int n) {
double resultado = -1;
double soma = 0;
double *notas = malloc(n * sizeof *notas);
if (notas == NULL) {
return -1;
}
for (int i = 0; i < n; i++) {
if (scanf("%lf", ¬as[i]) != 1 || notas[i] < 0 || notas[i] > 10) {
goto fim;
}
}
for (int i = 0; i < n; i++) {
soma += notas[i];
}
resultado = soma / n;
fim:
free(notas);
return resultado;
}
- double resultado = -1;
- O valor de "deu errado" já fica guardado desde o começo. Só muda no fim, se tudo der certo.
- goto fim;
- "Vá para
fim": o programa pula direto para a linha marcada comfim:, sem passar pelo resto. Ogototem má fama porque, usado à vontade, deixa o código impossível de seguir. Para pular para a limpeza no fim da função, que é sempre para baixo e sempre para o mesmo lugar, ele é um costume aceito em C. - fim: free(notas); return resultado;
- A única saída depois do
malloc. Todo caminho passa pelofree.
$ printf '7 11 8\n' | valgrind --leak-check=full ./saida2 «…» Nota inválida. ==441580== ==441580== HEAP SUMMARY: ==441580== in use at exit: 0 bytes in 0 blocks ==441580== total heap usage: 3 allocs, 3 frees, 8,216 bytes allocated ==441580== ==441580== All heap blocks were freed -- no leaks are possible «…» $ printf '7 9 8\n' | ./saida2 Média: 8.00
c) Liberar a caixa e esquecer o que tem dentro
Quando uma struct tem um ponteiro para outro bloco, são dois blocos, e cada um precisa do seu free. A ficha de aluno da Ana guarda o nome num bloco próprio, do tamanho exato:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct aluno {
char *nome;
double nota;
};
struct aluno *novo_aluno(const char *nome, double nota) {
struct aluno *a = malloc(sizeof *a);
if (a == NULL) {
return NULL;
}
a->nome = malloc(strlen(nome) + 1);
if (a->nome == NULL) {
free(a);
return NULL;
}
strcpy(a->nome, nome);
a->nota = nota;
return a;
}
int main(void) {
struct aluno *a = novo_aluno("Ana Beatriz", 9.5);
if (a == NULL) {
return 1;
}
printf("%s: %.1f\n", a->nome, a->nota);
free(a);
return 0;
}
O novo_aluno faz dois malloc: um para a struct (linha 11) e um para o nome (linha 15; a seta -> está em Structs e Ponteiros). Repare que ele já cuida de um vazamento: se o segundo malloc falhar, ele libera o primeiro antes de devolver NULL. Mas o main só libera a struct:
$ gcc -std=c23 -Wall -Wextra -g ficha.c -o ficha
$ valgrind --leak-check=full ./ficha
«… o cabeçalho …»
Ana Beatriz: 9.5
==438830==
==438830== HEAP SUMMARY:
==438830== in use at exit: 12 bytes in 1 blocks
==438830== total heap usage: 3 allocs, 2 frees, 4,124 bytes allocated
==438830==
==438830== 12 bytes in 1 blocks are definitely lost in loss record 1 of 1
==438830== at 0x4850858: malloc (vg_replace_malloc.c:447)
==438830== by 0x4001211: novo_aluno (ficha.c:15)
==438830== by 0x400128B: main (ficha.c:26)
«…»
$ gcc -std=c23 -Wall -Wextra -g -fsanitize=address ficha.c -o ficha-asan
$ ./ficha-asan
Ana Beatriz: 9.5
=================================================================
==439632==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 12 byte(s) in 1 object(s) allocated from:
#0 0x7f410512b60f in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:67
#1 0x55c6971712d4 in novo_aluno /home/ana/c/vaza/ficha.c:15
#2 0x55c6971713b5 in main /home/ana/c/vaza/ficha.c:26
«…»
SUMMARY: AddressSanitizer: 12 byte(s) leaked in 1 allocation(s).
As duas ferramentas apontam a linha 15: o nome ("Ana Beatriz": 11 letras + o '\0' = 12 bytes). O endereço do nome estava dentro da struct; quando a struct foi liberada, ele se perdeu junto. Se a struct não tivesse sido liberada, o Valgrind diria indirectly lost ("perdido por tabela") para o nome, como na lista ligada da aula Valgrind.
O conserto é uma função que desmonta a ficha na ordem inversa da montagem: primeiro o que está dentro, depois a caixa. Ao contrário não dá: depois do free(a), ler a->nome já é usar memória liberada.
Missão: escreva o corpo de liberar_aluno: libere o nome e depois a struct (nesta ordem). No main, troque o free(a); por liberar_aluno(a);. O editor não tem Valgrind, então a saída não muda (Ana Beatriz: 9.5); a missão confere o código. No seu Linux, confira com valgrind --leak-check=full.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct aluno {
char *nome;
double nota;
};
struct aluno *novo_aluno(const char *nome, double nota) {
struct aluno *a = malloc(sizeof *a);
if (a == NULL) {
return NULL;
}
a->nome = malloc(strlen(nome) + 1);
if (a->nome == NULL) {
free(a);
return NULL;
}
strcpy(a->nome, nome);
a->nota = nota;
return a;
}
void liberar_aluno(struct aluno *a) {
/* libere aqui: primeiro o nome, depois a struct */
}
int main(void) {
struct aluno *a = novo_aluno("Ana Beatriz", 9.5);
if (a == NULL) {
return 1;
}
printf("%s: %.1f\n", a->nome, a->nota);
free(a);
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct aluno {
char *nome;
double nota;
};
struct aluno *novo_aluno(const char *nome, double nota) {
struct aluno *a = malloc(sizeof *a);
if (a == NULL) {
return NULL;
}
a->nome = malloc(strlen(nome) + 1);
if (a->nome == NULL) {
free(a);
return NULL;
}
strcpy(a->nome, nome);
a->nota = nota;
return a;
}
void liberar_aluno(struct aluno *a) {
if (a == NULL) {
return;
}
free(a->nome);
free(a);
}
int main(void) {
struct aluno *a = novo_aluno("Ana Beatriz", 9.5);
if (a == NULL) {
return 1;
}
printf("%s: %.1f\n", a->nome, a->nota);
liberar_aluno(a);
return 0;
}
O if (a == NULL) return; da solução imita o free: assim liberar_aluno(NULL) também é inofensivo. Esse par de funções, uma que cria e outra que destrói, é o jeito padrão de organizar memória em C: quem usa a ficha não precisa saber quantos blocos ela tem por dentro. Se amanhã a struct ganhar um campo char *email, só o liberar_aluno muda.
4. "Mas o sistema não recolhe tudo no fim?"
Recolhe. Quando um programa termina, o Linux (e o Windows, e o macOS) devolve toda a memória dele, com ou sem free. Por isso o trocou.c e o ficha.c não fazem mal nenhum ao computador da Ana. Então por que se preocupar?
- O código muda de lugar. O
mainde hoje vira uma função chamada mil vezes amanhã, como oprocessarda seção 1. - Programas longos não terminam. Servidores, jogos e editores rodam por dias; cada vazamento repetido soma.
- Vazamentos escondem outros. Um relatório do Valgrind com 50 vazamentos "inofensivos" esconde o 51º, que é o perigoso. Com o relatório limpo, qualquer vazamento novo aparece na hora.
- Os testes automáticos falham. Com
-fsanitize=address, um único byte vazado faz o programa terminar com código 1.
A meta é sempre a mesma frase do Valgrind: All heap blocks were freed -- no leaks are possible. Os blocos que ainda têm um ponteiro apontando para eles no fim (numa variável global, por exemplo) o Valgrind chama de still reachable, "ainda alcançáveis": não são perdidos, mas também merecem o seu free (detalhes em Valgrind).
5. Hábitos que evitam vazamentos
| Hábito | Por quê |
|---|---|
Escreva o free logo depois de escrever o malloc, e só então o código do meio | Você não esquece o par. |
| Todo bloco tem um dono, dito num comentário quando uma função devolve memória ("quem chamar libera") | Ninguém fica na dúvida se deve liberar (e ninguém libera duas vezes). |
| Antes de escrever um endereço novo num ponteiro dono, libere o bloco antigo | Evita o "ponteiro trocado". |
Em funções com várias saídas, uma saída só com a limpeza (ou free antes de cada return) | Evita a "saída no meio". |
Para structs com ponteiros, um par criar/liberar, liberando na ordem inversa | Evita a "caixa sem o conteúdo". |
realloc sempre numa variável auxiliar | Uma falha não apaga o único endereço do bloco. |
Teste com -g -fsanitize=address sempre, e com o Valgrind de vez em quando, incluindo as entradas erradas | Os vazamentos dos caminhos de erro só aparecem quando o erro acontece. |
Quando dá errado
ERROR: LeakSanitizer: detected memory leaks e o código de saída virou 1
O que significa: o programa terminou com blocos reservados que ninguém mais aponta. A saída pode estar perfeita; o código 1 é do LeakSanitizer.
Como consertar: leia a primeira linha com um arquivo seu em cada Direct leak: é o malloc do bloco perdido. Descubra até quando ele precisa existir e ponha o free ali. Conserte os Direct leak primeiro; os Indirect leak costumam sumir junto.
malloc de dentro de strdup, getline ou outra função da biblioteca
O que significa: essas funções reservam memória para você e passam a posse: a documentação delas diz "libere com free". O vazamento é seu, não da biblioteca.
Como consertar: desça a pilha do relatório até a primeira linha do seu arquivo (a chamada do strdup ou do getline) e libere o resultado quando não precisar mais dele (aula Strings Dinâmicas).
O que significa: a memória não está perdida, só acumulada: uma lista que só cresce e nunca é esvaziada, um cache sem limite. No fim, alguém dá o free de tudo, então não há "vazamento" no relatório, mas durante a execução o uso não para de subir.
Como consertar: meça com /usr/bin/time -f "%M" ou acompanhe com o top, e libere os itens que não são mais necessários durante a execução, não só no fim.
Faltou memória só no editor do guia
O que significa: o editor limita cada programa a 256 MB, e um vazamento dentro de um laço esgota esse limite (no Linux o mesmo programa "funciona", usando muito mais memória do que devia).
Como consertar: procure o malloc dentro do laço (ou de uma função chamada no laço) cujo bloco nunca é liberado.
(in /home/ana/c/vaza/programa) em vez de arquivo.c:linha
O que significa: o programa foi compilado sem -g.
Como consertar: recompile com -g (aula Valgrind).
Referência rápida
| Comando ou mensagem | O que é |
|---|---|
/usr/bin/time -f "%M KB" ./prog | Mostra a memória máxima que o programa usou |
gcc … -g -fsanitize=address | Liga o ASan, com o LeakSanitizer junto |
gcc … -g -fsanitize=leak | Liga só o LeakSanitizer |
valgrind --leak-check=full ./prog | Roda no Valgrind e mostra onde cada bloco vazado nasceu |
valgrind --leak-check=full --error-exitcode=1 ./prog | Idem, terminando com 1 se houver erro (bom para testes) |
Direct leak / definitely lost | Bloco que ninguém mais aponta: vazamento |
Indirect leak / indirectly lost | Bloco só apontado por blocos perdidos |
still reachable | Não liberado, mas ainda apontado no fim |
All heap blocks were freed -- no leaks are possible | Nenhum vazamento |
Exercícios
1. A opção do Valgrind
Complete para o Valgrind mostrar a linha do malloc de cada bloco vazado:
2. A opção do gcc
Complete para ligar o AddressSanitizer, que traz o LeakSanitizer junto:
3. A ordem certa
Uma struct p tem um campo char *nome reservado com malloc. Qual free vem primeiro?
4. Onde está o vazamento?
char *msg = malloc(50); strcpy(msg, "Bom dia"); msg = malloc(50); strcpy(msg, "Boa tarde"); free(msg);
5. O Valgrind rodou o programa com uma entrada boa e disse no leaks are possible. Isso garante que o programa não vaza?
6. Por que um vazamento num programa de 1 segundo é menos grave que num servidor?
Missão final: o fechamento do caixa
Missão: a lojinha fecha o caixa de 400 dias seguidos, mas a memória do editor acaba no meio. A função relatorio tem dois vazamentos, dos jeitos que você viu nesta aula: um na saída antecipada (o dia em que a loja está fechada) e um no ponteiro trocado (o rascunho = final;). Conserte os dois: cada bloco reservado tem de ser liberado uma vez, em todo caminho. A saída deve ser 267 dias abertos, 133 fechados.
#include <stdio.h>
#include <stdlib.h>
#define BLOCO 2000000
/* Monta o relatório de um dia. Devolve 0 se montou, 1 se a loja
estava fechada e -1 se faltou memória. */
int relatorio(int dia) {
char *rascunho = malloc(BLOCO);
if (rascunho == NULL) {
return -1;
}
snprintf(rascunho, BLOCO, "Dia %d", dia);
if (dia % 3 == 0) {
return 1;
}
char *final = malloc(BLOCO);
if (final == NULL) {
free(rascunho);
return -1;
}
snprintf(final, BLOCO, "%s: caixa conferido", rascunho);
rascunho = final;
free(rascunho);
return 0;
}
int main(void) {
int abertos = 0;
int fechados = 0;
for (int dia = 1; dia <= 400; dia++) {
int r = relatorio(dia);
if (r < 0) {
printf("Faltou memória no dia %d!\n", dia);
return 1;
}
if (r == 0) {
abertos++;
} else {
fechados++;
}
}
printf("%d dias abertos, %d fechados.\n", abertos, fechados);
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#define BLOCO 2000000
/* Monta o relatório de um dia. Devolve 0 se montou, 1 se a loja
estava fechada e -1 se faltou memória. */
int relatorio(int dia) {
char *rascunho = malloc(BLOCO);
if (rascunho == NULL) {
return -1;
}
snprintf(rascunho, BLOCO, "Dia %d", dia);
if (dia % 3 == 0) {
free(rascunho);
return 1;
}
char *final = malloc(BLOCO);
if (final == NULL) {
free(rascunho);
return -1;
}
snprintf(final, BLOCO, "%s: caixa conferido", rascunho);
free(rascunho);
free(final);
return 0;
}
int main(void) {
int abertos = 0;
int fechados = 0;
for (int dia = 1; dia <= 400; dia++) {
int r = relatorio(dia);
if (r < 0) {
printf("Faltou memória no dia %d!\n", dia);
return 1;
}
if (r == 0) {
abertos++;
} else {
fechados++;
}
}
printf("%d dias abertos, %d fechados.\n", abertos, fechados);
return 0;
}
Se você consertar só um dos dois, o editor ainda fica sem memória, só que mais tarde (no dia 194 ou no 388): cada vazamento sozinho já passa dos 256 MB. No Linux, o programa original "funciona" e mostra a saída certa, reservando 800 MB sem nunca devolver; rode a sua solução com gcc -g -fsanitize=address e confira que o LeakSanitizer não diz nada.
Resumo
- Um vazamento é um bloco reservado que nunca é liberado, quase sempre porque o programa perdeu o endereço dele. Não dá aviso nem erro: o programa só cresce.
- Meça com
/usr/bin/time -f "%M"; ache a linha com-g -fsanitize=address(LeakSanitizer:Direct leak) ou comvalgrind --leak-check=full(definitely lost). Os dois mostram onde o bloco nasceu. - Os jeitos clássicos:
freeesquecido; ponteiro trocado sem liberar o bloco antigo;returnno meio da função que pula ofree; struct liberada sem liberar o que ela aponta. - Conserte com um dono para cada bloco, uma saída só com a limpeza (ou
freeantes de cadareturn) e parescriar/liberarque liberam na ordem inversa. - O sistema recolhe tudo no fim do programa, mas vazamentos repetidos derrubam programas longos e escondem outros erros. A meta é
All heap blocks were freed -- no leaks are possible. - No editor do guia não há Valgrind nem sanitizers; só um vazamento que esgota os 256 MB aparece (
Faltou memória).