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.

Você vai aprender
  • 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 malloc que vazou com o Valgrind e com o LeakSanitizer (-fsanitize=address).
  • Os jeitos clássicos de vazar: o free esquecido, 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 quando processar termina (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ável recibo some, 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.

No editor do guia, a memória acaba de verdade

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óprio malloc; a primeira linha com um arquivo seu (#1) é a pista: o malloc(TAMANHO) da linha 8. As linhas #3 a #5 são o começo de todo programa, antes do main; 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.

Só o fiscal de vazamentos: -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?SimNão (só o -g, para as linhas)
VelocidadeQuase normal10 a 50 vezes mais lento
O que dizDirect leak / Indirect leak, com a pilha do mallocdefinitely lost / indirectly lost / still reachable, com a pilha do malloc
Código de saída1 se vazouO do programa (ou o que você pedir com --error-exitcode=1)
No editor do guiaNenhum 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 o printf reserva 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 free da linha 20 liberou o da Bia, que era o único endereço ainda guardado em cliente. 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", &notas[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", &notas[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 com fim:, sem passar pelo resto. O goto tem 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 pelo free.
$ 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 main de hoje vira uma função chamada mil vezes amanhã, como o processar da 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ábitoPor quê
Escreva o free logo depois de escrever o malloc, e só então o código do meioVocê 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 antigoEvita 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 inversaEvita a "caixa sem o conteúdo".
realloc sempre numa variável auxiliarUma 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 erradasOs 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.

O relatório aponta um 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 Valgrind não acha nada, mas o programa continua crescendo

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.

O relatório mostra (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 mensagemO que é
/usr/bin/time -f "%M KB" ./progMostra a memória máxima que o programa usou
gcc … -g -fsanitize=addressLiga o ASan, com o LeakSanitizer junto
gcc … -g -fsanitize=leakLiga só o LeakSanitizer
valgrind --leak-check=full ./progRoda no Valgrind e mostra onde cada bloco vazado nasceu
valgrind --leak-check=full --error-exitcode=1 ./progIdem, terminando com 1 se houver erro (bom para testes)
Direct leak / definitely lostBloco que ninguém mais aponta: vazamento
Indirect leak / indirectly lostBloco só apontado por blocos perdidos
still reachableNão liberado, mas ainda apontado no fim
All heap blocks were freed -- no leaks are possibleNenhum vazamento

Exercícios

1. A opção do Valgrind

Complete para o Valgrind mostrar a linha do malloc de cada bloco vazado:

valgrind ./pedidos

2. A opção do gcc

Complete para ligar o AddressSanitizer, que traz o LeakSanitizer junto:

gcc -std=c23 -Wall -Wextra -g pedidos.c -o pedidos

3. A ordem certa

Uma struct p tem um campo char *nome reservado com malloc. Qual free vem primeiro?

free();

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 com valgrind --leak-check=full (definitely lost). Os dois mostram onde o bloco nasceu.
  • Os jeitos clássicos: free esquecido; ponteiro trocado sem liberar o bloco antigo; return no meio da função que pula o free; struct liberada sem liberar o que ela aponta.
  • Conserte com um dono para cada bloco, uma saída só com a limpeza (ou free antes de cada return) e pares criar/liberar que 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).