Alocação Dinâmica: malloc, calloc, realloc e free

[354] Alocação Dinâmica: malloc, calloc, realloc e free

Toda alocação cria uma dívida que só o free quita. Daí saem as quatro funções do heap, o hábito de anular o ponteiro logo depois de liberar, o motivo de escrever v = realloc(v, ...) ser frágil, e o aviso que pega quase todo mundo uma vez: malloc entrega memória com lixo dentro, nunca zerada.
Linguagem C

10 min de leitura

Este é, talvez, o artigo mais característico de todo o curso. Alocar memória manualmente é o que faz o C ser o C: nenhum coletor de lixo, nenhuma rede de segurança, apenas você e o controle direto sobre cada byte que pede ao sistema. Com o mapa da aula anterior na cabeça — pilha versus heap —, hoje você aprende a requisitar memória do heap, usá-la e devolvê-la. É uma habilidade que dá poder e cobra disciplina em igual medida.

malloc: pedindo memória ao sistema

A função malloc (memory allocation), do cabeçalho <stdlib.h>, pede ao sistema uma quantidade de bytes no heap e devolve um ponteiro para o início desse espaço:

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

int main(void) {
    // pede espaço para um int no heap
    int *p = malloc(sizeof(int));

    if (p == NULL) {        // malloc pode falhar!
        printf("Falha ao alocar memória.\n");
        return 1;
    }

    *p = 42;                // usa a memória como qualquer ponteiro
    printf("Valor no heap: %d\n", *p);

    free(p);                // devolve a memória ao sistema
    return 0;
}

Três coisas merecem sua atenção aqui, e cada uma é uma regra que você vai repetir milhares de vezes. Primeiro, passamos a malloc o número de bytes que queremos — e usamos sizeof(int) em vez de escrever 4, porque o tamanho de um int varia entre máquinas. Segundo, malloc pode falhar (se não houver memória disponível) e, nesse caso, devolve NULL; por isso sempre verificamos o retorno antes de usá-lo. Terceiro, e mais importante: todo malloc exige um free correspondente, mais cedo ou mais tarde. Essa é a lei fundamental do heap.

Alocando um vetor dinâmico

O verdadeiro poder de malloc aparece quando o tamanho só é conhecido em tempo de execução. Aqui, finalmente, resolvemos o problema que a pilha não conseguia:

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

int main(void) {
    int n;
    printf("Quantos números? ");
    scanf("%d", &n);

    // aloca espaço para n inteiros — n só é conhecido agora!
    int *v = malloc(n * sizeof(int));
    if (v == NULL) {
        printf("Falha ao alocar.\n");
        return 1;
    }

    for (int i = 0; i < n; i++) {
        v[i] = (i + 1) * 10; // usa como um vetor comum
    }

    for (int i = 0; i < n; i++) {
        printf("%d ", v[i]);
    }
    printf("\n");

    free(v); // libera o vetor inteiro de uma vez
    return 0;
}

Repare que, uma vez alocado, v se comporta exatamente como qualquer vetor: você usa v[i] normalmente. A diferença é que o tamanho n foi decidido em tempo de execução, e que a memória vive no heap — sobrevivendo até o free. O cálculo n * sizeof(int) é o padrão para alocar n elementos: total de bytes é igual ao número de elementos vezes o tamanho de cada um.

calloc: alocar e zerar

A função calloc (contiguous allocation) é irmã de malloc, com duas diferenças: ela recebe o número de elementos e o tamanho de cada um separadamente, e — o mais útil — zera toda a memória alocada. Com malloc, a memória vem com lixo (valores imprevisíveis do uso anterior); com calloc, vem limpa:

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

int main(void) {
    int n = 5;

    // aloca 5 inteiros, todos inicializados em 0
    int *v = calloc(n, sizeof(int));
    if (v == NULL) return 1;

    for (int i = 0; i < n; i++) {
        printf("%d ", v[i]); // 0 0 0 0 0 — garantidamente zerado
    }
    printf("\n");

    free(v);
    return 0;
}

Use calloc quando quiser começar com tudo em zero — por exemplo, um vetor de contadores. É um erro comum assumir que malloc zera a memória; ele não zera. Se você precisa de valores iniciais em zero, use calloc ou zere manualmente.

realloc: redimensionando o que já existe

E se você alocou espaço para 10 elementos e descobriu que precisa de 20? A função realloc redimensiona um bloco já alocado, preservando o conteúdo existente:

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

int main(void) {
    int *v = malloc(3 * sizeof(int));
    if (v == NULL) return 1;
    v[0] = 1; v[1] = 2; v[2] = 3;

    // agora precisamos de 6 posições
    int *maior = realloc(v, 6 * sizeof(int));
    if (maior == NULL) {
        free(v);         // realloc falhou: o v original ainda é válido
        return 1;
    }
    v = maior;           // sucesso: passamos a usar o novo ponteiro

    v[3] = 4; v[4] = 5; v[5] = 6;

    for (int i = 0; i < 6; i++) {
        printf("%d ", v[i]); // 1 2 3 4 5 6 — os antigos foram preservados
    }
    printf("\n");

    free(v);
    return 0;
}

realloc é sutil e merece cuidado. Ela pode precisar mover o bloco inteiro para outro lugar da memória (se não houver espaço contíguo para crescer no local atual), e nesse caso devolve um novo endereço, copiando o conteúdo antigo. Por isso o padrão idiomático: guarde o retorno numa variável temporária (maior) e só sobrescreva v após confirmar que não foi NULL. Se você fizesse v = realloc(v, ...) diretamente e a função falhasse, perderia o ponteiro original — e vazaria a memória que ele apontava. Esse detalhe separa código robusto de código frágil.

free: a outra metade de todo malloc

A função free devolve ao sistema a memória que você alocou. Ela é simples, mas cercada de armadilhas. Duas regras invioláveis:

Primeiro, nunca use memória depois de liberá-la. Após free(p), o ponteiro p ainda contém o endereço antigo, mas aquela memória já não é sua — usá-la é o use-after-free, uma das falhas de segurança mais graves que existem:

free(p);
*p = 10; // ERRO GRAVE: use-after-free, a memória já foi devolvida

Segundo, nunca libere duas vezes o mesmo bloco. O double-free corrompe as estruturas internas do gerenciador de memória e costuma travar o programa de formas imprevisíveis:

free(p);
free(p); // ERRO GRAVE: double-free

Uma defesa simples e muito usada contra ambos é, logo após liberar, atribuir NULL ao ponteiro. Como free(NULL) é seguro (não faz nada) e desreferenciar NULL trava imediatamente (em vez de corromper silenciosamente), esse hábito converte bugs sutis em falhas óbvias:

free(p);
p = NULL; // agora um uso acidental trava na hora, e um free duplo é inofensivo

O par sagrado: um free para cada malloc

Se há uma única frase para levar desta aula, é esta: toda alocação precisa de exatamente uma liberação. Cada malloc, calloc ou realloc bem-sucedido cria uma dívida que só o free quita. Esquecer o free gera um vazamento (memória presa até o fim do programa); liberar cedo demais gera um use-after-free; liberar duas vezes gera um double-free. O equilíbrio exato — uma alocação, um uso, uma liberação — é a dança que você vai praticar pelo resto da sua vida em C.

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

char *duplicar_texto(const char *origem) {
    // aloca espaço para a cópia, incluindo o terminador '\0'
    char *copia = malloc(strlen_simples(origem) + 1);
    if (copia == NULL) return NULL;

    int i = 0;
    while (origem[i] != '\0') {
        copia[i] = origem[i];
        i++;
    }
    copia[i] = '\0'; // não esqueça o terminador!
    return copia;    // devolve memória do heap, que sobrevive ao retorno
}

int strlen_simples(const char *s) {
    int n = 0;
    while (s[n] != '\0') n++;
    return n;
}

int main(void) {
    char *nome = duplicar_texto("Linguagem C");
    if (nome != NULL) {
        printf("%s\n", nome);
        free(nome); // quem recebe a memória é quem a libera
    }
    return 0;
}

Este exemplo mostra o padrão completo e o motivo de o heap existir: duplicar_texto cria uma string nova que sobrevive ao retorno da função (impossível na pilha) e devolve o ponteiro; main a usa e depois a libera. Note o + 1 na alocação, reservando espaço para o '\0' — a lição da aula de strings, agora aplicada à memória dinâmica.

O que vem a seguir

Hoje você adquiriu a habilidade mais icônica do C: pedir memória ao heap com malloc, calloc e realloc, e devolvê-la com free, respeitando o par sagrado de uma alocação para uma liberação. Vimos também os três pecados capitais da memória — vazamento, use-after-free e double-free. Mas conhecer as regras não basta; erros de memória são sorrateiros e escapam à leitura mais atenta. Na próxima aula, vamos aprender a caçá-los com ferramentas — o Valgrind e os sanitizers —, que enxergam o que nossos olhos não veem.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Aloque com malloc espaço para um único double, guarde nele o valor 3.14, imprima-o e libere a memória. Não esqueça de verificar se malloc retornou NULL.

Ver resposta

✓ Resposta:

#include <stdio.h>
#include <stdlib.h>
int main(void) {
    double *p = malloc(sizeof(double));
    if (p == NULL) return 1;
    *p = 3.14;
    printf("%.2f\n", *p);
    free(p);
    return 0;
}

Exercício 2

Peça ao usuário um número n e aloque dinamicamente um vetor de n inteiros. Preencha-o com os quadrados de 1 a n (v[i] = (i+1)*(i+1)), imprima e libere.

Ver resposta

✓ Resposta:

#include <stdio.h>
#include <stdlib.h>
int main(void) {
    int n;
    printf("n = ");
    scanf("%d", &n);
    if (n <= 0) return 1;

    int *v = malloc(n * sizeof(int));
    if (v == NULL) return 1;

    for (int i = 0; i < n; i++) v[i] = (i + 1) * (i + 1);
    for (int i = 0; i < n; i++) printf("%d ", v[i]);
    printf("\n");

    free(v);
    return 0;
}

Exercício 3

Explique a diferença entre malloc e calloc. Escreva um trecho que demonstre que a memória de malloc pode conter lixo, enquanto a de calloc vem zerada.

Ver resposta

✓ Resposta: malloc(bytes) reserva o espaço mas não inicializa o conteúdo — os bytes contêm o que quer que estivesse ali antes (lixo). calloc(n, tamanho) reserva n elementos e zera todos os bytes. Demonstração:

int *a = malloc(4 * sizeof(int));
int *b = calloc(4, sizeof(int));
for (int i = 0; i < 4; i++) printf("%d ", a[i]); // valores imprevisíveis (lixo)
printf("\n");
for (int i = 0; i < 4; i++) printf("%d ", b[i]); // 0 0 0 0, garantido
free(a); free(b);

(Na prática, malloc recém-alocado às vezes vem zerado por acaso, dependendo do sistema — mas você nunca deve contar com isso.)

Exercício 4

Explique por que o padrão p = realloc(p, novo_tamanho); é perigoso, e reescreva-o da forma segura usando uma variável temporária.

Ver resposta

✓ Resposta: É perigoso porque, se realloc falhar, ela devolve NULL mas não libera o bloco original — que continua válido e ainda precisa ser liberado. Ao fazer p = realloc(p, ...), você sobrescreve o ponteiro original com NULL, perde o único acesso ao bloco antigo e vaza aquela memória (e ainda fica sem os dados). Forma segura:

int *tmp = realloc(p, novo_tamanho);
if (tmp == NULL) {
    free(p);        // o p original ainda é válido; liberamos antes de sair
    return 1;
}
p = tmp;            // só agora é seguro sobrescrever p

Exercício 5

O trecho abaixo tem dois bugs de memória. Identifique ambos e corrija:

int *v = malloc(5 * sizeof(int));
v[0] = 10;
free(v);
printf("%d\n", v[0]);
free(v);
Ver resposta

✓ Resposta: Os dois bugs: (1) use-after-free — a linha printf("%d\n", v[0]); acessa v[0] depois de free(v), quando a memória já não é mais nossa. (2) double-free — o segundo free(v) libera de novo um bloco já liberado, corrompendo o gerenciador de memória. Versão corrigida (ler o valor antes de liberar, e liberar só uma vez):

int *v = malloc(5 * sizeof(int));
if (v == NULL) return 1;
v[0] = 10;
printf("%d\n", v[0]); // usa ANTES de liberar
free(v);
v = NULL;             // hábito defensivo: evita use-after-free e double-free acidentais
Comentários

Mais em Linguagem C

Vetores e o Primeiro Contato com a Memória Contígua
Vetores e o Primeiro Contato com a Memória Contígua

Guardados lado a lado na memória — e é esse detalhe, não a sintaxe dos…

Undefined Behavior: O Território Perigoso do C
Undefined Behavior: O Território Perigoso do C

Um índice a mais num vetor de cinco pode imprimir tudo certo, travar, ou virar…

Código Limpo em C: Nomes, Funções e Legibilidade
Código Limpo em C: Nomes, Funções e Legibilidade

f(a, b) e somar_intervalo(inicio, fim) fazem exatamente a mesma coisa; só a…