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

1. Introdução à alocação dinâmica de memória

1.1. Diferença entre alocação estática, automática e dinâmica

Em C, a memória pode ser gerenciada de três formas principais:

  • Estática: variáveis globais e static existem durante toda a execução do programa.
  • Automática: variáveis locais (na pilha) são criadas ao entrar em um escopo e destruídas ao sair.
  • Dinâmica: blocos de memória no heap são alocados e liberados manualmente pelo programador.

A alocação dinâmica oferece flexibilidade para criar estruturas de tamanho variável em tempo de execução.

1.2. Quando e por que usar alocação dinâmica em C

Use alocação dinâmica quando:
- O tamanho dos dados não é conhecido em tempo de compilação.
- Você precisa de estruturas que crescem ou encolhem (listas, árvores).
- O escopo da memória precisa ultrapassar a função que a criou.
- Os dados são grandes demais para caber na pilha.

1.3. Visão geral das funções da biblioteca <stdlib.h>

As quatro funções fundamentais para gerenciamento de heap em C são:

#include <stdlib.h>

void *malloc(size_t size);
void *calloc(size_t num, size_t size);
void *realloc(void *ptr, size_t new_size);
void free(void *ptr);

2. malloc: alocação de memória bruta

2.1. Sintaxe e funcionamento

malloc aloca um bloco contíguo de size bytes e retorna um ponteiro void* para o início do bloco. O conteúdo não é inicializado.

int *p = (int*) malloc(10 * sizeof(int));
if (p == NULL) {
    fprintf(stderr, "Falha na alocação de memória\n");
    exit(1);
}

2.2. Conversão de ponteiro void* para o tipo desejado

Em C padrão (não C++), o cast é opcional, mas recomendado para clareza:

double *vetor = malloc(5 * sizeof(double));       // sem cast
char *buffer = (char*) malloc(256 * sizeof(char)); // com cast

2.3. Verificação de falha na alocação

Sempre verifique se o retorno é NULL. Um malloc pode falhar por falta de memória disponível.

int *dados = malloc(1000000 * sizeof(int));
if (!dados) {
    perror("malloc");
    return -1;
}

3. calloc: alocação com inicialização zero

3.1. Sintaxe

calloc aloca memória para num elementos de size bytes cada e inicializa todos os bits com zero.

int *arr = (int*) calloc(20, sizeof(int));
// Equivalente a malloc(20 * sizeof(int)) + memset(arr, 0, 20 * sizeof(int))

3.2. Diferenças fundamentais entre malloc e calloc

Característica malloc calloc
Parâmetros 1 (tamanho total) 2 (quantidade e tamanho)
Inicialização Não (lixo de memória) Sim (tudo zero)
Performance Mais rápido Mais lento (inicializa)
Segurança Menor Maior

3.3. Vantagens de segurança

Usar calloc evita bugs causados por acesso a memória não inicializada:

struct Pessoa *pessoas = calloc(10, sizeof(struct Pessoa));
// Todos os campos numéricos serão 0, ponteiros serão NULL

4. realloc: redimensionamento de blocos alocados

4.1. Sintaxe

realloc redimensiona um bloco previamente alocado para new_size bytes.

int *arr = malloc(5 * sizeof(int));
arr = realloc(arr, 10 * sizeof(int)); // Expande para 10 elementos

4.2. Comportamento

  • Se o novo tamanho for maior, o conteúdo original é preservado e os novos bytes são não inicializados.
  • Se for menor, o conteúdo é truncado.
  • Se não for possível expandir no mesmo lugar, realloc aloca nova região, copia os dados e libera a antiga.
int *temp = realloc(arr, 20 * sizeof(int));
if (temp == NULL) {
    // arr ainda é válido, mas não foi redimensionado
    free(arr);
    exit(1);
}
arr = temp;

4.3. Cuidados importantes

Nunca faça arr = realloc(arr, novo_tamanho) diretamente sem variável temporária. Se realloc falhar, você perde o ponteiro original.

5. free: liberação de memória alocada

5.1. Sintaxe e regras

free libera o bloco apontado por ptr para o heap.

int *p = malloc(100 * sizeof(int));
// ... usa p ...
free(p);

Regras fundamentais:
- Só libere memória que foi alocada com malloc, calloc ou realloc.
- Nunca libere o mesmo ponteiro duas vezes.
- Liberar NULL é seguro (não faz nada).

5.2. Consequências de não liberar

Memory leak: a memória nunca é devolvida ao sistema, causando degradação e eventual falha.

// Exemplo de vazamento em loop
for (int i = 0; i < 1000; i++) {
    char *buf = malloc(1024); // sem free!
}

5.3. Boas práticas: ponteiros nulos após free

Atribua NULL ao ponteiro após liberá-lo para evitar ponteiros soltos (dangling pointers):

free(ptr);
ptr = NULL;

6. Armadilhas comuns e erros frequentes

6.1. Acesso a memória já liberada (dangling pointers)

int *p = malloc(sizeof(int));
free(p);
*p = 42; // Comportamento indefinido!

6.2. Vazamentos em funções

char* criar_string() {
    char *s = malloc(100);
    strcpy(s, "Olá");
    return s; // Quem chamou deve liberar!
}

int main() {
    char *str = criar_string();
    // ... esqueceu de free(str) ...
    return 0;
}

6.3. Subalocação e estouro de buffer

int *arr = malloc(5 * sizeof(int));
arr[10] = 42; // Estouro de buffer! Acesso fora da área alocada

7. Exemplos práticos integrados

7.1. Vetor dinâmico com malloc e realloc

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

int main() {
    int capacidade = 4;
    int *vetor = malloc(capacidade * sizeof(int));
    int tamanho = 0;

    for (int i = 0; i < 10; i++) {
        if (tamanho >= capacidade) {
            capacidade *= 2;
            int *temp = realloc(vetor, capacidade * sizeof(int));
            if (temp == NULL) {
                free(vetor);
                return 1;
            }
            vetor = temp;
        }
        vetor[tamanho++] = i * i;
    }

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

    free(vetor);
    return 0;
}

7.2. Matriz alocada dinamicamente com calloc

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

int main() {
    int linhas = 3, colunas = 4;
    int **matriz = calloc(linhas, sizeof(int*));

    for (int i = 0; i < linhas; i++) {
        matriz[i] = calloc(colunas, sizeof(int));
    }

    // Preenche e exibe a matriz
    for (int i = 0; i < linhas; i++) {
        for (int j = 0; j < colunas; j++) {
            matriz[i][j] = i * colunas + j;
            printf("%2d ", matriz[i][j]);
        }
        printf("\n");
    }

    // Liberação: ordem inversa da alocação
    for (int i = 0; i < linhas; i++) {
        free(matriz[i]);
    }
    free(matriz);

    return 0;
}

7.3. Uso combinado com structs

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

typedef struct {
    char *nome;
    int idade;
} Pessoa;

Pessoa* criar_pessoa(const char *nome, int idade) {
    Pessoa *p = malloc(sizeof(Pessoa));
    p->nome = malloc(strlen(nome) + 1);
    strcpy(p->nome, nome);
    p->idade = idade;
    return p;
}

void destruir_pessoa(Pessoa *p) {
    free(p->nome);
    free(p);
}

int main() {
    Pessoa *joao = criar_pessoa("João", 30);
    printf("%s tem %d anos\n", joao->nome, joao->idade);
    destruir_pessoa(joao);
    return 0;
}

8. Boas práticas e ferramentas de depuração

8.1. Estratégias para evitar memory leaks

  • Sempre pareie cada malloc/calloc/realloc com um free.
  • Use contadores de alocações em depuração.
  • Em projetos grandes, considere wrappers que rastreiam alocações.

8.2. Introdução ao Valgrind

Valgrind é uma ferramenta essencial para detectar vazamentos e erros de memória:

$ gcc -g programa.c -o programa
$ valgrind --leak-check=full ./programa

8.3. Padrões de design

  • Alocar e liberar no mesmo escopo: quando possível, mantenha alocação e liberação na mesma função.
  • Documente a responsabilidade: comente quem deve liberar a memória.
  • Use funções de criação/destruição: encapsule alocação e liberação em funções pareadas.

Referências