C malloc e free

Na aula Pilha e Heap você viu que o heap é a parte da memória que o programa pede durante a execução, do tamanho que quiser, e que vive até ser devolvida. As duas funções que fazem isso são o malloc (de memory allocation, "reserva de memória"), que pede um pedaço do heap, e o free ("liberar"), que o devolve. São duas funções pequenas, mas quase todo programa em C de verdade depende delas, e quase todos os erros graves de C nascem de usá-las errado. Nesta aula você aprende o uso certo e vê, com saídas reais, o que acontece em cada erro.

Você vai aprender
  • A reservar memória com malloc e a calcular o tamanho com sizeof *p.
  • A criar um array do tamanho que o usuário pedir, durante a execução.
  • Por que todo malloc precisa ser conferido contra NULL.
  • Que o malloc não limpa a memória: o que vem lá dentro é lixo.
  • A devolver a memória com free e as três regras do free.
  • Os erros clássicos (ponteiro pendurado, free duplo, free de um endereço errado), como o gcc, o AddressSanitizer e o Valgrind os mostram, e por que o editor do guia não os mostra.

Esta aula supõe que você já leu Ponteiros, Ponteiros e Arrays e Pilha e Heap. 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, com o comando padrão do guia: gcc -std=c23 -Wall -Wextra arquivo.c -o programa.


A ideia: o guarda-volumes

Volte ao guarda-volumes do restaurante da aula Pilha e Heap. O atendimento funciona assim:

  1. Você vai ao balcão e diz quantos bytes precisa: "quero um armário de 40 bytes".
  2. O atendente procura um armário livre desse tamanho e te entrega o número dele (o endereço). Se não houver nenhum, ele te entrega um papel escrito "NENHUM" (o NULL).
  3. Você guarda o que quiser lá dentro, pelo tempo que quiser. O armário vem como o último cliente deixou: pode ter lixo dentro.
  4. Quando não precisa mais, você devolve a chave no balcão. Daí em diante o armário é de outra pessoa: abrir de novo é invasão, e devolver a mesma chave duas vezes deixa o atendente maluco.
No guarda-volumesEm C
Pedir um armário de N bytesmalloc(N)
O número do armárioO endereço que o malloc devolve, guardado num ponteiro
O papel "NENHUM"NULL: não havia memória
O lixo do cliente anteriorO conteúdo indefinido da memória nova
Devolver a chavefree(ponteiro)

1. O primeiro malloc: um int no heap

O programa mais simples possível: reservar espaço para um inteiro, usar e devolver.

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int *pontos = malloc(sizeof *pontos);
    if (pontos == NULL) {
        printf("Faltou memória!\n");
        return 1;
    }
    *pontos = 1500;
    printf("Reservei %zu bytes no heap.\n", sizeof *pontos);
    printf("Pontos: %d\n", *pontos);
    free(pontos);
    return 0;
}
#include <stdlib.h>
O malloc e o free moram na stdlib.h ("standard library", biblioteca padrão), não na stdio.h. Esquecer este include é um erro comum (veja "Quando dá errado").
int *pontos = malloc(sizeof *pontos);
O coração da aula. O malloc recebe quantos bytes você quer e devolve o endereço do primeiro deles. sizeof *pontos quer dizer "o tamanho daquilo para onde pontos aponta", ou seja, o tamanho de um int: 4 bytes. O endereço vai para o ponteiro pontos.
if (pontos == NULL) { … }
Se não houver memória, o malloc devolve NULL (aula Ponteiro Nulo). Usar um NULL derruba o programa, então a conferência vem logo depois de todo malloc.
*pontos = 1500;
Daqui em diante, *pontos é um int como outro qualquer: dá para guardar, ler e fazer contas. A diferença é onde ele mora: no heap, e não na pilha.
free(pontos);
Devolve os 4 bytes. Repare que se passa o ponteiro (o número do armário), sem o *. Depois do free, *pontos não pode mais ser usado.
$ gcc -std=c23 -Wall -Wextra um.c -o um
$ ./um
Reservei 4 bytes no heap.
Pontos: 1500

De onde vem o tipo do ponteiro?

O malloc não sabe o que você vai guardar: ele só entrega bytes. Por isso devolve um void *, o "endereço sem etiqueta" da aula Ponteiro void*. Em C, um void * vira qualquer outro ponteiro sozinho, na atribuição: é o int *pontos = … que diz "aqui moram inteiros".

Sem conversão na frente do malloc

Você vai encontrar na internet int *p = (int *)malloc(sizeof(int));. O (int *) é uma conversão (aula Conversão de Tipos) que em C não é necessária: o void * já se converte sozinho. Ela é obrigatória em C++, e por isso aparece em códigos escritos por quem vem de lá. Em C, deixe sem: é menos para escrever e menos para errar.

Missão: o programa ainda não reserva nada: vidas começa com NULL e ele mostra Faltou memória!. Troque o NULL por um malloc do tamanho certo e acrescente o free(vidas); antes do return 0;. A saída deve ser Vidas: 3.

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int *vidas = NULL;
    if (vidas == NULL) {
        printf("Faltou memória!\n");
        return 1;
    }
    *vidas = 3;
    printf("Vidas: %d\n", *vidas);
    return 0;
}
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int *vidas = malloc(sizeof *vidas);
    if (vidas == NULL) {
        printf("Faltou memória!\n");
        return 1;
    }
    *vidas = 3;
    printf("Vidas: %d\n", *vidas);
    free(vidas);
    return 0;
}

2. Quantos bytes pedir: sizeof

O malloc conta em bytes, nunca em "itens". Para guardar 10 inteiros, você precisa de 10 vezes o tamanho de um inteiro. Há duas formas de escrever isso:

FormaExemploO que acontece se o tipo mudar
sizeof *p (o tamanho daquilo que o ponteiro aponta)int *idades = malloc(10 * sizeof *idades);Se um dia idades virar long *, o tamanho se ajusta sozinho.
sizeof(tipo)int *idades = malloc(10 * sizeof(int));Se idades virar long * e alguém esquecer de mudar o sizeof(int), o bloco fica pequeno demais.

As duas funcionam, e você vai ver as duas por aí. Este guia prefere sizeof *p: o tipo é escrito uma vez só. Repare que o sizeof *idades não segue o ponteiro de verdade (ele ainda nem tem valor); o compilador só olha o tipo de *idades. Mais sobre o operador em sizeof.

O erro mais comum é esquecer a multiplicação: malloc(n) reserva n bytes, não n inteiros. A Ana queria 10 idades:

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int n = 10;
    int *idades = malloc(n);
    if (idades == NULL) {
        return 1;
    }
    for (int i = 0; i < n; i++) {
        idades[i] = 20 + i;
    }
    printf("Última idade: %d\n", idades[n - 1]);
    free(idades);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra semsizeof.c -o semsizeof      « nenhum aviso
$ ./semsizeof
malloc(): corrupted top size
Abortado                   (imagem do núcleo gravada) ./semsizeof

O programa pediu 10 bytes e escreveu 40 (10 inteiros de 4 bytes). Os 30 bytes a mais caíram em cima das anotações internas que o malloc guarda ao lado de cada bloco. Quando o printf pediu memória para ele mesmo, o malloc encontrou as anotações estragadas ("tamanho do topo corrompido"), desistiu e abortou o programa (código 134). Repare que a mensagem não fala da linha 11, onde está o erro: ela aparece mais tarde, em outro lugar. Com o AddressSanitizer (aula Sanitizers), a linha certa aparece:

$ gcc -std=c23 -Wall -Wextra -g -fsanitize=address semsizeof.c -o semsizeof
$ ./semsizeof
=================================================================
==419872==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7b4eff9e0018 at pc 0x5602f1e2e2da bp 0x7ffc3f3235a0 sp 0x7ffc3f323590
WRITE of size 4 at 0x7b4eff9e0018 thread T0
    #0 0x5602f1e2e2d9 in main /home/ana/c/semsizeof.c:11
«…»
0x7b4eff9e001a is located 0 bytes after 10-byte region [0x7b4eff9e0010,0x7b4eff9e001a)
allocated by thread T0 here:
    #0 0x7f2f00b2b60f in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:67
    #1 0x5602f1e2e268 in main /home/ana/c/semsizeof.c:6
«… o resto do relatório …»

heap-buffer-overflow é "estouro de um bloco do heap": uma escrita de 4 bytes (WRITE of size 4, um int) na linha 11, logo depois do fim de uma região de 10 bytes (10-byte region) reservada na linha 6. O "10 bytes" é a pista: era para ser 40. O conserto é malloc(n * sizeof *idades).

No editor do guia, este erro passa calado

O mesmo semsizeof.c no editor mostra Última idade: 29 e termina com código 0, como se estivesse certo. O malloc do editor (da wasi-libc, para WebAssembly) arruma a memória de outro jeito e não confere as anotações. O erro continua lá: só não aparece. Por isso, os programas com malloc merecem uma rodada com -fsanitize=address ou com o Valgrind num Linux de verdade.


3. Um array do tamanho que o usuário pedir

Este é o motivo número um para usar o malloc: o tamanho só é conhecido durante a execução. A professora Ana não sabe quantos alunos cada turma tem; o programa pergunta e reserva exatamente o necessário:

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int n;
    printf("Quantos alunos? ");
    if (scanf("%d", &n) != 1 || n <= 0) {
        printf("Número inválido.\n");
        return 1;
    }

    double *notas = malloc(n * sizeof *notas);
    if (notas == NULL) {
        printf("Faltou memória!\n");
        return 1;
    }

    for (int i = 0; i < n; i++) {
        printf("Nota %d: ", i + 1);
        if (scanf("%lf", &notas[i]) != 1) {
            printf("Nota inválida.\n");
            free(notas);
            return 1;
        }
    }

    double soma = 0;
    for (int i = 0; i < n; i++) {
        soma += notas[i];
    }
    printf("Média da turma: %.2f\n", soma / n);

    free(notas);
    return 0;
}
if (scanf("%d", &n) != 1 || n <= 0)
Antes de reservar, confira o número: se o usuário digitar quatro ou -3, o programa para aqui. Um n negativo viraria um tamanho gigante no malloc, que recebe um size_t (sem sinal).
double *notas = malloc(n * sizeof *notas);
n notas de 8 bytes cada (o tamanho de um double). Com 4 alunos, 32 bytes.
&notas[i]
O ponteiro é usado com colchetes, como um array comum (aula Ponteiros e Arrays): notas[i] é a nota i, e &notas[i] é o endereço dela, que o scanf precisa.
free(notas); return 1;
Se uma nota vier errada, o programa desiste. Mas antes devolve a memória: todo caminho que sai do programa depois do malloc passa por um free. (Na saída do main o sistema recolheria tudo de qualquer jeito, mas o hábito importa: dentro de uma função comum, esquecer aqui é um vazamento.)
free(notas);
Um único free devolve o bloco inteiro, as n notas de uma vez. Nunca se libera item por item.
$ gcc -std=c23 -Wall -Wextra notas.c -o notas
$ ./notas
Quantos alunos? 4
Nota 1: 7.5
Nota 2: 8
Nota 3: 6.5
Nota 4: 9
Média da turma: 7.75
$ ./notas
Quantos alunos? quatro
Número inválido.

Compare com um array comum, double notas[30];: ou sobra espaço (turma de 12), ou falta (turma de 45). Com o malloc, o tamanho é exato. E se o tamanho mudar depois que o array já existe (chegou um aluno novo), o realloc da próxima aula, calloc e realloc, aumenta o bloco.

Missão: este programa usa um array fixo de 3 notas, e a turma de hoje tem 5 alunos (a entrada já está pronta na caixa). Rode e veja Só cabem 3 notas!. Troque o array por um malloc de n notas, logo depois de ler o n (apague o teste do "só cabem 3"), confira o NULL e chame free no fim. A saída deve terminar com Média da turma: 8.10.

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    double notas[3];
    int n;
    printf("Quantos alunos? ");
    if (scanf("%d", &n) != 1 || n <= 0) {
        printf("Número inválido.\n");
        return 1;
    }
    if (n > 3) {
        printf("Só cabem 3 notas!\n");
        return 1;
    }

    for (int i = 0; i < n; i++) {
        printf("Nota %d: ", i + 1);
        if (scanf("%lf", &notas[i]) != 1) {
            printf("Nota inválida.\n");
            return 1;
        }
    }

    double soma = 0;
    for (int i = 0; i < n; i++) {
        soma += notas[i];
    }
    printf("Média da turma: %.2f\n", soma / n);
    return 0;
}
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int n;
    printf("Quantos alunos? ");
    if (scanf("%d", &n) != 1 || n <= 0) {
        printf("Número inválido.\n");
        return 1;
    }

    double *notas = malloc(n * sizeof *notas);
    if (notas == NULL) {
        printf("Faltou memória!\n");
        return 1;
    }

    for (int i = 0; i < n; i++) {
        printf("Nota %d: ", i + 1);
        if (scanf("%lf", &notas[i]) != 1) {
            printf("Nota inválida.\n");
            free(notas);
            return 1;
        }
    }

    double soma = 0;
    for (int i = 0; i < n; i++) {
        soma += notas[i];
    }
    printf("Média da turma: %.2f\n", soma / n);
    free(notas);
    return 0;
}

4. Conferir o NULL: quando o malloc falha

O heap é grande, mas não é infinito. Quando não dá para atender o pedido, o malloc não trava nem avisa: ele devolve NULL, em silêncio. A Ana pediu 100 bilhões de bytes (100 GB) num computador que não tem tudo isso:

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    size_t bytes = 100000000000;
    char *bloco = malloc(bytes);
    if (bloco == NULL) {
        printf("Não deu: malloc devolveu NULL.\n");
        return 1;
    }
    printf("Consegui!\n");
    free(bloco);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra enorme.c -o enorme
$ ./enorme
Não deu: malloc devolveu NULL.

Sem o if, a primeira escrita em bloco[0] seria uma escrita no endereço zero: Falha de segmentação (aula Ponteiro Nulo). Com o if, o programa sai de forma organizada, com uma mensagem que diz o que houve. Num programa pequeno de aula, o malloc quase nunca falha; mas o hábito de conferir custa uma linha e evita um erro que, quando acontece, é difícil de reproduzir.

No editor do guia, o limite é 256 MB

O programa do editor roda em WebAssembly de 32 bits, com no máximo 256 MB de memória. Lá, um malloc de 300 MB já devolve NULL, e o mesmo pedido funciona no gcc do Linux. Outro detalhe: no editor, size_t tem 4 bytes e não consegue guardar 100 bilhões; o Clang avisa implicit conversion from 'long long' to 'size_t' (aka 'unsigned long') changes value from 100000000000 to 1215752192 [-Wconstant-conversion] e pede uns 1,2 GB, que também voltam como NULL.


5. O malloc não limpa a memória

O armário vem como o último cliente deixou. O malloc só marca o bloco como seu; não apaga o que estava lá. Este programa reserva 8 inteiros, preenche, devolve, e depois reserva outros 8 sem preencher, mostrando o que veio dentro:

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int *antes = malloc(8 * sizeof *antes);
    if (antes == NULL) {
        return 1;
    }
    for (int i = 0; i < 8; i++) {
        antes[i] = (i + 1) * 11;
    }
    free(antes);

    int *notas = malloc(8 * sizeof *notas);
    if (notas == NULL) {
        return 1;
    }
    for (int i = 0; i < 8; i++) {
        printf("%d ", notas[i]);
    }
    printf("\n");
    free(notas);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra lixo.c -o lixo
$ ./lixo
1694505054 5 0 0 55 66 77 88
$ ./lixo
1575119293 5 0 0 55 66 77 88       « o primeiro número muda a cada execução

O segundo malloc recebeu o mesmo armário que o primeiro acabou de devolver. As quatro últimas posições ainda têm o 55 66 77 88 do programa anterior. As quatro primeiras foram rabiscadas pelo próprio malloc, que usa o começo de um bloco livre para anotar a lista de blocos livres. No editor do guia, o resultado é outro (11 22 33 44 55 66 77 88, porque o malloc da wasi-libc anota em outro lugar), e isso só confirma a regra: o conteúdo de um bloco novo é lixo, e lixo pode ser qualquer coisa.

Por isso, depois de um malloc, dê valor a tudo antes de ler (como o laço do scanf do notas.c). Se você quer o bloco começando com zeros, há uma função própria, o calloc, da próxima aula, calloc e realloc.


6. free: devolver a chave

O free recebe o endereço que o malloc deu e devolve o bloco ao heap, para ser reaproveitado. Ele não devolve nada (é void) e não muda o seu ponteiro: depois do free(p), p continua guardando o mesmo número, só que agora de um armário que não é mais seu. As regras são três:

RegraSe quebrar
1. Depois do free(p), não use mais *p nem p[i]."Ponteiro pendurado": lê lixo ou estraga dados de outro bloco.
2. Cada bloco é liberado uma vez só.free(): double free detected, o programa aborta.
3. Só se passa ao free um endereço que veio do malloc (ou do calloc/realloc), e exatamente o endereço do começo.free(): invalid pointer, o programa aborta.

E uma exceção útil: free(NULL) é permitido e não faz nada. Por isso o costume de, logo depois do free(p), escrever p = NULL;: um segundo free(p) por engano vira inofensivo, e um if (p != NULL) passa a dizer a verdade.

Regra 1: o ponteiro pendurado

Um ponteiro que guarda o endereço de memória já liberada se chama ponteiro pendurado (dangling pointer). O p = NULL protege aquele ponteiro, mas não as cópias dele. Aqui o copia continua pendurado:

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int *placar = malloc(sizeof *placar);
    if (placar == NULL) {
        return 1;
    }
    *placar = 7;
    int *copia = placar;

    free(placar);
    placar = NULL;

    printf("Placar: %d\n", *copia);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra apelido.c -o apelido
apelido.c: In function ‘main’:
apelido.c:15:5: warning: pointer ‘copia’ used after ‘free’ [-Wuse-after-free]
   15 |     printf("Placar: %d\n", *copia);
      |     ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
apelido.c:12:5: note: call to ‘free’ here
   12 |     free(placar);
      |     ^~~~~~~~~~~~
$ ./apelido
Placar: 1495898798
warning: pointer ‘copia’ used after ‘free’ [-Wuse-after-free]
"Ponteiro copia usado depois do free". O gcc percebeu que copia e placar apontam para o mesmo lugar, e a note mostra o free da linha 12. Ele só consegue avisar porque tudo está na mesma função; quando o free fica em outra função, o aviso some (veja a regra 2).
Placar: 1495898798
O programa rodou e mostrou lixo em vez de 7: o free usou o bloco para as anotações dele. Nada de falha de segmentação: o erro passa sem barulho, e o programa segue com um valor errado.

Para ver o erro de verdade, chame os fiscais. O AddressSanitizer para o programa na linha do erro e conta a história do bloco:

$ gcc -std=c23 -Wall -Wextra -g -fsanitize=address apelido.c -o apelido
«… o mesmo aviso …»
$ ./apelido
=================================================================
==419821==ERROR: AddressSanitizer: heap-use-after-free on address 0x7bcc115e0010 at pc 0x55ef43f57308 bp 0x7fff91a04070 sp 0x7fff91a04060
READ of size 4 at 0x7bcc115e0010 thread T0
    #0 0x55ef43f57307 in main /home/ana/c/apelido.c:15
«…»
0x7bcc115e0010 is located 0 bytes inside of 4-byte region [0x7bcc115e0010,0x7bcc115e0014)
freed by thread T0 here:
    #0 0x7fac1272a3ff in free ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:51
    #1 0x55ef43f572c8 in main /home/ana/c/apelido.c:12
«…»
previously allocated by thread T0 here:
    #0 0x7fac1272b60f in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:67
    #1 0x55ef43f5725e in main /home/ana/c/apelido.c:5
«… o resto do relatório …»

Leia de cima para baixo: leitura de 4 bytes depois do free (heap-use-after-free) na linha 15; o bloco foi liberado na linha 12 e tinha sido reservado na linha 5. O Valgrind, sem recompilar com nada especial (só o -g), diz o mesmo com outras palavras:

$ gcc -std=c23 -Wall -Wextra -g apelido.c -o apelido-vg
«… o mesmo aviso …»
$ valgrind ./apelido-vg
«… o cabeçalho …»
==419837== Invalid read of size 4
==419837==    at 0x40011DB: main (apelido.c:15)
==419837==  Address 0x4a8e040 is 0 bytes inside a block of size 4 free'd
==419837==    at 0x48538BF: free (vg_replace_malloc.c:990)
==419837==    by 0x40011CE: main (apelido.c:12)
==419837==  Block was alloc'd at
==419837==    at 0x4850858: malloc (vg_replace_malloc.c:447)
==419837==    by 0x400119E: main (apelido.c:5)
==419837==
Placar: 7
«… o resumo: ERROR SUMMARY: 1 errors from 1 contexts …»

Dentro do Valgrind o programa até mostrou Placar: 7, porque o Valgrind não reaproveita o bloco liberado logo de cara. O valor certo, aqui, não quer dizer que o código está certo: o Invalid read diz que não está.

No editor do guia: Placar: 7, sem erro nenhum

O editor mostra Placar: 7 e código 0. O Clang não tem o aviso -Wuse-after-free do gcc, e o malloc do editor não rabisca o bloco liberado. É o pior cenário: o erro existe e tudo parece certo. Os erros de memória que não são ponteiro nulo não aparecem no editor; para eles, use o gcc com -fsanitize=address ou o Valgrind no seu Linux.

Missão: rode e veja o editor mostrar Placar depois do free: 7, um valor que já não existe (no gcc sairia lixo). Conserte com o costume desta seção: logo depois do free(placar);, faça o ponteiro valer NULL. Aí o if passa a dizer a verdade, e a saída mostra O placar já foi liberado.

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int *placar = malloc(sizeof *placar);
    if (placar == NULL) {
        return 1;
    }
    *placar = 7;
    printf("Placar: %d\n", *placar);
    free(placar);

    if (placar != NULL) {
        printf("Placar depois do free: %d\n", *placar);
    } else {
        printf("O placar já foi liberado.\n");
    }
    return 0;
}
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int *placar = malloc(sizeof *placar);
    if (placar == NULL) {
        return 1;
    }
    *placar = 7;
    printf("Placar: %d\n", *placar);
    free(placar);
    placar = NULL;

    if (placar != NULL) {
        printf("Placar depois do free: %d\n", *placar);
    } else {
        printf("O placar já foi liberado.\n");
    }
    return 0;
}

Regra 2: o free duplo

O caso típico: uma função libera o bloco e quem a chamou não sabe, e libera de novo.

#include <stdio.h>
#include <stdlib.h>

void encerrar(int *placar) {
    printf("Placar final: %d\n", *placar);
    free(placar);
}

int main(void) {
    int *placar = malloc(sizeof *placar);
    if (placar == NULL) {
        return 1;
    }
    *placar = 42;
    encerrar(placar);
    free(placar);
    printf("Fim do jogo.\n");
    return 0;
}
$ gcc -std=c23 -Wall -Wextra duplo.c -o duplo      « nenhum aviso: os dois free estão em funções diferentes
$ ./duplo
Placar final: 42
free(): double free detected in tcache 2
Abortado                   (imagem do núcleo gravada) ./duplo
$ echo $?
134

A própria glibc (a biblioteca C do Linux) percebeu: "free(): liberação dupla detectada" (o tcache é um cache interno do malloc) e abortou (134 = 128 + 6, o sinal SIGABRT). O Fim do jogo. nunca apareceu. Ela diz que houve, mas não onde. O ASan diz onde, e ainda mostra que o primeiro free foi dentro de encerrar:

$ gcc -std=c23 -Wall -Wextra -g -fsanitize=address duplo.c -o duplo
$ ./duplo
=================================================================
==419866==ERROR: AddressSanitizer: attempting double-free on 0x7b530fbe0010 in thread T0:
    #0 0x7f3310d2a3ff in free ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:51
    #1 0x5571f18f335b in main /home/ana/c/duplo.c:16
«…»
freed by thread T0 here:
    #0 0x7f3310d2a3ff in free ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:51
    #1 0x5571f18f32d7 in encerrar /home/ana/c/duplo.c:6
    #2 0x5571f18f334f in main /home/ana/c/duplo.c:15
«…»
previously allocated by thread T0 here:
    #0 0x7f3310d2b60f in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:67
    #1 0x5571f18f32f0 in main /home/ana/c/duplo.c:10
«…»
SUMMARY: AddressSanitizer: double-free /home/ana/c/duplo.c:16 in main
==419866==ABORTING

No editor do guia, o free duplo passa calado: o programa mostra Placar final: 42 e Fim do jogo. e termina com 0. O conserto é decidir quem libera (seção 7): ou encerrar libera e o main não, ou o contrário. Nunca os dois.

Regra 3: só o endereço que veio do malloc

Dois jeitos de quebrar esta regra. O primeiro: liberar uma variável local, que mora na pilha e nunca veio do malloc:

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int vidas = 3;
    int *p = &vidas;
    printf("Vidas: %d\n", *p);
    free(p);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra freelocal.c -o freelocal
freelocal.c: In function ‘main’:
freelocal.c:8:5: warning: ‘free’ called on unallocated object ‘vidas’ [-Wfree-nonheap-object]
    8 |     free(p);
      |     ^~~~~~~
freelocal.c:5:9: note: declared here
    5 |     int vidas = 3;
      |         ^~~~~
$ ./freelocal
Vidas: 3
free(): invalid pointer
Abortado                   (imagem do núcleo gravada) ./freelocal

O gcc avisou ("free chamado num objeto que não foi reservado: vidas") e, ao rodar, a glibc abortou com "ponteiro inválido". O segundo jeito é mais traiçoeiro: o endereço veio do malloc, mas o ponteiro andou:

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int *notas = malloc(5 * sizeof *notas);
    if (notas == NULL) {
        return 1;
    }
    int *p = notas;
    for (int i = 0; i < 5; i++) {
        *p = 7;
        p++;
    }
    printf("Preenchi 5 notas.\n");
    free(p);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra freemeio.c -o freemeio      « nenhum aviso
$ ./freemeio
Preenchi 5 notas.
free(): invalid pointer
Abortado                   (imagem do núcleo gravada) ./freemeio

Depois do laço, p aponta para depois da quinta nota, e não para o começo do bloco. O free precisa do endereço exato que o malloc devolveu, porque é ali que ele encontra as anotações do bloco. O conserto é free(notas);: guarde sempre o ponteiro original e ande com uma cópia. No editor do guia, os dois programas terminam com código 0, sem mensagem.


7. Quem libera? O dono da memória

A memória do heap não pertence a uma função: ela vive até alguém chamar free. Por isso, todo bloco precisa de um dono, a parte do programa responsável por liberá-lo. A regra mais usada:

  • Uma função que reserva e devolve o ponteiro (como o criar_contador da aula Pilha e Heap) passa a posse para quem chamou: quem recebe o ponteiro dá o free.
  • Uma função que só recebe o ponteiro para ler ou mudar o conteúdo (como um mostrar(notas, n)) não libera nada.
  • Se uma função precisa liberar o que recebeu (como o encerrar do duplo.c), o nome e um comentário devem dizer isso, e quem chamou não toca mais no ponteiro.
#include <stdio.h>
#include <stdlib.h>

/* Reserva n inteiros com os valores 1, 2, …, n.
   Quem chamar é o dono: tem de chamar free no fim. */
int *sequencia(int n) {
    int *v = malloc(n * sizeof *v);
    if (v == NULL) {
        return NULL;
    }
    for (int i = 0; i < n; i++) {
        v[i] = i + 1;
    }
    return v;
}

/* Só lê: não libera. */
void mostrar(const int *v, int n) {
    for (int i = 0; i < n; i++) {
        printf("%d ", v[i]);
    }
    printf("\n");
}

int main(void) {
    int *numeros = sequencia(5);
    if (numeros == NULL) {
        printf("Faltou memória!\n");
        return 1;
    }
    mostrar(numeros, 5);
    free(numeros);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra dono.c -o dono
$ ./dono
1 2 3 4 5

Repare no const int *v de mostrar: além de não liberar, ela promete não mudar os números (aula const e Ponteiros). A biblioteca padrão segue a mesma regra: o strdup da aula Strings Dinâmicas devolve memória nova e a documentação dele diz "libere com free".


Qual usar

SituaçãoUse
Poucos dados, tamanho fixo, usados só dentro da funçãoVariável ou array local, sem malloc (aula Pilha e Heap)
Tamanho conhecido só na execução, dados grandes, ou dados que saem da funçãomalloc + free
Bloco que precisa começar com zeroscalloc (próxima aula)
Bloco que precisa crescer ou encolherrealloc (próxima aula) e Array que Cresce
O tamanho no mallocn * sizeof *p (o tipo aparece uma vez só)
(int *) na frente do mallocEm C, não precisa. Só em C++.

Quando dá errado

error: implicit declaration of function ‘malloc’
$ gcc -std=c23 -Wall -Wextra semstdlib.c -o semstdlib
semstdlib.c: In function ‘main’:
semstdlib.c:4:19: error: implicit declaration of function ‘malloc’ [-Wimplicit-function-declaration]
    4 |     int *pontos = malloc(sizeof *pontos);
      |                   ^~~~~~
semstdlib.c:2:1: note: include ‘<stdlib.h>’ or provide a declaration of ‘malloc’
    1 | #include <stdio.h>
  +++ |+#include <stdlib.h>
    2 |
«… o mesmo para o free …»

O que significa: "declaração implícita da função malloc": o programa usa malloc sem ter incluído o cabeçalho que a declara. A note já mostra o conserto, com um + na linha que falta. No editor, o Clang diz error: call to undeclared library function 'malloc' with type 'void *(__size_t)' (aka 'void *(unsigned long)'); ISO C99 and later do not support implicit function declarations e note: include the header <stdlib.h> or explicitly provide a declaration for 'malloc'.

Como consertar: #include <stdlib.h> no começo do arquivo.

malloc(): corrupted top size, free(): invalid next size e parecidos

O que significa: o programa escreveu além do fim de um bloco e estragou as anotações do malloc. A mensagem aparece mais tarde, num malloc ou free qualquer, longe do erro de verdade. A causa mais comum é o tamanho errado: malloc(n) em vez de malloc(n * sizeof *p), ou o + 1 do '\0' esquecido numa string.

Como consertar: confira as contas de cada malloc e rode com -g -fsanitize=address: o heap-buffer-overflow aponta a linha que escreveu demais e o tamanho do bloco.

free(): double free detected in tcache 2

O que significa: o mesmo bloco foi liberado duas vezes. No editor do guia, passa calado.

Como consertar: decida quem é o dono do bloco e libere num lugar só. Depois do free(p), faça p = NULL;. Para achar os dois free, use o ASan (attempting double-free) ou o Valgrind (Invalid free()).

free(): invalid pointer

O que significa: o free recebeu um endereço que não é o começo de um bloco do malloc: uma variável local (o gcc avisa com -Wfree-nonheap-object), um literal de texto ou um ponteiro que andou (p++). No editor do guia, passa calado.

Como consertar: passe ao free exatamente o ponteiro que o malloc devolveu. Para percorrer o bloco, use um índice (notas[i]) ou uma cópia do ponteiro.

Valores estranhos, que mudam a cada execução

O que significa: duas suspeitas: o programa lê um bloco do malloc antes de dar valor a ele (lixo), ou lê um bloco já liberado (ponteiro pendurado; o gcc às vezes avisa com -Wuse-after-free).

Como consertar: dê valor a cada posição antes de ler (ou use calloc), e não use ponteiros depois do free. O Valgrind acha os dois: Conditional jump or move depends on uninitialised value(s) para o lixo e Invalid read para o bloco liberado.

Falha de segmentação logo depois de um malloc

O que significa: o malloc devolveu NULL e o programa usou o ponteiro sem conferir.

Como consertar: um if (p == NULL) logo depois de cada malloc, com uma mensagem e uma saída organizada.


Referência rápida

CódigoO que faz
#include <stdlib.h>Declara malloc, free, calloc, realloc
void *malloc(size_t bytes);Reserva bytes bytes no heap; devolve o endereço do primeiro, ou NULL se não deu. O conteúdo é lixo.
void free(void *p);Devolve o bloco que começa em p. free(NULL) não faz nada.
T *p = malloc(sizeof *p);Um item do tipo T
T *v = malloc(n * sizeof *v);Um array de n itens do tipo T; use v[0] a v[n-1]
if (p == NULL) { … }Logo depois de todo malloc
free(p); p = NULL;Devolver e "esquecer" o endereço
-fsanitize=address, valgrindAcham estouro, uso depois do free, free duplo e vazamentos (no Linux; não no editor)

Exercícios

1. O cabeçalho

Qual cabeçalho declara o malloc e o free?

#include <>

2. Espaço para 20 preços

Complete para reservar 20 double:

double *precos = malloc();

3. Sem erro na falha

Quando não há memória, o que o malloc devolve?

4. Depois de int *v = malloc(5 * sizeof *v);, o que há em v[0] antes de qualquer atribuição?

5. Qual destas linhas é um erro?

6. Um programa com free duplo roda no editor do guia sem nenhuma mensagem. O que isso quer dizer?

Missão final: os preços da lojinha

Missão: a função ler_precos ainda não faz nada e devolve NULL. Escreva o corpo dela: reserve n preços com malloc, confira o NULL, leia cada preço com scanf("%lf", …) (se um falhar, libere o bloco e devolva NULL) e devolva o ponteiro. No main, que é o dono do bloco, devolva a memória com free(lista); no fim. A entrada já está na caixa; a saída deve terminar com:

Total: R$ 52.30
Mais caro: R$ 19.90
#include <stdio.h>
#include <stdlib.h>

/* Reserva e lê n preços. Quem chamar tem de chamar free. */
double *ler_precos(int n) {
    /* escreva aqui */
    return NULL;
}

int main(void) {
    int n;
    printf("Quantos produtos? ");
    if (scanf("%d", &n) != 1 || n <= 0) {
        printf("Número inválido.\n");
        return 1;
    }

    double *lista = ler_precos(n);
    if (lista == NULL) {
        printf("Não consegui ler os preços.\n");
        return 1;
    }

    double total = 0;
    double maior = lista[0];
    for (int i = 0; i < n; i++) {
        total += lista[i];
        if (lista[i] > maior) {
            maior = lista[i];
        }
    }
    printf("Total: R$ %.2f\n", total);
    printf("Mais caro: R$ %.2f\n", maior);
    return 0;
}
#include <stdio.h>
#include <stdlib.h>

/* Reserva e lê n preços. Quem chamar tem de chamar free. */
double *ler_precos(int n) {
    double *precos = malloc(n * sizeof *precos);
    if (precos == NULL) {
        return NULL;
    }
    for (int i = 0; i < n; i++) {
        printf("Preço %d: ", i + 1);
        if (scanf("%lf", &precos[i]) != 1) {
            free(precos);
            return NULL;
        }
    }
    return precos;
}

int main(void) {
    int n;
    printf("Quantos produtos? ");
    if (scanf("%d", &n) != 1 || n <= 0) {
        printf("Número inválido.\n");
        return 1;
    }

    double *lista = ler_precos(n);
    if (lista == NULL) {
        printf("Não consegui ler os preços.\n");
        return 1;
    }

    double total = 0;
    double maior = lista[0];
    for (int i = 0; i < n; i++) {
        total += lista[i];
        if (lista[i] > maior) {
            maior = lista[i];
        }
    }
    printf("Total: R$ %.2f\n", total);
    printf("Mais caro: R$ %.2f\n", maior);
    free(lista);
    return 0;
}

No seu Linux, rode a solução com valgrind --leak-check=full ./precos < entrada.txt e confira o All heap blocks were freed -- no leaks are possible. Apague o free(lista); e rode de novo: o Valgrind mostra o bloco perdido. É o assunto da aula Vazamentos de Memória.


Resumo

  • malloc(bytes) (de stdlib.h) reserva memória no heap e devolve o endereço, ou NULL se não deu. Confira o NULL logo depois de todo malloc.
  • O tamanho é em bytes: T *v = malloc(n * sizeof *v);. Esquecer o sizeof estoura o bloco, e o erro aparece longe (malloc(): corrupted top size).
  • O ponteiro do malloc funciona como um array: v[i]. E o tamanho pode vir do usuário.
  • O conteúdo de um bloco novo é lixo: dê valor antes de ler, ou use calloc.
  • free(p) devolve o bloco. Três regras: não usar depois, liberar uma vez só, e só o endereço exato que veio do malloc. free(NULL) não faz nada; faça p = NULL depois do free.
  • Todo bloco tem um dono, responsável pelo free.
  • No editor do guia, ponteiro pendurado, free duplo e free de endereço errado passam calados. No Linux, o gcc aborta em alguns casos, e o -fsanitize=address e o Valgrind mostram todos, com a linha.