Alocação e desalocação de memória sob demanda com malloc
e free
Na Aula 37 aprendemos a modelar e gerenciar listas ligadas usando a
implementação estática: um array de tamanho fixo MAX
armazenava todos os registros, e duas cadeias de cursores controlavam quais
posições estavam em uso e quais estavam disponíveis.
Hoje aprenderemos a implementação dinâmica: alocaremos e
desalocaremos a memória para cada elemento sob demanda, usando
malloc e free.
| Aspecto | Estática (Aula 37) | Dinâmica (esta aula) |
|---|---|---|
| Armazenamento | Array fixo A[MAX] — sempre na memória |
Nodo alocado individualmente com malloc |
| Tamanho máximo | Fixo em tempo de compilação (#define MAX) |
Limitado apenas pela memória disponível do sistema |
| Memória gasta | MAX registros — mesmo que a lista esteja quase vazia | Exatamente o necessário — 0 nodos extras |
| Cadeia de disponíveis | Necessária — gerenciada manualmente com dispo |
Desnecessária — o sistema operacional gerencia o heap |
| Campo de ligação | Cursor inteiro (int prox — índice) |
Ponteiro (struct aux* prox — endereço de memória) |
| Liberação de nodo | devolverNo — devolve à cadeia de disponíveis |
free(p) — devolve ao sistema operacional |
Na implementação estática, o campo prox guardava um índice
(número inteiro). Na dinâmica, ele guarda um endereço de memória real —
que é exatamente o que um ponteiro armazena.
O marcador de fim de lista deixa de ser INVALIDO = −1 e passa a ser
NULL — o endereço nulo, universalmente reconhecido em C.
inicio = 2010)proxprox de 7 aponta para 8
(endereço 2180). A ordem é definida pelos ponteiros, não pelos endereços.
A struct do nodo agora contém um ponteiro para o próprio tipo
— a struct auto-referenciada. O nome-tag aux é necessário porque
o typedef ainda não está completo quando o campo prox
é declarado.
#include <stdio.h> #include <stdlib.h> /* malloc e free */ #include <stdbool.h> #define ERRO -1 typedef int TIPOCHAVE; typedef struct { TIPOCHAVE chave; // outros campos do registro... } REGISTRO; /* Nodo: dado + ponteiro para o próximo nodo */ typedef struct aux { REGISTRO reg; struct aux* prox; /* struct auto-referenciada */ } ELEMENTO; /* Apelido de tipo: PONT é um ponteiro para ELEMENTO */ typedef ELEMENTO* PONT; /* Cabeça da lista: apenas o ponteiro para o 1º nodo */ typedef struct { PONT inicio; /* NULL = lista vazia */ } LISTA;
prox era int (índice); agora é struct aux* (endereço).LISTA tinha inicio e dispo; agora tem só inicio.PONT (ponteiro para nodo) substitui o papel dos índices.typedef ELEMENTO* PONT?
É um apelido de conveniência: em vez de escrever ELEMENTO* p
em toda função, escrevemos apenas PONT p. O significado é idêntico.
Inicializar a lista dinâmica é muito mais simples do que na versão estática:
basta apontar inicio para NULL. Não há array a
preparar, nem cadeia de disponíveis a montar.
void inicializarLista(LISTA* l) {
l->inicio = NULL; /* lista vazia — nenhum nodo alocado ainda */
}
inicializarListainicio).
for i = 0..MAX−1) para montar a cadeia de disponíveis.
Aqui, uma linha única basta.
Não há campo nroElem. Para contar os nodos, percorremos a cadeia
a partir de inicio até encontrar NULL:
int tamanho(LISTA* l) {
PONT p = l->inicio;
int tam = 0;
while (p != NULL) {
tam++;
p = p->prox; /* avança para o próximo nodo */
}
return tam;
}
PONT p = l->inicio — nunca
modifique l->inicio diretamente para percorrer a lista. O ponteiro
local p é uma cópia que avança livremente sem perder a referência
ao início.
O padrão de travessia é idêntico ao de tamanho: ponteiro auxiliar
p percorre a cadeia até NULL, imprimindo a chave de
cada nodo:
void exibirLista(LISTA* l) {
PONT p = l->inicio;
printf("Lista: \" ");
while (p != NULL) {
printf("%i ", p->reg.chave);
p = p->prox;
}
printf("\"\n");
}
p = inicio = 2010 → chave=5 → imprime 5 → p = p->prox = 2310p = 2310 → chave=7 → imprime 7 → p = p->prox = 2180p = 2180 → chave=8 → imprime 8 → p = p->prox = 2050p = 2050 → chave=9 → imprime 9 → p = p->prox = NULLp = NULL → loop encerraLista: " 5 7 8 9 "
buscaSequencialExcTanto a inserção quanto a exclusão precisam de dois endereços ao mesmo tempo:
Uma única função pode resolver as duas perguntas de uma vez.
Mas como retornar dois endereços diferentes? Em C, uma função tem
apenas um valor de retorno. A solução é passar um ponteiro para
ponteiro (PONT* ant) como parâmetro de saída:
a função escreve o predecessor em *ant e retorna o elemento
encontrado (ou NULL).
/* Busca auxiliar: retorna o nodo com chave ch (ou NULL se não existir)
e escreve em *ant o endereço do predecessor (NULL se for o 1º).
Usada como base de inserção e exclusão. */
PONT buscaSequencialExc(LISTA* l, TIPOCHAVE ch, PONT* ant) {
*ant = NULL;
PONT atual = l->inicio;
while ((atual != NULL) && (atual->reg.chave < ch)) {
*ant = atual;
atual = atual->prox;
}
if ((atual != NULL) && (atual->reg.chave == ch))
return atual;
return NULL;
}
PONT* ant significa:
PONT é ELEMENTO*, então PONT* é
ELEMENTO** — um ponteiro para um ponteiro para nodo.
Dentro da função, *ant = atual escreve o endereço do predecessor
diretamente na variável do chamador. Ao sair da função, o chamador lê
o predecessor em sua própria variável local.
*ant = NULL, atual = 2010 (chave=5) → 5 < 7 → *ant = 2010, atual = 2310atual = 2310 (chave=7) → 7 não é < 7 → sai do whileatual != NULL && chave == 7 → retorna 23102310 (nodo 7) | *ant = 2010 (nodo 5, predecessor)
*ant = NULL, atual = 2010 (chave=5) → 5 < 6 → *ant = 2010, atual = 2310atual = 2310 (chave=7) → 7 não é < 6 → sai do whilechave 7 ≠ 6 → retorna NULLNULL (não existe) | *ant = 2010 (predecessor que seria de 6)
Com buscaSequencialExc disponível, a inserção fica limpa:
chamamos a auxiliar para obter simultaneamente se o elemento já existe
(duplicata) e quem seria seu predecessor. Depois alocamos o nodo e
ajustamos dois ponteiros.
bool inserirElemListaOrd(LISTA* l, REGISTRO reg) {
PONT ant;
PONT p = buscaSequencialExc(l, reg.chave, &ant);
/* duplicata: rejeita */
if (p != NULL) return false;
/* ── aloca o novo nodo ── */
PONT novo = (PONT) malloc(sizeof(ELEMENTO));
if (novo == NULL) return false; /* sem memória disponível */
novo->reg = reg;
novo->prox = p; /* p == NULL aqui, mas aponta para quem virá depois */
/* ── encadeia o novo nodo ── */
if (ant == NULL)
l->inicio = novo; /* inserção no início (lista vazia ou menor elemento) */
else
ant->prox = novo; /* inserção no meio ou no final */
return true;
}
buscaSequencialExc retornar
NULL (elemento não existe), o ponteiro p aponta
para o primeiro nodo cuja chave é maior que a nova — ou é
NULL se o novo vai para o final. Por isso novo->prox = p
encaixa o novo nodo corretamente antes do seu sucessor em qualquer caso.
malloc
malloc(sizeof(ELEMENTO)) solicita ao sistema operacional exatamente
o número de bytes necessários para um ELEMENTO e retorna o endereço
do bloco alocado. Se não houver memória disponível, retorna NULL —
sempre verifique esse retorno antes de usar o ponteiro.
novo->prox = p
(o novo guarda para onde vai), depois ant->prox = novo (o antecessor
aponta para o novo). Se invertidas, ant->prox seria sobrescrito antes de
ser copiado para novo->prox, e perderíamos o restante da lista.
Assim como na inserção, a exclusão usa buscaSequencialExc
para obter simultaneamente o nodo a remover e seu predecessor.
Se o nodo existir, desligamos da cadeia e chamamos free:
bool excluirElemLista(LISTA* l, TIPOCHAVE ch) {
PONT ant;
PONT p = buscaSequencialExc(l, ch, &ant);
/* não encontrou */
if (p == NULL) return false;
/* desliga o nodo da cadeia */
if (ant == NULL)
l->inicio = p->prox; /* era o primeiro elemento */
else
ant->prox = p->prox; /* meio ou final */
free(p); /* devolve a memória ao sistema operacional */
return true;
}
buscaSequencialExc e depois verificam ant == NULL
para decidir se atualizam l->inicio ou ant->prox.
A diferença é que a inserção adiciona um nodo (com malloc)
e a exclusão remove (com free).
ant->prox = p->prox
deve vir antes de free(p). Após o free, o conteúdo do
bloco liberado é indefinido — acessar p->prox depois seria comportamento
indefinido.
Na versão estática, reinicializar era simples: bastava ajustar inicio
e dispo porque a memória do array pertencia à struct e seria
liberada quando o escopo terminasse.
Na versão dinâmica, cada nodo foi alocado individualmente com malloc
e precisa ser liberado individualmente com free — caso contrário,
ocorre vazamento de memória (memory leak):
void reinicializarLista(LISTA* l) {
PONT p = l->inicio;
while (p != NULL) {
PONT aux = p->prox; /* salva o próximo ANTES de liberar p */
free(p);
p = aux; /* avança com o ponteiro salvo */
}
l->inicio = NULL;
}
malloc não é jamais liberado com free. Em programas
de longa execução, vazamentos acumulam-se e eventualmente esgotam a memória disponível.
p->prox antes do free?
Após free(p), a memória apontada por p é liberada
e seu conteúdo torna-se indefinido. Se tentarmos ler p->prox
depois do free, o comportamento é indefinido — o programa
pode travar ou produzir resultados incorretos.
As operações são estruturalmente idênticas — o que muda é apenas o mecanismo de gerenciamento de memória:
| Operação | Estática (Aula 37) | Dinâmica (esta aula) |
|---|---|---|
| Inicializar | for monta cadeia de disponíveis; inicio=INVALIDO; dispo=0 |
l->inicio = NULL |
| Reservar nodo | obterNo(l) — retira índice da cadeia de disponíveis |
malloc(sizeof(ELEMENTO)) — aloca no heap |
| Liberar nodo | devolverNo(l, i) — devolve índice à cadeia de disponíveis |
free(p) — devolve memória ao sistema |
| Lista vazia? | l->inicio == INVALIDO |
l->inicio == NULL |
| Avançar na cadeia | i = l->A[i].prox |
p = p->prox |
| Reinicializar | inicio=INVALIDO; dispo=0 (memória não é liberada) |
Loop com free em cada nodo, depois inicio=NULL |
int main() {
LISTA l;
REGISTRO r;
inicializarLista(&l);
exibirLista(&l);
printf("Tamanho: %d\n\n", tamanho(&l));
/* Inserções fora de ordem — a lista se auto-ordena */
r.chave = 7; inserirElemListaOrd(&l, r);
r.chave = 3; inserirElemListaOrd(&l, r);
r.chave = 9; inserirElemListaOrd(&l, r);
r.chave = 1; inserirElemListaOrd(&l, r);
r.chave = 5; inserirElemListaOrd(&l, r);
exibirLista(&l);
printf("Tamanho: %d\n\n", tamanho(&l));
/* Inserção de duplicata */
r.chave = 3;
if (!inserirElemListaOrd(&l, r))
printf("Inserção de 3 recusada: elemento já existe\n\n");
/* Busca */
PONT ant_busca;
PONT pos = buscaSequencialExc(&l, 9, &ant_busca);
if (pos != NULL)
printf("Chave 9 encontrada (endereço %p)\n", (void*)pos);
pos = buscaSequencialExc(&l, 4, &ant_busca);
if (pos == NULL)
printf("Chave 4 não encontrada\n\n");
/* Exclusões */
if (excluirElemLista(&l, 3)) printf("Chave 3 excluída\n");
if (excluirElemLista(&l, 1)) printf("Chave 1 excluída\n");
if (!excluirElemLista(&l, 8)) printf("Chave 8 não encontrada\n\n");
exibirLista(&l);
printf("Tamanho: %d\n\n", tamanho(&l));
/* Libera toda a memória antes de encerrar */
reinicializarLista(&l);
exibirLista(&l);
printf("Tamanho: %d\n", tamanho(&l));
return 0;
}
Lista: " "
Tamanho: 0
Lista: " 1 3 5 7 9 "
Tamanho: 5
Inserção de 3 recusada: elemento já existe
Chave 9 encontrada (endereço 0x...)
Chave 4 não encontrada
Chave 3 excluída
Chave 1 excluída
Chave 8 não encontrada
Lista: " 5 7 9 "
Tamanho: 3
Lista: " "
Tamanho: 0
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#define ERRO -1
typedef int TIPOCHAVE;
typedef struct { TIPOCHAVE chave; } REGISTRO;
typedef struct aux { REGISTRO reg; struct aux* prox; } ELEMENTO;
typedef ELEMENTO* PONT;
typedef struct { PONT inicio; } LISTA;
/* ── Inicialização ── */
void inicializarLista(LISTA* l) { l->inicio = NULL; }
/* ── Tamanho ── */
int tamanho(LISTA* l) {
PONT p = l->inicio; int tam = 0;
while (p != NULL) { tam++; p = p->prox; }
return tam;
}
/* ── Exibição ── */
void exibirLista(LISTA* l) {
PONT p = l->inicio;
printf("Lista: \" ");
while (p != NULL) { printf("%i ", p->reg.chave); p = p->prox; }
printf("\"\n");
}
/* ── Busca auxiliar ── */
PONT buscaSequencialExc(LISTA* l, TIPOCHAVE ch, PONT* ant) {
*ant = NULL;
PONT atual = l->inicio;
while ((atual != NULL) && (atual->reg.chave < ch)) { *ant = atual; atual = atual->prox; }
if ((atual != NULL) && (atual->reg.chave == ch)) return atual;
return NULL;
}
/* ── Inserção ── */
bool inserirElemListaOrd(LISTA* l, REGISTRO reg) {
PONT ant;
PONT p = buscaSequencialExc(l, reg.chave, &ant);
if (p != NULL) return false;
PONT novo = (PONT) malloc(sizeof(ELEMENTO));
if (novo == NULL) return false;
novo->reg = reg; novo->prox = p;
if (ant == NULL) l->inicio = novo;
else ant->prox = novo;
return true;
}
/* ── Exclusão ── */
bool excluirElemLista(LISTA* l, TIPOCHAVE ch) {
PONT ant;
PONT p = buscaSequencialExc(l, ch, &ant);
if (p == NULL) return false;
if (ant == NULL) l->inicio = p->prox;
else ant->prox = p->prox;
free(p);
return true;
}
/* ── Reinicialização ── */
void reinicializarLista(LISTA* l) {
PONT p = l->inicio;
while (p != NULL) { PONT aux = p->prox; free(p); p = aux; }
l->inicio = NULL;
}
malloc
— não há tamanho máximo fixo nem cadeia de disponíveis para gerenciar.struct aux* prox (ponteiro para o próprio tipo)
em vez de int prox (índice).PONT = ELEMENTO* simplifica as assinaturas das funções.l->inicio = NULL — uma única linha.while (p != NULL),
usando ponteiro auxiliar local PONT p.PONT (ou NULL) em vez de índice.malloc substitui obterNo;
a lógica de encadeamento (ant / p) é idêntica à Aula 37.free(p) substitui devolverNo;
salve p->prox antes de liberar.free
em cada nodo — obrigatório para evitar vazamento de memória.Teste sua compreensão sobre a lista ligada com implementação dinâmica: