Gerenciar estruturas dinâmicas é uma habilidade essencial para qualquer programador C que deseja criar aplicações eficientes e confiáveis. Diferente de linguagens com coleta de lixo, em C o programador é totalmente responsável pela alocação e liberação de memória. Isso oferece controle total, mas também exige disciplina para evitar erros comuns como vazamentos de memória, uso após liberação e dupla liberação.

Nesta aula, vamos explorar as funções básicas de alocação (malloc, calloc, realloc) e liberação (free), discutir como evitar vazamentos de memória, apresentar padrões de ownership que ajudam a definir quem é responsável por liberar cada bloco de memória, e finalizar com boas práticas que tornam seu código mais seguro e sustentável.

Alocação e liberação

Em C, a memória dinâmica é alocada no heap usando as funções da biblioteca padrão malloc, calloc e realloc, todas declaradas em <stdlib.h>. A função malloc aloca um bloco de bytes e retorna um ponteiro para o início, mas não inicializa o conteúdo. calloc aloca memória para um array de elementos e inicializa todos os bytes com zero. realloc redimensiona um bloco previamente alocado, preservando o conteúdo até o novo tamanho.

É crucial verificar se a alocação foi bem-sucedida: se o sistema não tiver memória suficiente, a função retorna NULL. Ignorar esse retorno pode levar a desreferenciamento de ponteiro nulo e crashes. A liberação é feita com a função free, que recebe o ponteiro retornado pela alocação. Após liberar, o ponteiro não deve ser usado novamente; é boa prática atribuir NULL ao ponteiro após liberá-lo para evitar ponteiros pendentes.

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

int main() {
    int *ptr = malloc(10 * sizeof(int));
    if (ptr == NULL) {
        fprintf(stderr, "Erro de alocação\n");
        return 1;
    }
    // Use a memória
    ptr[0] = 42;
    printf("%d\n", ptr[0]);
    free(ptr);
    ptr = NULL; // evita ponteiro pendente
    return 0;
}

Exemplo com calloc e realloc:

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

int main() {
    int *arr = calloc(5, sizeof(int)); // todos os elementos zerados
    if (arr == NULL) return 1;
    arr[0] = 10;
    arr[1] = 20;
    // Redimensiona para 10 elementos
    int *tmp = realloc(arr, 10 * sizeof(int));
    if (tmp == NULL) {
        free(arr);
        return 1;
    }
    arr = tmp;
    arr[9] = 99;
    printf("%d %d %d\n", arr[0], arr[1], arr[9]);
    free(arr);
    return 0;
}

Evitando vazamentos

Vazamento de memória ocorre quando um bloco alocado não é liberado, fazendo com que a memória seja perdida até o término do programa. Em programas de longa duração, isso pode esgotar a memória disponível. Para evitar vazamentos, é fundamental garantir que cada malloc/calloc/realloc tenha um free correspondente em todos os caminhos de execução, incluindo caminhos de erro.

Uma técnica comum é usar ferramentas como Valgrind para detectar vazamentos durante o desenvolvimento. Além disso, seguir a regra de que quem aloca é quem libera (ou documentar explicitamente a responsabilidade) ajuda a evitar confusões. Em estruturas de dados dinâmicas, como listas encadeadas ou árvores, é preciso liberar todos os nós recursivamente, tomando cuidado para não perder referências.

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

typedef struct Node {
    int data;
    struct Node *next;
} Node;

Node* create_node(int data) {
    Node *n = malloc(sizeof(Node));
    if (n == NULL) return NULL;
    n->data = data;
    n->next = NULL;
    return n;
}

void free_list(Node *head) {
    while (head != NULL) {
        Node *tmp = head;
        head = head->next;
        free(tmp);
    }
}

int main() {
    Node *head = create_node(1);
    head->next = create_node(2);
    head->next->next = create_node(3);
    // ... uso
    free_list(head);
    return 0;
}

Para estruturas mais complexas, como árvores binárias, a liberação deve ser feita em pós-ordem para não perder os filhos:

typedef struct TreeNode {
    int val;
    struct TreeNode *left, *right;
} TreeNode;

void free_tree(TreeNode *root) {
    if (root == NULL) return;
    free_tree(root->left);
    free_tree(root->right);
    free(root);
}

Padrões de ownership

Ownership (posse) define qual parte do código é responsável por liberar a memória de um determinado bloco. Em C, não há regras automáticas, então é crucial estabelecer convenções. Um padrão comum é a função que aloca ser a dona, e a responsabilidade de liberar é transferida para o chamador. Por exemplo, uma função create_pessoa retorna um ponteiro para uma estrutura alocada dinamicamente; o chamador deve liberar com free_pessoa.

Outro padrão é o uso de 'ownership' por referência: uma função pode receber um ponteiro e não assumir a posse, deixando claro que o chamador mantém a responsabilidade. Isso é comum em funções que apenas processam dados, como imprimir ou modificar.

Para evitar ambiguidades, muitos projetos adotam convenções de nomenclatura, como prefixar funções que alocam com create_ e as que liberam com destroy_ ou free_. Também é útil documentar nos comentários quem é o dono da memória.

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

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

void destroi_pessoa(Pessoa *p) {
    if (p == NULL) return;
    free(p->nome);
    free(p);
}

int main() {
    Pessoa *p = cria_pessoa("João", 30);
    // ...
    destroi_pessoa(p);
    return 0;
}

Boas práticas

Algumas boas práticas ajudam a reduzir erros de memória em C:

  • Sempre inicialize ponteiros com NULL.
  • Verifique o retorno de funções de alocação.
  • Libere memória imediatamente quando não for mais necessária.
  • Após free, atribua NULL ao ponteiro.
  • Evite alocar grandes blocos em loops; aloque fora e reutilize.
  • Use ferramentas de análise estática (como cppcheck) e dinâmica (Valgrind, AddressSanitizer).
  • Documente a posse da memória em comentários.
  • Prefira funções de alocação que inicializam (calloc) quando precisar de zeros.

Além disso, considere encapsular alocação e liberação em funções específicas, como visto nos exemplos, para centralizar a lógica e reduzir duplicação.

Referências

Exercícios

  1. Escreva um programa que aloque um array de 100 inteiros com malloc, preencha com valores, e depois libere a memória corretamente. Verifique se a alocação falhou e trate o erro.

    ✓ Resposta:
    #include <stdio.h>
    #include <stdlib.h>
    
    int main() {
        int *arr = malloc(100 * sizeof(int));
        if (arr == NULL) {
            fprintf(stderr, "Erro de alocação\n");
            return 1;
        }
        for (int i = 0; i < 100; i++) {
            arr[i] = i * 2;
        }
        // Uso
        printf("%d\n", arr[99]);
        free(arr);
        arr = NULL;
        return 0;
    }
  2. Implemente uma função que receba um ponteiro para uma string e retorne uma cópia alocada dinamicamente. A função deve tratar erros de alocação. Lembre-se de incluir o caractere nulo.

    ✓ Resposta:
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    char* duplica_string(const char *s) {
        if (s == NULL) return NULL;
        size_t len = strlen(s);
        char *copia = malloc((len + 1) * sizeof(char));
        if (copia == NULL) return NULL;
        strcpy(copia, s);
        return copia;
    }
    
    int main() {
        const char *orig = "Olá, mundo!";
        char *c = duplica_string(orig);
        if (c == NULL) {
            fprintf(stderr, "Falha na cópia\n");
            return 1;
        }
        printf("%s\n", c);
        free(c);
        return 0;
    }
  3. Crie uma estrutura Pessoa com nome (ponteiro para char) e idade. Escreva funções cria_pessoa e destroi_pessoa que gerenciem a memória corretamente, incluindo tratamento de erros. Teste no main.

    ✓ Resposta:
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    typedef struct {
        char *nome;
        int idade;
    } Pessoa;
    
    Pessoa* cria_pessoa(const char *nome, int idade) {
        if (nome == NULL) return NULL;
        Pessoa *p = malloc(sizeof(Pessoa));
        if (p == NULL) return NULL;
        p->nome = malloc(strlen(nome) + 1);
        if (p->nome == NULL) {
            free(p);
            return NULL;
        }
        strcpy(p->nome, nome);
        p->idade = idade;
        return p;
    }
    
    void destroi_pessoa(Pessoa *p) {
        if (p == NULL) return;
        free(p->nome);
        free(p);
    }
    
    int main() {
        Pessoa *p = cria_pessoa("Maria", 25);
        if (p == NULL) {
            fprintf(stderr, "Erro ao criar pessoa\n");
            return 1;
        }
        printf("%s tem %d anos\n", p->nome, p->idade);
        destroi_pessoa(p);
        return 0;
    }
  4. Escreva uma função que receba um ponteiro para um inteiro alocado dinamicamente e o libere, mas somente se o ponteiro não for NULL. Em seguida, atribua NULL ao ponteiro no chamador. Mostre como usar corretamente.

    ✓ Resposta:
    #include <stdio.h>
    #include <stdlib.h>
    
    void libera_int(int **ptr) {
        if (ptr == NULL || *ptr == NULL) return;
        free(*ptr);
        *ptr = NULL;
    }
    
    int main() {
        int *x = malloc(sizeof(int));
        if (x == NULL) return 1;
        *x = 42;
        printf("%d\n", *x);
        libera_int(&x);
        // agora x é NULL
        if (x == NULL) printf("Ponteiro liberado e nulo\n");
        return 0;
    }
  5. Explique por que o código a seguir tem um vazamento de memória e corrija-o:
    void func() {
        int *p = malloc(sizeof(int));
        *p = 10;
        // ...
        // esqueceu de free
    }

    ✓ Resposta: O código aloca memória com malloc mas nunca chama free, causando vazamento de memória. A correção é adicionar free(p); antes do retorno da função, e idealmente atribuir p = NULL; após liberar. Versão corrigida:
    void func() {
        int *p = malloc(sizeof(int));
        if (p == NULL) return;
        *p = 10;
        // ...
        free(p);
        p = NULL;
    }