C Listas Ligadas

Um array guarda os valores colados na memória, um do lado do outro. Isso é ótimo para ler, mas ruim para crescer e para tirar coisas do meio. A lista ligada faz diferente: cada valor mora numa caixinha separada, criada com malloc, e cada caixinha guarda o endereço da próxima. Nesta aula você vai montar uma lista ligada do zero, inserir, percorrer, buscar, remover e devolver toda a memória, conferindo com o valgrind que nada vazou.

Você vai aprender
  • O que é um nó e por que ele tem um campo struct No *prox.
  • Como percorrer a lista com um ponteiro que "anda" de nó em nó até o NULL.
  • Como inserir no início (rápido) e no fim (precisa andar até o último).
  • Como buscar um valor e como remover um nó sem perder o resto da lista.
  • Como liberar a lista inteira e conferir com o valgrind.
  • Quando usar lista ligada e quando usar array.

Esta aula usa structs, a seta -> e malloc e free. Todos os programas foram compilados com o gcc 15.2 na máquina fictícia da Ana (usuária ana, computador notebook), na pasta ~/c, com o comando padrão do guia: gcc -std=c23 -Wall -Wextra arquivo.c -o programa. Quando aparecer -g, é para o valgrind mostrar o arquivo e a linha (veja Valgrind).


A ideia: a caça ao tesouro

Pense numa caça ao tesouro. Você recebe o primeiro bilhete. Ele diz "a primeira pista é 10; o próximo bilhete está embaixo do vaso". No vaso, outro bilhete: "a pista é 20; o próximo está na gaveta". Na gaveta: "a pista é 30; acabou".

Os bilhetes estão espalhados pela casa, mas você nunca se perde, porque cada um diz onde está o próximo. Para chegar ao terceiro, você precisa passar pelo primeiro e pelo segundo. E para pôr um bilhete novo no começo, basta escrever nele "o próximo está onde estava o primeiro".

Uma lista ligada é isso. Compare com o array, que é uma fileira de armários colados, numerados:

Um array: três valores colados na memória, numerados de 0 a 2 array100201302 Uma lista ligada: três nós separados; cada um aponta para o próximo e o último aponta para NULL 10lista2030NULL
Em cima, o array: os três valores colados, e você chega a qualquer um pelo número (array[2]). Embaixo, a lista ligada: cada caixa tem duas partes, o valor e uma seta para a próxima caixa. A última aponta para NULL ("nada"), que é o "acabou" do bilhete. A variável lista guarda só o endereço da primeira caixa.
Na caça ao tesouroNa lista ligada
Um bilheteUm nó (uma struct criada com malloc)
A pista escrita no bilheteO campo valor
"O próximo está na gaveta"O campo prox: o endereço do próximo nó
"Acabou"prox valendo NULL
O primeiro bilhete, que você recebe na mãoA variável lista (também chamada de cabeça ou head)

1. O nó: uma struct que aponta para outra igual

Cada caixinha da lista é um nó (em inglês, node). Em C, um nó é uma struct com o valor e um ponteiro para o próximo nó:

struct No {
    int valor;
    struct No *prox;
};
struct No
O nome do tipo do nó. "No" é "nó" sem o acento, porque nomes em C não aceitam acentos (veja Nomes de Variáveis).
int valor;
O dado que a lista guarda. Aqui é um int, mas poderia ser um preço, um nome, uma struct inteira.
struct No *prox;
"Próximo". É um ponteiro para outro struct No: guarda o endereço do nó seguinte, ou NULL se este é o último.

Repare na esquisitice: dentro de struct No aparece struct No de novo. Isso se chama struct autorreferente ("que se refere a si mesma"). Funciona porque o campo é um ponteiro: um endereço tem sempre o mesmo tamanho (8 bytes no gcc do laboratório), não importa para que tipo ele aponta. O C não precisa saber o tamanho de struct No para reservar 8 bytes para um endereço.

Sem a estrela, não dá: um nó que contém um nó inteiro, que contém outro nó inteiro… teria tamanho infinito. O gcc recusa:

struct No {
    int valor;
    struct No prox;
};

int main(void) {
    return 0;
}
$ gcc -std=c23 -Wall -Wextra autoref.c -o autoref
autoref.c:3:15: error: field ‘prox’ has incomplete type
    3 |     struct No prox;
      |               ^~~~

Field ‘prox’ has incomplete type = "o campo ‘prox’ tem tipo incompleto". Na linha 3, o struct No ainda está sendo definido (o } não chegou), então o compilador não sabe o tamanho dele. Conserto: struct No *prox;, com a estrela.


2. Três nós ligados à mão e o primeiro percurso

Antes de usar malloc, vamos montar três nós como variáveis comuns e ligá-los com o operador & ("endereço de"). Isso mostra a ideia sem nenhuma outra novidade:

#include <stdio.h>

struct No {
    int valor;
    struct No *prox;
};

int main(void) {
    struct No c = {30, NULL};
    struct No b = {20, &c};
    struct No a = {10, &b};

    struct No *p = &a;
    while (p != NULL) {
        printf("%d\n", p->valor);
        p = p->prox;
    }
    return 0;
}
$ gcc -std=c23 -Wall -Wextra tres.c -o tres
$ ./tres
10
20
30
LinhaO que faz
struct No c = {30, NULL};O último nó: valor 30, e o prox é NULL ("acabou").
struct No b = {20, &c};Valor 20, e o prox guarda o endereço de c. Por isso c foi criado antes.
struct No a = {10, &b};Valor 10, apontando para b. a é o começo da lista.
struct No *p = &a;Um ponteiro que começa no primeiro nó. É o nosso "dedo" que vai andar pela lista.
while (p != NULL)Enquanto o dedo estiver em cima de algum nó…
p->valor…mostra o valor do nó em que ele está…
p = p->prox;…e anda: o dedo passa para o endereço guardado no prox. Depois do 30, o prox é NULL, e o laço termina.
O ponteiro p começa no primeiro nó 10p2030NULL Depois de p = p->prox, p está no segundo nó 1020p30NULL
A linha p = p->prox; faz o ponteiro p andar uma casa: de 10 para 20, depois para 30 e, por fim, para NULL. Os nós não se mexem; quem anda é o p.
Guarde este laço

for (p = lista; p != NULL; p = p->prox) é a forma de percorrer qualquer lista ligada. Você vai vê-lo em quase todas as funções desta aula. Ele é o equivalente do for (i = 0; i < n; i++) dos arrays.


3. Nós de verdade: criados com malloc

Nós em variáveis comuns somem quando a função termina, e você precisa saber de antemão quantos são. Numa lista de verdade, cada nó é criado com malloc ("memory allocation", reservar memória) na hora em que é preciso. Vamos escrever uma função que cria um nó e outra que o põe no início da lista:

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

struct No {
    int valor;
    struct No *prox;
};

struct No *novo_no(int valor) {
    struct No *n = malloc(sizeof *n);
    if (n == NULL) {
        fprintf(stderr, "Sem memória\n");
        exit(1);
    }
    n->valor = valor;
    n->prox = NULL;
    return n;
}

struct No *inserir_inicio(struct No *lista, int valor) {
    struct No *n = novo_no(valor);
    n->prox = lista;
    return n;
}

void imprimir(const struct No *lista) {
    for (const struct No *p = lista; p != NULL; p = p->prox) {
        printf("%d -> ", p->valor);
    }
    printf("NULL\n");
}

int main(void) {
    struct No *lista = NULL;
    imprimir(lista);
    lista = inserir_inicio(lista, 30);
    imprimir(lista);
    lista = inserir_inicio(lista, 20);
    imprimir(lista);
    lista = inserir_inicio(lista, 10);
    imprimir(lista);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra inicio.c -o inicio
$ ./inicio
NULL
30 -> NULL
20 -> 30 -> NULL
10 -> 20 -> 30 -> NULL
struct No *lista = NULL;
A lista começa vazia. Lista vazia = ponteiro NULL. Nunca deixe esse ponteiro sem valor inicial.
malloc(sizeof *n)
Reserva espaço para um nó. sizeof *n é "o tamanho daquilo para que n aponta"; se um dia o tipo mudar, a conta continua certa (o mesmo que sizeof(struct No)).
if (n == NULL)
O malloc devolve NULL quando não há memória. Confira sempre. Aqui o programa avisa no stderr e termina com exit(1) (veja exit).
n->prox = NULL;
Todo nó nasce "sozinho", sem próximo. Assim nenhum campo fica com lixo.
n->prox = lista;
O nó novo aponta para quem era o primeiro.
return n;
O nó novo passa a ser o primeiro. Quem chama precisa guardar esse retorno: lista = inserir_inicio(lista, 30);.
const struct No *
Em imprimir, o const promete que a função só lê os nós (veja const e Ponteiros).

Inserir no início, passo a passo

Veja o que acontece na última chamada, lista = inserir_inicio(lista, 10);, quando a lista já tem 20 e 30:

  1. novo_no(10) cria o nó 10, sozinho, com prox em NULL. Passo 1: o nó 10 foi criado, separado da lista 20lista30NULL10nNULL
  2. n->prox = lista; faz o nó 10 apontar para o nó 20, o primeiro de agora. Passo 2: o nó 10 aponta para o 20 20lista30NULL10n
  3. return n; e lista = …: a variável lista passa a apontar para o nó 10. Passo 3: lista aponta para o 10, que aponta para o 20 10lista2030NULL

Nada foi copiado nem empurrado: só duas setas mudaram. Por isso inserir no início leva o mesmo tempo com 3 ou com 3 milhões de nós.

A ordem importa

Se você fizer lista = n; antes de n->prox = lista;, o n->prox vai apontar para ele mesmo, e o resto da lista se perde para sempre (vazamento). Regra: primeiro ligue o nó novo, depois mude a cabeça.

Percorrer para contar, somar, imprimir

A função imprimir usa o laço que você guardou, na forma de for. Qualquer pergunta sobre "todos os nós" segue o mesmo molde: quantos são, qual a soma, qual o maior. Pratique:

Experimente: a função contar sempre devolve 0. Escreva dentro dela um laço que percorre a lista e soma 1 em total para cada nó, até o programa mostrar Tamanho: 3. Apague a linha (void)lista; quando usar o parâmetro.

Tamanho: 3
#include <stdio.h>
#include <stdlib.h>

struct No {
    int valor;
    struct No *prox;
};

struct No *inserir_inicio(struct No *lista, int valor) {
    struct No *n = malloc(sizeof *n);
    if (n == NULL) {
        exit(1);
    }
    n->valor = valor;
    n->prox = lista;
    return n;
}

int contar(const struct No *lista) {
    int total = 0;
    // percorra a lista e some 1 para cada nó
    (void)lista;  // apague esta linha: ela só cala o aviso "unused parameter"
    return total;
}

void liberar(struct No *lista) {
    while (lista != NULL) {
        struct No *prox = lista->prox;
        free(lista);
        lista = prox;
    }
}

int main(void) {
    struct No *lista = NULL;
    lista = inserir_inicio(lista, 7);
    lista = inserir_inicio(lista, 5);
    lista = inserir_inicio(lista, 9);
    printf("Tamanho: %d\n", contar(lista));
    liberar(lista);
    return 0;
}
#include <stdio.h>
#include <stdlib.h>

struct No {
    int valor;
    struct No *prox;
};

struct No *inserir_inicio(struct No *lista, int valor) {
    struct No *n = malloc(sizeof *n);
    if (n == NULL) {
        exit(1);
    }
    n->valor = valor;
    n->prox = lista;
    return n;
}

int contar(const struct No *lista) {
    int total = 0;
    for (const struct No *p = lista; p != NULL; p = p->prox) {
        total++;
    }
    return total;
}

void liberar(struct No *lista) {
    while (lista != NULL) {
        struct No *prox = lista->prox;
        free(lista);
        lista = prox;
    }
}

int main(void) {
    struct No *lista = NULL;
    lista = inserir_inicio(lista, 7);
    lista = inserir_inicio(lista, 5);
    lista = inserir_inicio(lista, 9);
    printf("Tamanho: %d\n", contar(lista));
    liberar(lista);
    return 0;
}

4. Inserir no fim

Para pôr um valor no fim, a lista precisa ser percorrida até o último nó, aquele cujo prox é NULL. Aí o prox do último passa a apontar para o nó novo:

struct No *inserir_fim(struct No *lista, int valor) {
    struct No *n = novo_no(valor);
    if (lista == NULL) {
        return n;
    }
    struct No *p = lista;
    while (p->prox != NULL) {
        p = p->prox;
    }
    p->prox = n;
    return lista;
}
LinhaO que faz
if (lista == NULL) return n;Lista vazia: o nó novo é o primeiro e o último. Sem este if, a linha p->prox abaixo leria um ponteiro NULL e o programa morreria (veja Quando dá errado).
while (p->prox != NULL)Repare: aqui o teste é p->prox, e não p. O laço para em cima do último nó, e não depois dele.
p->prox = n;O último passa a apontar para o novo. O prox do novo já é NULL (veio assim do novo_no).
return lista;O primeiro nó não mudou, mas a função devolve a cabeça mesmo assim, para ser usada sempre do mesmo jeito: lista = inserir_fim(lista, 40);.
Inserir 40 no fim: p parou no último nó, o 30; o nó 40 foi criado 10lista2030pNULL40nNULL Depois de p->prox = n, o 30 aponta para o 40 10lista2030p40nNULL
Em cima, o while já levou p até o último nó (o 30) e o nó 40 acabou de nascer. Embaixo, depois de p->prox = n;: uma seta só mudou.
Início é rápido; fim é lento (nesta versão)

Inserir no início não depende do tamanho da lista. Inserir no fim precisa visitar todos os nós: com 1 milhão de nós, são 1 milhão de passos a cada inserção. A seção Formas diferentes mostra o conserto: guardar também um ponteiro para o último nó.


5. Buscar um valor

Buscar é percorrer e parar quando achar. A função devolve o endereço do nó encontrado, ou NULL se o valor não está na lista:

struct No *buscar(struct No *lista, int valor) {
    for (struct No *p = lista; p != NULL; p = p->prox) {
        if (p->valor == valor) {
            return p;
        }
    }
    return NULL;
}

Devolver o nó (e não só "sim" ou "não") deixa quem chamou mudar o valor achado: struct No *x = buscar(lista, 30); if (x != NULL) x->valor = 35;. Sempre confira o NULL antes de usar a seta. Numa lista, buscar é sempre visitar nó por nó, do começo: não existe atalho como array[i]. Em listas grandes, isso pesa (a aula Busca compara as formas de buscar).


6. Remover um nó

Remover tem duas partes: religar a lista por cima do nó e depois liberar o nó com free. Para religar, você precisa do nó anterior, porque é o prox dele que muda. Por isso a função anda com dois ponteiros: ant (anterior) e p (atual):

struct No *remover(struct No *lista, int valor) {
    struct No *ant = NULL;
    struct No *p = lista;
    while (p != NULL && p->valor != valor) {
        ant = p;
        p = p->prox;
    }
    if (p == NULL) {
        return lista;
    }
    if (ant == NULL) {
        lista = p->prox;
    } else {
        ant->prox = p->prox;
    }
    free(p);
    return lista;
}
while (p != NULL && p->valor != valor)
Anda até achar o valor ou até a lista acabar. A ordem do && importa: se p for NULL, o C nem olha p->valor (veja Operadores Lógicos).
ant = p; p = p->prox;
Os dois andam juntos: ant fica sempre uma casa atrás de p.
if (p == NULL) return lista;
Não achou: devolve a lista do jeito que estava.
if (ant == NULL) lista = p->prox;
Achou no primeiro nó (não há anterior): a cabeça passa a ser o segundo.
ant->prox = p->prox;
Achou no meio ou no fim: o anterior "pula" o nó removido.
free(p);
Só agora, com a lista já religada, o nó é devolvido. Depois disso, p não pode mais ser usado.
Remover 20: ant está no 10 e p no 20 10ant20p30NULL Depois de ant->prox = p->prox: o 10 pula o 20 e aponta para o 30 10ant20p30NULL
Em cima, o laço parou: p no nó que sai, ant no anterior. Embaixo, a seta do 10 passou por cima do 20 e foi direto ao 30 (em azul). O nó 20, tracejado, ficou fora da lista, e o free(p) o devolve.

O programa completo

Juntando tudo (com a função liberar, explicada logo abaixo):

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

struct No {
    int valor;
    struct No *prox;
};

struct No *novo_no(int valor) {
    struct No *n = malloc(sizeof *n);
    if (n == NULL) {
        fprintf(stderr, "Sem memória\n");
        exit(1);
    }
    n->valor = valor;
    n->prox = NULL;
    return n;
}

struct No *inserir_inicio(struct No *lista, int valor) {
    struct No *n = novo_no(valor);
    n->prox = lista;
    return n;
}

struct No *inserir_fim(struct No *lista, int valor) {
    struct No *n = novo_no(valor);
    if (lista == NULL) {
        return n;
    }
    struct No *p = lista;
    while (p->prox != NULL) {
        p = p->prox;
    }
    p->prox = n;
    return lista;
}

void imprimir(const struct No *lista) {
    for (const struct No *p = lista; p != NULL; p = p->prox) {
        printf("%d -> ", p->valor);
    }
    printf("NULL\n");
}

struct No *buscar(struct No *lista, int valor) {
    for (struct No *p = lista; p != NULL; p = p->prox) {
        if (p->valor == valor) {
            return p;
        }
    }
    return NULL;
}

struct No *remover(struct No *lista, int valor) {
    struct No *ant = NULL;
    struct No *p = lista;
    while (p != NULL && p->valor != valor) {
        ant = p;
        p = p->prox;
    }
    if (p == NULL) {
        return lista;
    }
    if (ant == NULL) {
        lista = p->prox;
    } else {
        ant->prox = p->prox;
    }
    free(p);
    return lista;
}

void liberar(struct No *lista) {
    while (lista != NULL) {
        struct No *prox = lista->prox;
        free(lista);
        lista = prox;
    }
}

int main(void) {
    struct No *lista = NULL;
    lista = inserir_fim(lista, 20);
    lista = inserir_fim(lista, 30);
    lista = inserir_inicio(lista, 10);
    lista = inserir_fim(lista, 40);
    imprimir(lista);

    struct No *achou = buscar(lista, 30);
    printf("buscar 30: %s\n", achou != NULL ? "achou" : "não achou");
    printf("buscar 99: %s\n", buscar(lista, 99) != NULL ? "achou" : "não achou");

    lista = remover(lista, 30);
    imprimir(lista);
    lista = remover(lista, 10);
    imprimir(lista);
    lista = remover(lista, 99);
    imprimir(lista);

    liberar(lista);
    lista = NULL;
    return 0;
}
$ gcc -std=c23 -Wall -Wextra -g lista.c -o lista
$ ./lista
10 -> 20 -> 30 -> 40 -> NULL
buscar 30: achou
buscar 99: não achou
10 -> 20 -> 40 -> NULL
20 -> 40 -> NULL
20 -> 40 -> NULL

Linha por linha da saída: os três inserir_fim e o inserir_inicio montaram 10 20 30 40; a busca achou o 30 e não achou o 99; remover(lista, 30) tirou um nó do meio; remover(lista, 10) tirou o primeiro (e a cabeça mudou para o 20); remover(lista, 99) não achou nada e devolveu a lista igual.

Experimente: este é o programa completo. Remova o último nó (o 40) e confira que o 20 passa a apontar para NULL. Tente remover de uma lista vazia. Use buscar para trocar o valor 20 por 25.

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

struct No {
    int valor;
    struct No *prox;
};

struct No *novo_no(int valor) {
    struct No *n = malloc(sizeof *n);
    if (n == NULL) {
        fprintf(stderr, "Sem memória\n");
        exit(1);
    }
    n->valor = valor;
    n->prox = NULL;
    return n;
}

struct No *inserir_inicio(struct No *lista, int valor) {
    struct No *n = novo_no(valor);
    n->prox = lista;
    return n;
}

struct No *inserir_fim(struct No *lista, int valor) {
    struct No *n = novo_no(valor);
    if (lista == NULL) {
        return n;
    }
    struct No *p = lista;
    while (p->prox != NULL) {
        p = p->prox;
    }
    p->prox = n;
    return lista;
}

void imprimir(const struct No *lista) {
    for (const struct No *p = lista; p != NULL; p = p->prox) {
        printf("%d -> ", p->valor);
    }
    printf("NULL\n");
}

struct No *buscar(struct No *lista, int valor) {
    for (struct No *p = lista; p != NULL; p = p->prox) {
        if (p->valor == valor) {
            return p;
        }
    }
    return NULL;
}

struct No *remover(struct No *lista, int valor) {
    struct No *ant = NULL;
    struct No *p = lista;
    while (p != NULL && p->valor != valor) {
        ant = p;
        p = p->prox;
    }
    if (p == NULL) {
        return lista;
    }
    if (ant == NULL) {
        lista = p->prox;
    } else {
        ant->prox = p->prox;
    }
    free(p);
    return lista;
}

void liberar(struct No *lista) {
    while (lista != NULL) {
        struct No *prox = lista->prox;
        free(lista);
        lista = prox;
    }
}

int main(void) {
    struct No *lista = NULL;
    lista = inserir_fim(lista, 20);
    lista = inserir_fim(lista, 30);
    lista = inserir_inicio(lista, 10);
    lista = inserir_fim(lista, 40);
    imprimir(lista);

    struct No *achou = buscar(lista, 30);
    printf("buscar 30: %s\n", achou != NULL ? "achou" : "não achou");
    printf("buscar 99: %s\n", buscar(lista, 99) != NULL ? "achou" : "não achou");

    lista = remover(lista, 30);
    imprimir(lista);
    lista = remover(lista, 10);
    imprimir(lista);
    lista = remover(lista, 99);
    imprimir(lista);

    liberar(lista);
    lista = NULL;
    return 0;
}

7. Liberar a lista inteira (e o valgrind)

Cada nó veio de um malloc, então cada nó precisa do seu free. Esquecer não dá erro nenhum na hora: o programa só vai segurando memória. É o vazamento de memória. O programa inicio.c lá de cima não liberava nada. O valgrind mostra:

$ gcc -std=c23 -Wall -Wextra -g inicio.c -o inicio
$ valgrind --leak-check=full ./inicio
==32493== Memcheck, a memory error detector
==32493== Copyright (C) 2002-2024, and GNU GPL'd, by Julian Seward et al.
==32493== Using Valgrind-3.26.0 and LibVEX; rerun with -h for copyright info
==32493== Command: ./inicio
==32493==
NULL
30 -> NULL
20 -> 30 -> NULL
10 -> 20 -> 30 -> NULL
==32493==
==32493== HEAP SUMMARY:
==32493==     in use at exit: 48 bytes in 3 blocks
==32493==   total heap usage: 4 allocs, 1 frees, 4,144 bytes allocated
==32493==
==32493== 48 (16 direct, 32 indirect) bytes in 1 blocks are definitely lost in loss record 3 of 3
==32493==    at 0x4850858: malloc (vg_replace_malloc.c:447)
==32493==    by 0x40011E1: novo_no (inicio.c:10)
==32493==    by 0x400124E: inserir_inicio (inicio.c:21)
==32493==    by 0x4001332: main (inicio.c:40)
==32493==
==32493== LEAK SUMMARY:
==32493==    definitely lost: 16 bytes in 1 blocks
==32493==    indirectly lost: 32 bytes in 2 blocks
==32493==      possibly lost: 0 bytes in 0 blocks
==32493==    still reachable: 0 bytes in 0 blocks
==32493==         suppressed: 0 bytes in 0 blocks
==32493==
==32493== For lists of detected and suppressed errors, rerun with: -s
==32493== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
  • in use at exit: 48 bytes in 3 blocks: no fim, 3 blocos (os 3 nós) ainda estavam reservados. Cada nó tem 16 bytes no laboratório: 4 do int, 4 de "enchimento" para alinhar e 8 do ponteiro (veja sizeof).
  • 4 allocs, 1 frees: os 3 nós e mais um bloco que o próprio printf reservou para a saída (e devolveu sozinho no fim).
  • definitely lost: 16 bytes in 1 blocks: o nó 10, o primeiro. Ninguém mais aponta para ele: está "definitivamente perdido". A linha main (inicio.c:40) mostra onde ele nasceu.
  • indirectly lost: 32 bytes in 2 blocks: o 20 e o 30. Só eram alcançáveis pelo nó 10; perdido o 10, eles se perderam "indiretamente".

Para liberar, ande pela lista e dê free em cada nó. O detalhe que importa: guarde o próximo antes de liberar o atual.

void liberar(struct No *lista) {
    while (lista != NULL) {
        struct No *prox = lista->prox;
        free(lista);
        lista = prox;
    }
}
Liberar: prox já guarda o endereço do 20 antes do free no 10 10lista?20prox30NULL
Depois de free(lista), o nó 10 (cinza) não existe mais e o campo prox dele não pode ser lido. Mas o endereço do 20 já estava salvo na variável prox. A linha lista = prox; anda para o 20, e o laço repete.

Com o liberar no fim do main, o programa completo lista.c fica limpo:

$ valgrind --leak-check=full ./lista
==28184== Memcheck, a memory error detector
==28184== Copyright (C) 2002-2024, and GNU GPL'd, by Julian Seward et al.
==28184== Using Valgrind-3.26.0 and LibVEX; rerun with -h for copyright info
==28184== Command: ./lista
==28184==
10 -> 20 -> 30 -> 40 -> NULL
buscar 30: achou
buscar 99: não achou
10 -> 20 -> 40 -> NULL
20 -> 40 -> NULL
20 -> 40 -> NULL
==28184==
==28184== HEAP SUMMARY:
==28184==     in use at exit: 0 bytes in 0 blocks
==28184==   total heap usage: 5 allocs, 5 frees, 4,160 bytes allocated
==28184==
==28184== All heap blocks were freed -- no leaks are possible
==28184==
==28184== For lists of detected and suppressed errors, rerun with: -s
==28184== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

All heap blocks were freed -- no leaks are possible = "todos os blocos foram liberados: não há vazamento possível". São 5 malloc (4 nós + o do printf) e 5 free (2 do remover, 2 do liberar, 1 do printf). ERROR SUMMARY: 0 errors: nenhum acesso errado à memória.

Depois de liberar, zere a cabeça

O main faz liberar(lista); lista = NULL;. Assim, se alguém usar a lista depois, ela está vazia (e não apontando para memória devolvida). A versão com No **, logo abaixo, faz isso sozinha.

No editor do guia

O editor não tem valgrind, e lá cada nó tem 8 bytes, e não 16 (o editor é de 32 bits: int e ponteiro têm 4 bytes cada). Os vazamentos e os usos de memória já liberada podem passar calados no editor. Para conferir, use o valgrind no seu computador.


Formas diferentes

a) Com typedef e ponteiro para ponteiro

Muito código escreve No em vez de struct No com um typedef, e passa &lista para as funções mudarem a cabeça elas mesmas, com um ponteiro para ponteiro (No **). A função fica livre para devolver outra coisa, como um bool dizendo se o malloc deu certo:

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

typedef struct No {
    int valor;
    struct No *prox;
} No;

bool inserir_inicio(No **inicio, int valor) {
    No *n = malloc(sizeof *n);
    if (n == NULL) {
        return false;
    }
    n->valor = valor;
    n->prox = *inicio;
    *inicio = n;
    return true;
}

void liberar(No **inicio) {
    No *p = *inicio;
    while (p != NULL) {
        No *prox = p->prox;
        free(p);
        p = prox;
    }
    *inicio = NULL;
}

int main(void) {
    No *lista = NULL;
    for (int v = 30; v >= 10; v -= 10) {
        if (!inserir_inicio(&lista, v)) {
            fprintf(stderr, "Sem memória\n");
            liberar(&lista);
            return 1;
        }
    }
    for (No *p = lista; p != NULL; p = p->prox) {
        printf("%d ", p->valor);
    }
    printf("\n");
    liberar(&lista);
    printf("Depois de liberar: %s\n", lista == NULL ? "NULL" : "ainda aponta");
    return 0;
}
$ gcc -std=c23 -Wall -Wextra duplo.c -o duplo
$ ./duplo
10 20 30
Depois de liberar: NULL
typedef struct No { … } No;
Cria o apelido No. Dentro das chaves, o apelido ainda não existe, por isso o campo continua struct No *prox;.
No **inicio
O endereço da variável lista do main. *inicio é a própria lista.
*inicio = n;
Muda a lista do main diretamente: ninguém precisa lembrar de guardar o retorno.
bool
No C23, bool, true e false são palavras da linguagem, sem #include C23. No C99 a C17, inclua <stdbool.h> (veja Booleanos).

b) Com ponteiro para o fim

Uma struct "dona da lista" guarda o primeiro nó, o último e o tamanho. Inserir no fim fica tão rápido quanto no início, porque não precisa andar até o último: ele já está guardado.

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

typedef struct No {
    int valor;
    struct No *prox;
} No;

typedef struct {
    No *inicio;
    No *fim;
    int tamanho;
} Lista;

bool inserir_fim(Lista *l, int valor) {
    No *n = malloc(sizeof *n);
    if (n == NULL) {
        return false;
    }
    n->valor = valor;
    n->prox = NULL;
    if (l->fim == NULL) {
        l->inicio = n;
    } else {
        l->fim->prox = n;
    }
    l->fim = n;
    l->tamanho++;
    return true;
}

void liberar(Lista *l) {
    No *p = l->inicio;
    while (p != NULL) {
        No *prox = p->prox;
        free(p);
        p = prox;
    }
    l->inicio = l->fim = NULL;
    l->tamanho = 0;
}

int main(void) {
    Lista senhas = {NULL, NULL, 0};
    for (int s = 101; s <= 104; s++) {
        if (!inserir_fim(&senhas, s)) {
            fprintf(stderr, "Sem memória\n");
            liberar(&senhas);
            return 1;
        }
    }
    printf("%d senhas: ", senhas.tamanho);
    for (No *p = senhas.inicio; p != NULL; p = p->prox) {
        printf("%d ", p->valor);
    }
    printf("\nÚltima: %d\n", senhas.fim->valor);
    liberar(&senhas);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra -g fim_rapido.c -o fim_rapido
$ ./fim_rapido
4 senhas: 101 102 103 104
Última: 104
$ valgrind -q --leak-check=full ./fim_rapido
4 senhas: 101 102 103 104
Última: 104

O -q ("quiet", quieto) faz o valgrind falar só quando acha problema: como não apareceu nada além da saída do programa, não há vazamento nem erro. Esta versão é a base das filas da próxima aula.

Qual usar

FormaComo chamaQuando usar
Devolve a cabeçalista = inserir_inicio(lista, 10);Para aprender e em programas pequenos. Fácil de ler; o perigo é esquecer o lista =.
Ponteiro para ponteiroinserir_inicio(&lista, 10);Quando a função precisa devolver outra coisa (sucesso ou erro). Muito comum em bibliotecas.
Struct com início, fim e tamanhoinserir_fim(&senhas, 101);Programas de verdade: inserir no fim e saber o tamanho ficam instantâneos.

Lista ligada ou array?

OperaçãoArray (ou array que cresce)Lista ligada
Pegar o elemento número iInstantâneo: v[i]Lento: andar i nós desde o começo
Inserir no inícioLento: empurrar todos uma casa para a direitaInstantâneo: mudar duas setas
Inserir no fimRápido (quase sempre; às vezes um realloc)Instantâneo com ponteiro para o fim; lento sem ele
Remover do meio (já sabendo onde)Lento: puxar os seguintes uma casa para a esquerdaInstantâneo: religar uma seta e dar free
Memória por elementoSó o valorO valor + o ponteiro (e o custo de cada malloc)
Percorrer tudoMuito rápido: tudo colado, o processador adoraMais lento: os nós ficam espalhados pela memória

Na prática, o array que cresce ganha na maioria dos programas. A lista ligada brilha quando há muitas inserções e remoções no começo ou no meio, e é a base de outras estruturas: pilhas e filas, os "baldes" da tabela hash e, com dois ponteiros por nó, as árvores.


Quando dá errado

Lista vazia: Falha de segmentação (código 139)

Esta função quer o último valor e esquece o caso da lista vazia:

#include <stdio.h>

struct No {
    int valor;
    struct No *prox;
};

int ultimo(const struct No *lista) {
    const struct No *p = lista;
    while (p->prox != NULL) {
        p = p->prox;
    }
    return p->valor;
}

int main(void) {
    struct No *lista = NULL;
    printf("Último: %d\n", ultimo(lista));
    return 0;
}
$ gcc -std=c23 -Wall -Wextra vazia.c -o vazia
$ ./vazia
Falha de segmentação (imagem do núcleo gravada)
$ echo $?
139

O que significa: com a lista vazia, p é NULL, e p->prox tenta ler a memória no endereço zero. O sistema mata o programa (veja Ponteiro Nulo). O gcc não avisou nada: o erro só aparece rodando. O editor do guia mostra a mesma mensagem e o mesmo código 139.

Como consertar: toda função que usa p-> logo de cara precisa tratar a lista vazia antes: if (lista == NULL) { … }. Desconfie de todo p->prox->: o p->prox pode ser NULL.

Usar o nó depois do free: warning: pointer ‘p’ used after ‘free’

Um liberar "esperto" demais, com o laço de sempre:

void liberar(struct No *lista) {
    for (struct No *p = lista; p != NULL; p = p->prox) {
        free(p);
    }
}
$ gcc -std=c23 -Wall -Wextra -g uaf.c -o uaf
uaf.c: In function ‘liberar’:
uaf.c:18:45: warning: pointer ‘p’ used after ‘free’ [-Wuse-after-free]
   18 |     for (struct No *p = lista; p != NULL; p = p->prox) {
      |                                           ~~^~~~~~~~~
uaf.c:19:9: note: call to ‘free’ here
   19 |         free(p);
      |         ^~~~~~~
$ ./uaf; echo "código: $?"
free(): invalid pointer
código: 134

O que significa: o p = p->prox roda depois do free(p): ele lê o campo de um nó que já foi devolvido. O gcc percebeu e avisou (used after free = "usado depois do free"). No laboratório, a biblioteca do C notou a bagunça e abortou o programa (código 134). Pode ser pior: às vezes o programa roda "normal" e falha só no dia da apresentação. O editor do guia, por exemplo, roda este programa sem reclamar. O valgrind aponta o erro com precisão:

$ valgrind ./uaf
==29164== Memcheck, a memory error detector
==29164== Copyright (C) 2002-2024, and GNU GPL'd, by Julian Seward et al.
==29164== Using Valgrind-3.26.0 and LibVEX; rerun with -h for copyright info
==29164== Command: ./uaf
==29164==
==29164== Invalid read of size 8
==29164==    at 0x4001200: liberar (uaf.c:18)
==29164==    by 0x4001265: main (uaf.c:28)
==29164==  Address 0x4aa60e8 is 8 bytes inside a block of size 16 free'd
==29164==    at 0x48538BF: free (vg_replace_malloc.c:990)
==29164==    by 0x40011FB: liberar (uaf.c:19)
==29164==    by 0x4001265: main (uaf.c:28)
==29164==  Block was alloc'd at
==29164==    at 0x4850858: malloc (vg_replace_malloc.c:447)
==29164==    by 0x40011A5: inserir_inicio (uaf.c:10)
==29164==    by 0x400124B: main (uaf.c:26)
==29164==
==29164==
==29164== HEAP SUMMARY:
==29164==     in use at exit: 0 bytes in 0 blocks
==29164==   total heap usage: 3 allocs, 3 frees, 48 bytes allocated
==29164==
==29164== All heap blocks were freed -- no leaks are possible
==29164==
==29164== For lists of detected and suppressed errors, rerun with: -s
==29164== ERROR SUMMARY: 3 errors from 1 contexts (suppressed: 0 from 0)

Invalid read of size 8 = "leitura inválida de 8 bytes" (o ponteiro prox), na linha 18. 8 bytes inside a block of size 16 free'd: o byte 8 de um nó de 16 bytes que já tinha sido liberado na linha 19. Foram 3 erros, um por nó.

Como consertar: guarde o próximo antes do free, como na função liberar da seção 7.

Esqueceu de guardar o retorno: a lista continua vazia

Escrever inserir_inicio(lista, 10); sem o lista = na frente compila sem aviso nenhum. O nó é criado, a função devolve o endereço dele… e ninguém guarda. A lista continua NULL e o nó vaza. No C23 você pode pedir ajuda ao compilador marcando a função com [[nodiscard]] ("não descarte") C23:

[[nodiscard]] struct No *inserir_inicio(struct No *lista, int valor) {
$ gcc -std=c23 -Wall -Wextra esqueceu.c -o esqueceu
esqueceu.c: In function ‘main’:
esqueceu.c:19:5: warning: ignoring return value of ‘inserir_inicio’, declared with attribute ‘nodiscard’ [-Wunused-result]
   19 |     inserir_inicio(lista, 10);
      |     ^~~~~~~~~~~~~~~~~~~~~~~~~
esqueceu.c:9:26: note: declared here
    9 | [[nodiscard]] struct No *inserir_inicio(struct No *lista, int valor) {
      |                          ^~~~~~~~~~~~~~
$ ./esqueceu
lista vazia

O que significa: ignoring return value = "ignorando o valor de retorno" de uma função marcada como nodiscard. O Clang do editor diz ignoring return value of function declared with 'nodiscard' attribute.

Como consertar: lista = inserir_inicio(lista, 10);. Ou use a forma com &lista, que não depende de retorno (veja Atributos).

error: unknown type name ‘No’
$ gcc -std=c23 -Wall -Wextra semtypedef.c -o semtypedef
semtypedef.c:3:5: error: unknown type name ‘No’
    3 |     No *prox;
      |     ^~

O que significa: dentro do typedef struct No { … } No;, o campo foi escrito No *prox;. O apelido No só passa a existir no } No; do fim (unknown type name = "nome de tipo desconhecido").

Como consertar: dentro das chaves, escreva struct No *prox;.

Perdeu o começo da lista

Percorrer com a própria variável da cabeça (while (lista != NULL) lista = lista->prox;) deixa lista em NULL no fim. Os nós continuam na memória, mas ninguém mais sabe onde: vazamento, e a lista "sumiu".

Como consertar: percorra com um ponteiro auxiliar (p), nunca com a cabeça. Nas funções, passar a cabeça como parâmetro já protege: o parâmetro é uma cópia (só o liberar anda com ela de propósito).


Referência rápida

EscritaO que faz
struct No { int valor; struct No *prox; };Define o nó: um valor e o endereço do próximo
struct No *lista = NULL;Lista vazia
for (p = lista; p != NULL; p = p->prox)Percorre todos os nós
n->prox = lista; lista = n;Insere n no início (nessa ordem)
while (p->prox != NULL) p = p->prox;Para no último nó (a lista não pode estar vazia)
ultimo->prox = n;Insere n no fim
ant->prox = p->prox; free(p);Remove p, que vem depois de ant
lista = p->prox; free(p);Remove o primeiro nó (p == lista)
prox = p->prox; free(p); p = prox;Libera um nó sem perder o resto
valgrind --leak-check=full ./progConfere vazamentos e acessos inválidos (compile com -g)

Exercícios

1. O campo que liga

Complete a definição do nó:

struct No { int valor; *prox; };

2. Andar uma casa

Complete o laço que percorre a lista:

for (p = lista; p != NULL; p = )

3. Lista vazia

Que valor tem a cabeça de uma lista vazia?

struct No *lista = ;

4. Para inserir o nó n no início, qual é a ordem certa?

5. Por que a função remover anda com dois ponteiros, ant e p?

6. O valgrind diz definitely lost: 16 bytes in 1 blocks e indirectly lost: 32 bytes in 2 blocks. O que aconteceu?

7. Qual operação é lenta numa lista ligada e instantânea num array?

Missão final: os pedidos da lojinha

Missão: a Ana anota os pedidos do dia numa lista ligada, na ordem em que chegam: 15, 40 e 25 reais. Mas a função inserir_fim está colocando cada pedido no início, e a função somar sempre devolve 0. Conserte as duas: inserir_fim deve andar até o último nó (cuidado com a lista vazia) e somar deve percorrer a lista somando os valores.

Pedidos: 15 -> 40 -> 25 -> NULL
Total: 80
#include <stdio.h>
#include <stdlib.h>

struct No {
    int valor;
    struct No *prox;
};

struct No *novo_no(int valor) {
    struct No *n = malloc(sizeof *n);
    if (n == NULL) {
        fprintf(stderr, "Sem memória\n");
        exit(1);
    }
    n->valor = valor;
    n->prox = NULL;
    return n;
}

// CONSERTE: hoje ela coloca o nó novo no INÍCIO
struct No *inserir_fim(struct No *lista, int valor) {
    struct No *n = novo_no(valor);
    n->prox = lista;
    return n;
}

// CONSERTE: hoje ela sempre devolve 0
int somar(const struct No *lista) {
    int soma = 0;
    (void)lista;
    return soma;
}

void imprimir(const struct No *lista) {
    printf("Pedidos: ");
    for (const struct No *p = lista; p != NULL; p = p->prox) {
        printf("%d -> ", p->valor);
    }
    printf("NULL\n");
}

void liberar(struct No *lista) {
    while (lista != NULL) {
        struct No *prox = lista->prox;
        free(lista);
        lista = prox;
    }
}

int main(void) {
    struct No *pedidos = NULL;
    pedidos = inserir_fim(pedidos, 15);
    pedidos = inserir_fim(pedidos, 40);
    pedidos = inserir_fim(pedidos, 25);
    imprimir(pedidos);
    printf("Total: %d\n", somar(pedidos));
    liberar(pedidos);
    return 0;
}
#include <stdio.h>
#include <stdlib.h>

struct No {
    int valor;
    struct No *prox;
};

struct No *novo_no(int valor) {
    struct No *n = malloc(sizeof *n);
    if (n == NULL) {
        fprintf(stderr, "Sem memória\n");
        exit(1);
    }
    n->valor = valor;
    n->prox = NULL;
    return n;
}

struct No *inserir_fim(struct No *lista, int valor) {
    struct No *n = novo_no(valor);
    if (lista == NULL) {
        return n;
    }
    struct No *p = lista;
    while (p->prox != NULL) {
        p = p->prox;
    }
    p->prox = n;
    return lista;
}

int somar(const struct No *lista) {
    int soma = 0;
    for (const struct No *p = lista; p != NULL; p = p->prox) {
        soma += p->valor;
    }
    return soma;
}

void imprimir(const struct No *lista) {
    printf("Pedidos: ");
    for (const struct No *p = lista; p != NULL; p = p->prox) {
        printf("%d -> ", p->valor);
    }
    printf("NULL\n");
}

void liberar(struct No *lista) {
    while (lista != NULL) {
        struct No *prox = lista->prox;
        free(lista);
        lista = prox;
    }
}

int main(void) {
    struct No *pedidos = NULL;
    pedidos = inserir_fim(pedidos, 15);
    pedidos = inserir_fim(pedidos, 40);
    pedidos = inserir_fim(pedidos, 25);
    imprimir(pedidos);
    printf("Total: %d\n", somar(pedidos));
    liberar(pedidos);
    return 0;
}

No seu computador, compile a solução com -g e rode valgrind --leak-check=full ./pedidos: com o liberar no fim, a resposta tem de ser All heap blocks were freed -- no leaks are possible.


Resumo

  • Uma lista ligada é uma corrente de nós. Cada nó é uma struct com o valor e o ponteiro struct No *prox para o próximo; o último aponta para NULL. A lista em si é só o ponteiro para o primeiro nó (NULL = vazia).
  • Percorrer: for (p = lista; p != NULL; p = p->prox). Use um ponteiro auxiliar, nunca a cabeça.
  • Inserir no início: n->prox = lista; lista = n;, nessa ordem. Instantâneo.
  • Inserir no fim: ande até o nó cujo prox é NULL e ligue o novo nele. Trate a lista vazia. Com um ponteiro para o fim, fica instantâneo.
  • Remover: ache o nó e o anterior, religue (ant->prox = p->prox, ou mude a cabeça se for o primeiro) e só então free(p).
  • Liberar: guarde o prox antes de cada free. Todo malloc tem o seu free; confira com valgrind --leak-check=full.
  • Lista ligada ganha em inserir e remover no começo e no meio; o array ganha em acessar por posição e em percorrer.