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
- The C Programming Language (K&R), Kernighan & Ritchie — Cap. 8.7, A Storage Allocator
- cppreference —
malloc,calloc,realloc,free— https://en.cppreference.com/w/c/memory - Modern C, Jens Gustedt — capítulo sobre alocação dinâmica e tempo de vida — https://gustedt.gitlabpages.inria.fr/modern-c/
- CERT C — MEM series (MEM30-C a MEM36-C), sobre uso correto de memória dinâmica — https://wiki.sei.cmu.edu/confluence/display/c
- Understanding and Using C Pointers, Richard Reese (O'Reilly) — capítulos sobre gerenciamento dinâmico
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