flowchart LR
A("Alocar") --> B("Usar")
B --> C("Liberar")
C --> D("Invalidar")
D --> A
Gerenciamento da Memória
Até aqui, vimos como generalizar dados e comportamentos em C usando ponteiros e callbacks. No entanto, toda essa flexibilidade traz a responsabilidade de gerenciar manualmente a memória.
Nesta seção, vamos entender como a linguagem C lida com a alocação dinâmica, isto é, a capacidade de reservar e liberar memória em tempo de execução. Mais importante, veremos como evitar os erros de memória mais comuns que podem comprometer a estabilidade e a segurança de um programa.
Alocação e liberação dinâmica
Quando trabalhamos com arrays ou estruturas em C, geralmente definimos seus tamanhos em tempo de compilação. Eles ficam armazenados em uma área de memória chamada stack.
No entanto, há muitos casos em que o tamanho dos dados não é conhecido antecipadamente. Para esses casos, o C fornece funções de alocação dinâmica de memória, que permitem reservar e liberar espaço em tempo de execução. Esses blocos de memória são criados em uma região especial chamada heap.
Dizemos que a memória stack é estática, enquanto que a heap é dinâmica. Na stack, quando a função termina, a variável é destruída. Já na heap, o programador decide quando alocar e liberar.
A alocação dinâmica oferece flexibilidade, mas também exige disciplina. O princípio fundamental é: quem aloca, deve liberar.
Alocação simples
A função malloc (de memory allocation) reserva um bloco contíguo de memória de tamanho especificado em bytes.
int *v = malloc(5 * sizeof(int));O argumento é o número total de bytes a serem alocados. No trecho acima, estamos alocando o espaço suficiente para cinco inteiros. O retorno é um ponteiro genérico (void*) para o início do bloco. Caso a alocação falhe, malloc retorna NULL.
Se liga! Em C, ao contrário de C++, não há necessidade de fazer o cast do ponteiro genérico neste caso.
Como não temos garantia que a memória foi alocada, precisamos tratar quando malloc falha. Isso é geramente feito com um bloco if.
int *v = malloc(5 * sizeof(int));
if (v == NULL) {
fprintf(stderr, "Erro: sem memória suficiente!\n");
return 1;
} No trecho acima, poderíamos ter usado printf ao invés de fprintf. A escolha, por fprintf se dá por ser o padrão para erros. Ele permite especificar exatamente para onde a saída deve ir. Ao usarmos stderr (de standard error), estamos dizendo ao sistema que esta mensagem é um erro e deve ser tratada como tal.
Se liga! A função perror imprime uma mensagem de erro em stderr também e é muito usada para depuração.
Quando usamos printf a saída é armazenada em buffer, portanto a mensagem de erro poderia ficar presa e nunca ser exibida antes do programa abortar. Já quando optamos por fprintf, a saída é unbuffered, ou seja, é exibida imediatamente no terminal.
Resumindo, use fprinf mensagens de erro, diagnóstico e logs críticos e use printf para mensagens de sucesso, resultado da aplicação e saída esperada.
O return 1 informa ao sistema operacional que algo deu errado .
Alocação e inicialização
A função calloc (de contiguous allocation) funciona como malloc, mas com duas diferenças:
- Recebe dois parâmetros, número de elementos e tamanho de cada um;
- Inicializa o bloco com zeros.
int *v = calloc(5, sizeof(int));Assim, todo as posições começam zeradas, o que evita leituras de lixo de memória.
Redimensionamento de bloco
A função realloc permite ajustar o tamanho de um bloco de memória já alocado.
int *v = calloc(5, sizeof(int));
int *novo = realloc(v, 10 * sizeof(int));Se houver espaço contíguo suficiente, o bloco é expandido no mesmo local. Caso contrário, um novo bloco é alocado e o conteúdo antigo é copiado automaticamente. Em caso de falha, retorna NULL e não desaloca o bloco de memória original. Por isso, é recomendado sempre usar um ponteiro temporário:
int *v = calloc(5, sizeof(int));
int *temp = realloc(v, 10 * sizeof(int));
if (temp == NULL) {
fprintf(stderr, "Erro ao realocar memória!\n");
return 1;
}
v = temp; // Atualiza o ponteiro com segurançaLiberação de memória
A função free libera um bloco previamente alocado com malloc, calloc ou realloc. Ela devolve o espaço ao sistema operacional, mas não zera o ponteiro.
free(v);
v = NULL; // Boa práticaApós liberar, o ponteiro ainda guarda o endereço antigo, agora inválido. Atribuir NULL ajuda a evitar o uso acidental, um erro clássico conhecido como dangling pointer.
Este ciclo é a base de toda manipulação segura de memória dinâmica em C:
- Alocar (
malloc,callocourealloc) - Usar (ler e escrever por meio do ponteiro)
- Liberar (
free) - Invalidar o ponteiro (
ptr = NULL)
Com essas funções, temos total controle sobre a vida útil de cada dado em memória. No entanto, esse poder vem acompanhado de riscos. Podemos citar os acessos indevidos, vazamentos e corrupção de memória como erros comuns quando o ciclo de alocação não é seguido corretamente.
Na próxima seção, vamos explorar as funções de manipulação de blocos de memória e entender como operá-las com segurança antes de mergulhar nos erros clássicos de memória.
Manipulação de blocos de memória
Quando trabalhamos com alocação dinâmica, frequentemente precisamos copiar dados de uma região de memória para outra. O C oferece um conjunto de funções na biblioteca <string.h> para manipular blocos de memória de forma genérica, usando ponteiros do tipo void *.
Essas funções operam diretamente em bytes, e não conhecem o tipo de dado armazenado. Por isso, exigem atenção com o tamanho dos blocos manipulados (geralmente expresso com size_t).
As três funções principais para manipulação de blocos de memória são:
memcpy: copia bytes de uma origem para um destinomemmove: copia bytes com segurança mesmo se houver sobreposiçãomemset: preenche uma área de memória com um valor constante
Copiando blocos de memória
A função memcpy copia \(n\) bytes do bloco apontado por src (origem) para o bloco apontado por dest (destino).
void *memcpy(void *dest, const void *src, size_t n);Exemplo prático:
Saída:
1 2 3 4 5
A função memcpy não deve ser usada quando as regiões de origem e destino se sobrepõem. O comportamento é indefinido, podendo corromper os dados.
Cópia segura com sobreposição
A função memmove é semelhante a memcpy, mas trata corretamente casos de sobreposição. Se o bloco de destino estiver dentro da área de origem (ou vice-versa), ela ajusta a direção da cópia automaticamente.
void *memmove(void *dest, const void *src, size_t n);Exemplo:
Se tivéssemos usado memcpy nesse exemplo, o conteúdo de texto poderia ser corrompido, pois as regiões texto e texto + 2 se sobrepõem.
Inicializando memória
A função memset preenche um bloco de memória com um valor constante em bytes. Ela é muito usada para inicializar buffers ou zerar estruturas.
void *memset(void *ptr, int valor, size_t n);Exemplo:
Se liga! O valor passado a memset é interpretado como um byte, e não como um inteiro completo. Por exemplo, memset(v, 1, sizeof(int)*5) preencherá todos os bytes com 0x01, não com o inteiro 1.
Saída:
0 0 0 0 0
Os seguintes cuidados são recomendados quando estamos programando com uso de memória dinâmica.
- Sempre use
sizeofpara calcular o número correto de bytes.- Evite expressões mágicas como
memcpy(dest, src, 20). - Ao invés, use
memcpy(dest, src, n * sizeof(T)).
- Evite expressões mágicas como
- Prefira
memmovequando houver dúvida sobre sobreposição. - Zere estruturas antes de usá-las (por exemplo, buffers de strings).
- Evite manipular memória de tipos complexos (estruturas com ponteiros internos) com
memcpy- Isso pode quebrar o encapsulamento e gerar cópias superficiais perigosas.
Com essas funções, temos ferramentas poderosas para manipular diretamente a memória. Isso é um recurso essencial para implementar estruturas genéricas e operações de baixo nível. Entretanto, o uso incorreto dessas funções é uma das principais causas de erros sutis e difíceis de depurar em C.
Na próxima seção, estudaremos exatamente esses erros, os erros clássicos de memória, como dangling pointers, buffer overflow e double free, e como evitá-los com boas práticas.
Erros clássicos
Gerenciar memória manualmente é uma das maiores responsabilidades (e riscos) ao programar em C. Ao lidar com ponteiros e alocação dinâmica, pequenos descuidos podem causar comportamentos indefinidos, travamentos ou até falhas de segurança.
Nesta seção, veremos os erros mais comuns, seus sintomas e como evitá-los.
Ponteiros não inicializados (wild pointers)
Um wild pointer é um ponteiro que não foi inicializado antes do uso. Ele contém um valor de endereço aleatório, podendo apontar para qualquer lugar da memória.
Esse código pode travar o programa (segmentation fault), corromper dados ou até “funcionar”, mascarando o erro (o pior cenário).
Como boa prática, sempre inicialize ponteiros:
int *p = NULL;Se liga! Um ponteiro deve sempre apontar para algo válido, ou para NULL. Isso é chamado de inicialização defensiva.
Essa prática evita comportamentos indefinidos quando o ponteiro é usado antes de ser atribuído. Ela também facilita verificações de segurança:
if (p != NULL) {
*p = 42;
}Ponteiros pendentes (dangling pointers)
Um dangling pointer ocorre quando o ponteiro ainda referencia uma região de memória já liberada.
O bloco de memória foi liberado, mas o ponteiro ainda “acha” que é válido. Acessar ou modificar essa região causa comportamento indefinido.
Como boa prática, após free(p), defina p = NULL; para evitar acessos acidentais.
Vazamentos de memória (memory leaks)
Um memory leak acontece quando um programa perde a referência para uma região de memória alocada, sem chamá-la com free.
A memória continua alocada, mas inacessível. Em programas longos, isso pode acumular e exaurir a memória do sistema.
Como boas prática, sempre emparelhe malloc e free. Pois, cada chamada de alocação dinâmica deve ter uma contrapartida de liberação. Evite fluxos de execução que impeçam o free de ser chamado.
char *p = malloc(100);
if (!p) return; // ❌ erro de alocação
// ...
free(p);Centralize liberações num único ponto de saída, especialmente em funções longas.
Liberação duplicada (double free)
Um double free ocorre quando tentamos liberar o mesmo bloco de memória mais de uma vez.
Isso causa corrupção da estrutura interna do heap e frequentemente resulta em segmentation fault.
Como boa prática, após liberar, defina p = NULL.
Em programas maiores, é comum que uma função faça alocação de memória e outra a utilize. Para evitar vazamentos ou liberações incorretas, defina quem é o dono (owner) de cada bloco alocado.
Exemplo:
char* criar_mensagem() {
char *msg = malloc(50);
sprintf(msg, "Olá, mundo!");
return msg; // a função devolve a posse
}
int main() {
char *m = criar_mensagem();
puts(m);
free(m); // responsabilidade de quem recebeu
}Acesso fora dos limites (buffer overflow e underrun)
Um buffer overflow ocorre quando se escreve além do final (ou antes do início) de um bloco de memória.
Esse tipo de erro é grave: pode corromper variáveis adjacentes, causar travamentos ou vulnerabilidades de segurança.
Como boas prática, sempre verifique limites de vetores. Ao usar malloc, garanta que o tamanho esteja correto com sizeof.
Podemos resumir os tipos de erros abordados por meio da seguinte tabela.
| Tipo de Erro | Causa | Sintoma | Prevenção |
|---|---|---|---|
| Wild Pointer | Ponteiro não inicializado | Crash aleatório | Inicializar com NULL |
| Dangling Pointer | Uso após free |
Segmentation fault | p = NULL após free |
| Memory Leak | Perda de referência | Aumento de uso de memória | Sempre liberar |
| Double Free | Liberação repetida | Corrupção do heap | Controle de ownership |
| Buffer Overflow | Escrita fora dos limites | Corrupção de memória | Verificar limites |
Se liga! Ownership se refere ao proprietário do ponteiro. Se duas regiões estão liberando o ponteiro (double free), então estão compartilhando a guarda. Evite isso!
Erros de memória estão entre os mais sutis e perigosos em C. Eles não apenas causam falhas, mas podem introduzir vulnerabilidades de segurança sérias. Com boas práticas e ferramentas adequadas, é possível evitá-los e desenvolver sistemas robustos e confiáveis.
Boas práticas
A seguir, algumas sugestões de boas práticas quando gerenciamos a memória em C.
Evite operações parciais
Nunca tente liberar apenas parte de uma estrutura alocada. Sempre opere sobre a mesma referência retornada por malloc.
int *v = malloc(10 * sizeof(int));
int *p = v + 5;
free(p); // ❌ comportamento indefinido
free(v); // ✅ corretoZerar memória após liberação (quando necessário)
Em sistemas que lidam com dados sensíveis (como senhas), vale a pena limpar o conteúdo antes de liberar.
memset(senha, 0, tamanho);
free(senha);Isso impede que dados fiquem acessíveis em regiões de memória reaproveitadas.
Macros e wrappers de segurança
Em projetos grandes, é comum encapsular chamadas de alocação/liberação em funções auxiliares. Isso facilita rastrear erros e aplicar verificações centralizadas.
void* safe_malloc(size_t n) {
void *p = malloc(n);
if (p == NULL) {
fprintf(stderr, "Erro: malloc falhou\n");
return 1;
}
return p;
}Se liga! Com esse padrão, você garante que toda alocação seja verificada.
Nesta unidade, você aprendeu
✅ a alocar e desalocar a memória
✅ a copiar blocos de memória
✅ a evitar erros clássicos no gerenciamento de memrória
✅ a aplicar boas práticas em grandes projetos
Gerenciar memória em C é uma arte de equilíbrio entre controle total e responsabilidade absoluta. Com disciplina e padrões bem definidos, é possível construir sistemas robustos, performáticos e seguros.