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ça

Liberaçã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ática
Importante

Apó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:

flowchart LR
    A("Alocar") --> B("Usar")
    B --> C("Liberar")  
    C --> D("Invalidar")
    D --> A

  • Alocar (malloc, calloc ou realloc)
  • 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 destino
  • memmove: copia bytes com segurança mesmo se houver sobreposição
  • memset: 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:

#include <stdio.h>
#include <string.h>

int main() {
    int origem[5] = {1, 2, 3, 4, 5};
    int destino[5];

    memcpy(destino, origem, 5 * sizeof(int));

    for (int i = 0; i < 5; i++)
        printf("%d ", destino[i]);
    
    return 0;
}

Saída:

1 2 3 4 5
Importante

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:

#include <stdio.h>
#include <string.h>

int main() {
    char texto[20] = "ABCDEF";

    // Copiando parte sobreposta
    memmove(texto + 2, texto, 4);
    texto[6] = '\0'; // Caracter de fim de string

    printf("%s\n", texto); // Resultado: "ABABCD"
    return 0;
}

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:

#include <stdio.h>
#include <string.h>

int main() {
    int v[5];
    memset(v, 0, 5 * sizeof(int));

    for (int i = 0; i < 5; i++)
        printf("%d ", v[i]);
    
    return 0;
}

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
Importante

Os seguintes cuidados são recomendados quando estamos programando com uso de memória dinâmica.

  • Sempre use sizeof para 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)).
  • Prefira memmove quando 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.

#include <stdio.h>

int main() {
    int *p;       // não inicializado!
    *p = 42;      // ❌ comportamento indefinido!
    printf("%d\n", *p);
    return 0;
}
Cuidado

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.

#include <stdlib.h>

int main() {
    int *p = malloc(sizeof(int));
    *p = 10;
    free(p);
    printf("%d\n", *p); // ❌ uso após liberação
    return 0;
}
Cuidado

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.

#include <stdlib.h>

int main() {
    int *p = malloc(100 * sizeof(int));
    p = NULL;  // ❌ perdemos a referência — vazamento!
    return 0;
}
Cuidado

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);
Dica

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.

#include <stdlib.h>

int main() {
    int *p = malloc(sizeof(int));
    free(p);
    free(p); // ❌ ERRO: double free
    return 0;
}
Cuidado

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.

#include <stdio.h>

int main() {
    int v[3] = {1, 2, 3};
    v[3] = 99; // índice inválido — overflow
    printf("%d\n", v[3]); // ❌ comportamento indefinido
    return 0;
}
Cuidado

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); // ✅ correto

Zerar 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.

# Qual é a principal diferença entre a memória *stack* e a *heap*? > Pense em quem controla o tempo de vida das variáveis. 1. [ ] Ambas são áreas de memória estática. 1. [ ] A *stack* é usada para dados dinâmicos e a *heap* para dados locais. 1. [x] A *stack* é gerenciada automaticamente, enquanto a *heap* exige liberação manual. 1. [ ] A *heap* é usada apenas por funções recursivas. # Qual é o princípio fundamental da alocação dinâmica de memória em C? > Ele garante que não haja vazamentos de memória. 1. [x] Quem aloca, deve liberar. 1. [ ] Cada variável deve ser liberada ao final da função. 1. [ ] Sempre usar `free` antes de `malloc`. 1. [ ] Usar `calloc` e `realloc` em conjunto. # O que acontece se `malloc` falhar ao tentar alocar memória? > Lembre-se do valor de retorno. 1. [ ] Retorna um ponteiro inválido. 1. [x] Retorna `NULL`. 1. [ ] Lança uma exceção. 1. [ ] Finaliza automaticamente o programa. # Qual das seguintes funções inicializa automaticamente a memória com zeros? > Ela recebe dois parâmetros: número de elementos e tamanho de cada um. 1. [ ] `malloc` 1. [x] `calloc` 1. [ ] `realloc` 1. [ ] `memset` # Qual é o uso correto da função `realloc` para redimensionar um vetor de inteiros? > Considere a boa prática de usar um ponteiro temporário. 1. [ ] `v = realloc(v, novo_tamanho);` 1. [ ] `realloc(v, novo_tamanho);` 1. [x] `temp = realloc(v, novo_tamanho); if (temp != NULL) v = temp;` 1. [ ] `realloc(&v, novo_tamanho);` # Quais das alternativas abaixo são boas práticas ao liberar memória? > Lembre-se do ciclo: alocar, usar, liberar, invalidar. - [x] Sempre liberar com `free` antes de perder a referência. - [x] Definir o ponteiro como `NULL` após liberar. - [ ] Usar `delete` para desalocar. - [ ] Reutilizar o mesmo ponteiro após `free` sem reatribuir. # Coloque na ordem correta as etapas do ciclo de manipulação de memória dinâmica. > Siga o ciclo apresentado no fluxograma da seção. 1. Alocar (`malloc`, `calloc` ou `realloc`) 2. Usar (ler e escrever por meio do ponteiro) 3. Liberar (`free`) 4. Invalidar o ponteiro (`ptr = NULL`) # Qual é a função adequada para copiar dados entre regiões de memória que podem se sobrepor? > Ela é mais segura que `memcpy` em casos de sobreposição. 1. [ ] `memcpy` 1. [x] `memmove` 1. [ ] `memset` 1. [ ] `strcpy` # Quais são cuidados essenciais ao usar `memcpy`, `memmove` e `memset`? > Elas trabalham em nível de bytes. - [x] Sempre usar `sizeof` para calcular o número correto de bytes. - [x] Usar `memmove` quando houver dúvida sobre sobreposição. - [ ] Usar `memcpy` para estruturas com ponteiros internos. - [ ] Passar valores inteiros diretamente para `memset` sem conversão. # Qual é o erro que ocorre ao usar um ponteiro não inicializado? > Ele contém um endereço aleatório. 1. [x] *Wild pointer* 1. [ ] *Dangling pointer* 1. [ ] *Memory leak* 1. [ ] *Double free* # Quais das situações abaixo podem causar comportamento indefinido? > Pense em acessos inválidos à memória. - [x] Usar ponteiro após `free`. - [x] Escrever fora dos limites de um vetor. - [ ] Liberar ponteiro definido como `NULL`. - [ ] Atribuir `NULL` a um ponteiro liberado. # O que caracteriza um *memory leak*? > O programa perde acesso ao bloco de memória alocado. 1. [x] Perda da referência para uma região alocada sem `free`. 1. [ ] Escrita além dos limites do vetor. 1. [ ] Liberação dupla da mesma região. 1. [ ] Ponteiro não inicializado. # Como evitar o erro *double free*? > Lembre-se da prática após `free`. - [x] Atribuir `NULL` após liberar. - [x] Controlar claramente quem é o “dono” da memória. - [ ] Chamar `free` duas vezes para garantir a liberação. - [ ] Usar `malloc` e `free` na mesma função sempre. # Qual prática ajuda a evitar *buffer overflow*? > Pense no controle dos índices. 1. [x] Verificar limites dos vetores antes de acessar. 1. [ ] Usar `malloc` sempre em vez de arrays. 1. [ ] Evitar o uso de `sizeof`. 1. [ ] Usar ponteiros não inicializados. # Quais são boas práticas de segurança com memória sensível? > Por exemplo, quando lidamos com senhas. - [x] Zerar a memória antes de liberar. - [x] Encapsular alocações em funções seguras como `safe_malloc`. - [ ] Deixar senhas em memória após uso. - [ ] Evitar o uso de `free` para não perder dados. # Coloque na ordem correta as ações da função `safe_malloc` descrita no texto. > Pense no fluxo interno da função. 1. Chamar `malloc` com o tamanho desejado. 2. Verificar se o retorno é `NULL`. 3. Imprimir mensagem de erro em `stderr` se necessário. 4. Retornar o ponteiro válido ao chamador.
De volta ao topo