Aula 38 — Lista Ligada com Implementação Dinâmica

Alocação e desalocação de memória sob demanda com malloc e free

De estática para dinâmica

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
Vantagens da implementação dinâmica: não gastamos memória que não estamos usando, e não precisamos gerenciar uma lista de elementos disponíveis — a complexidade do código diminui consideravelmente.

Ideia geral: ponteiros como endereços reais

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.

Lista dinâmica com 4 nodos — ordem lógica: 5 → 7 → 8 → 9
inicio 2010
end: 2010
chave = 5
prox → 2310
→
end: 2310
chave = 7
prox → 2180
→
end: 2180
chave = 8
prox → 2050
→
end: 2050
chave = 9
prox = NULL
✕
fim
Temos um ponteiro para o primeiro elemento (inicio = 2010)
Cada elemento indica seu sucessor via campo prox
Os endereços 2010, 2310, 2180, 2050 são arbitrários — o sistema aloca onde houver espaço livre
A ordem lógica não precisa refletir a ordem de alocação. O nodo com chave=7 está no endereço 2310 e o nodo com chave=8 está em 2180 — mesmo assim a lista está ordenada porque o campo prox de 7 aponta para 8 (endereço 2180). A ordem é definida pelos ponteiros, não pelos endereços.

Modelagem: as estruturas de dados

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;
Compare com a Aula 37:
Por que 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.

Inicialização

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 */
}
Estado após inicializarLista
inicio NULL
Nenhum nodo existe na memória heap.
A lista ocupa apenas 8 bytes (o ponteiro inicio).
Compare com a Aula 37: lá, a inicialização percorria o array inteiro (for i = 0..MAX−1) para montar a cadeia de disponíveis. Aqui, uma linha única basta.

Retornar o número de elementos

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;
}
Padrão de travessia: 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.

Exibição / Impressão

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");
}
Para a lista 5 → 7 → 8 → 9 → NULL:

p = inicio = 2010 → chave=5 → imprime 5 → p = p->prox = 2310
p = 2310 → chave=7 → imprime 7 → p = p->prox = 2180
p = 2180 → chave=8 → imprime 8 → p = p->prox = 2050
p = 2050 → chave=9 → imprime 9 → p = p->prox = NULL
p = NULL → loop encerra

Saída: Lista: " 5 7 8 9 "

Busca auxiliar: buscaSequencialExc

Tanto 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;
}
O que 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.
Buscando a chave 7 na lista 5 → 7 → 8 → 9:

*ant = NULL, atual = 2010 (chave=5) → 5 < 7 → *ant = 2010, atual = 2310
atual = 2310 (chave=7) → 7 não é < 7 → sai do while
atual != NULL && chave == 7 → retorna 2310

Resultado: retorno = 2310 (nodo 7)  |  *ant = 2010 (nodo 5, predecessor)
Buscando a chave 6 na lista 5 → 7 → 8 → 9 (elemento não existe):

*ant = NULL, atual = 2010 (chave=5) → 5 < 6 → *ant = 2010, atual = 2310
atual = 2310 (chave=7) → 7 não é < 6 → sai do while
chave 7 ≠ 6 → retorna NULL

Resultado: retorno = NULL (não existe)  |  *ant = 2010 (predecessor que seria de 6)
O predecessor é retornado mesmo quando o elemento não existe. Isso é exatamente o que a inserção precisa: saber onde encaixar o novo nodo mesmo quando ele ainda não está na lista.

Inserção de um elemento

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;
}
Atenção: após 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.

O papel do 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.

Inserindo o 6 na lista 5 → 7 → 8 → 9
buscaSequencialExc retorna: p = NULL (6 não existe) | *ant = 2010 (nodo 5)
inicio2010
ant2010
pNULL→2310
end: 2010
chave = 5
prox → 2310
→
end: 2310
chave = 7 ← p
prox → 2180
→
end: 2180
chave = 8
prox → 2050
→
end: 2050
chave = 9
prox = NULL
malloc aloca novo nodo → supondo end: 5500
Depois:
inicio2010
end: 2010
chave = 5
prox → 5500 ←
→
end: 5500 NOVO
chave = 6
prox → 2310
→
end: 2310
chave = 7
prox → 2180
→
end: 2050
chave = 9
prox = NULL
novo->prox = p (2310) — novo aponta para o antigo sucessor de 5
ant->prox = novo (5500) — nodo 5 passa a apontar para o novo
Apenas 2 ponteiros alterados. Nenhum dado movido.
A ordem das duas atribuições importa: primeiro 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.

Exclusão de um elemento

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;
}
Compare inserção e exclusão lado a lado: ambas chamam 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).
Excluindo o 7 — lista 5 → 7 → 8 → 9
Antes — buscaSequencialExc(l, 7, &ant)
inicio2010
ant2010
p2310
2010
chave = 5
prox → 2310
→
2310 ← p
chave = 7
prox → 2180
→
2180
chave = 8
prox → 2050
Depois — ant->prox = p->prox; free(p)
inicio2010
2010
chave = 5
prox → 2180 ←
→
2180
chave = 8
prox → 2050
→
2050
chave = 9
prox = NULL
ant->prox = p->prox: nodo 5 salta o nodo 7, apontando direto para 8
free(p): memória do endereço 2310 devolvida ao sistema — não existe mais no heap
Use o ponteiro antes de liberá-lo. A linha 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.

Reinicializar a lista

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;
}
Vazamento de memória (memory leak) ocorre quando um bloco alocado com malloc não é jamais liberado com free. Em programas de longa execução, vazamentos acumulam-se e eventualmente esgotam a memória disponível.
Por que salvar 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.

Comparação lado a lado: estática × dinâmica

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

Exemplo completo com todas as operações

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

Código-fonte completo

#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;
}

Resumo: o que você precisa saber desta aula

  1. A implementação dinâmica aloca cada nodo sob demanda com malloc — não há tamanho máximo fixo nem cadeia de disponíveis para gerenciar.
  2. A struct usa struct aux* prox (ponteiro para o próprio tipo) em vez de int prox (índice).
  3. O apelido PONT = ELEMENTO* simplifica as assinaturas das funções.
  4. Inicialização: l->inicio = NULL — uma única linha.
  5. Tamanho e exibição: percorrem com while (p != NULL), usando ponteiro auxiliar local PONT p.
  6. Busca: retorna PONT (ou NULL) em vez de índice.
  7. Inserção: malloc substitui obterNo; a lógica de encadeamento (ant / p) é idêntica à Aula 37.
  8. Exclusão: free(p) substitui devolverNo; salve p->prox antes de liberar.
  9. Reinicialização: percorre toda a cadeia chamando free em cada nodo — obrigatório para evitar vazamento de memória.

Exercícios

Teste sua compreensão sobre a lista ligada com implementação dinâmica:

1. Qual é a principal vantagem da implementação dinâmica sobre a estática?

2. Em inserirElemListaOrd, por que é importante fazer novo->prox = p antes de ant->prox = novo?

3. Em excluirElemLista, por que salvamos p->prox indiretamente via ant->prox = p->prox antes de chamar free(p)?

4. O que acontece se omitirmos reinicializarLista ao final do programa?

5. Qual é o papel do apelido typedef ELEMENTO* PONT?