Gerenciando estruturas dinâmicas
Esta aula aborda o gerenciamento de estruturas dinâmicas em C, focando em alocação e liberação de memória, prevenção de vazamentos, padrões de ownership e boas práticas para escrever código robusto e seguro.
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, atribuaNULLao 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
- malloc - cppreference.com
- calloc - cppreference.com
- realloc - cppreference.com
- free - cppreference.com
- GNU C Library Manual: Memory Allocation
- Valgrind Quick Start
- AddressSanitizer - Clang
- malloc(3) - Linux manual page
Exercícios
- 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; } - 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; } - Crie uma estrutura
Pessoacom nome (ponteiro para char) e idade. Escreva funçõescria_pessoaedestroi_pessoaque 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; } - 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; } - 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 commallocmas nunca chamafree, causando vazamento de memória. A correção é adicionarfree(p);antes do retorno da função, e idealmente atribuirp = NULL;após liberar. Versão corrigida:void func() { int *p = malloc(sizeof(int)); if (p == NULL) return; *p = 10; // ... free(p); p = NULL; }