Ótimo, Mate. O PDF que o professor Renzo disponibilizou é uma introdução clara às listas ligadas, que são uma estrutura fundamental em C. Vamos entender o conceito e o que cada parte do material significa.
O professor começa comparando com vetores:
-
Em um vetor, os elementos ficam lado a lado na memória (contíguos).
-
Isso é bom para acesso rápido, mas ruim se você precisar mudar o tamanho.
- Se o vetor for pequeno, pode faltar espaço.
- Se for grande, você desperdiça memória.
-
E mesmo que haja espaço livre, pode não haver um bloco contínuo do tamanho que você precisa.
A lista ligada resolve isso: Cada elemento (chamado nó) é alocado dinamicamente, e um nó aponta para o próximo. Assim, os elementos não precisam estar juntos na memória.
Em C, o nó é definido assim:
typedef struct no *p_no;
struct no {
int dado; // valor armazenado
p_no prox; // ponteiro para o próximo nó
};Aqui:
-
p_noé um apelido parastruct no *(ou seja, um ponteiro para nó). -
Cada nó contém:
- um valor (
dado); - um ponteiro para o próximo nó (
prox).
- um valor (
Se prox for NULL, significa que aquele é o último nó da lista.
p_no criar_lista() {
return NULL;
}Uma lista vazia é representada por NULL, pois ainda não há nós.
void destruir_lista(p_no lista) {
if (lista != NULL) {
destruir_lista(lista->prox);
free(lista);
}
}Aqui:
- A função chama a si mesma para liberar todos os nós.
- Quando chega no último (
prox == NULL), libera a memória de volta.
p_no adicionar_elemento(p_no lista, int valor) {
p_no novo = malloc(sizeof(struct no));
novo->dado = valor;
novo->prox = lista;
return novo;
}O que acontece:
- Aloca memória para um novo nó.
- Guarda o valor nele.
- Faz o novo nó apontar para a lista antiga.
- Retorna o ponteiro para o novo nó, que passa a ser o início da lista.
Exemplo:
lista = adicionar_elemento(lista, 10);
lista = adicionar_elemento(lista, 20);
lista = adicionar_elemento(lista, 30);A memória fica assim:
[30] -> [20] -> [10] -> NULL
void imprime(p_no lista) {
p_no atual;
for (atual = lista; atual != NULL; atual = atual->prox)
printf("%d\n", atual->dado);
}void imprime_recursivo(p_no lista) {
if (lista != NULL) {
printf("%d\n", lista->dado);
imprime_recursivo(lista->prox);
}
}Ambas percorrem a lista até encontrar NULL.
p_no busca(p_no lista, int x) {
if (lista == NULL || lista->dado == x)
return lista;
return busca(lista->prox, x);
}Ela retorna o ponteiro para o nó que contém o valor x, ou NULL se não existir.
p_no remove(p_no lista, int x) {
if (lista == NULL) return NULL;
if (lista->dado == x) {
p_no proximo = lista->prox;
free(lista);
return proximo;
}
lista->prox = remove(lista->prox, x);
return lista;
}p_no remove_todos(p_no lista, int x) {
if (lista == NULL) return NULL;
lista->prox = remove_todos(lista->prox, x);
if (lista->dado == x) {
p_no proximo = lista->prox;
free(lista);
return proximo;
}
return lista;
}#include <stdio.h>
#include <stdlib.h>
typedef struct no *p_no;
struct no {
int dado;
p_no prox;
};
p_no criar_lista() {
return NULL;
}
p_no adicionar_elemento(p_no lista, int valor) {
p_no novo = malloc(sizeof(struct no));
novo->dado = valor;
novo->prox = lista;
return novo;
}
void imprime(p_no lista) {
for (p_no atual = lista; atual != NULL; atual = atual->prox)
printf("%d ", atual->dado);
printf("\n");
}
void destruir_lista(p_no lista) {
if (lista != NULL) {
destruir_lista(lista->prox);
free(lista);
}
}
int main() {
int num;
p_no lista = criar_lista();
printf("Digite números positivos (0 para parar): ");
scanf("%d", &num);
while (num > 0) {
lista = adicionar_elemento(lista, num);
scanf("%d", &num);
}
imprime(lista);
destruir_lista(lista);
return 0;
}Se você digitar:
10 20 30 0
A lista fica:
[30] -> [20] -> [10] -> NULL
Porque cada novo elemento é adicionado no início.
Boa pergunta, Mate. O operador -> em C é usado para acessar campos de uma struct por meio de um ponteiro.
Vamos entender isso em partes:
Exemplo:
struct no {
int dado;
struct no *prox;
};
struct no n;
n.dado = 10;Aqui, n não é um ponteiro, é a própria struct.
Então, para acessar o campo dado, usamos o ponto (.):
n.dado.
struct no *p;
p = &n;Agora p não é a struct em si, e sim um endereço que aponta para ela.
Para acessar dado, primeiro precisaríamos “seguir” o ponteiro:
(*p).dado = 10;O que isso significa:
*pacessa o conteúdo apontado (a struct em si);(*p).dadoacessa o campodadodessa struct.
O -> é apenas uma forma abreviada de escrever (*p).campo.
Essas duas linhas são iguais:
(*p).dado = 10;
p->dado = 10;novo->dado = valor;
novo->prox = lista;novoé um ponteiro para uma struct do tipono, criada commalloc.- Então
novo->dadoquer dizer “o campodadoda struct apontada pornovo”.
Se você tentasse usar novo.dado, daria erro, porque novo não é a struct em si, é um ponteiro.
| Situação | Operador | Exemplo |
|---|---|---|
| Você tem a struct diretamente | . |
n.dado |
| Você tem um ponteiro para struct | -> |
p->dado |
p->campo é o mesmo que |
(*p).campo |