Nesta aula, vamos construir um projeto completo de um gerenciador de registros (como uma agenda de contatos) utilizando uma lista ligada em C. Este projeto integra conceitos fundamentais: ponteiros, estruturas, alocação dinâmica, manipulação de memória e interação com o usuário via menu. Ao final, você terá um programa funcional que permite criar, listar, atualizar e excluir registros em memória, com a devida liberação de memória ao final.

A lista ligada é uma estrutura de dados dinâmica onde cada elemento (nó) contém um dado e um ponteiro para o próximo nó. Diferente de arrays, ela cresce e encolhe conforme necessário, sem desperdício de memória. Este projeto será um exemplo prático de como aplicar CRUD (Create, Read, Update, Delete) em uma estrutura encadeada, com um menu textual simples.

CRUD em memória

CRUD é um acrônimo para as quatro operações básicas de persistência: Create (criar), Read (ler), Update (atualizar) e Delete (excluir). Em nosso gerenciador, essas operações serão realizadas diretamente na memória RAM, ou seja, os dados são perdidos quando o programa termina. Apesar disso, o foco está na lógica de manipulação da lista ligada.

Para implementar o CRUD, precisamos de funções que percorram a lista, insiram nós no início/meio/fim, localizem nós por um campo-chave (ex.: ID) e removam nós. Vamos definir uma estrutura Contato com campos como id, nome e telefone. A lista será composta por nós que contêm um Contato e um ponteiro para o próximo nó.

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

#define MAX_NOME 100
#define MAX_TEL 20

typedef struct {
    int id;
    char nome[MAX_NOME];
    char telefone[MAX_TEL];
} Contato;

typedef struct No {
    Contato dado;
    struct No* prox;
} No;

As operações de CRUD serão implementadas como funções que recebem um ponteiro para o ponteiro da cabeça da lista (para permitir alterações na própria cabeça). Por exemplo, a função de inserir no início:

void inserir_inicio(No** cabeca, Contato novo) {
    No* novo_no = (No*)malloc(sizeof(No));
    if (novo_no == NULL) {
        printf("Erro de alocação.\n");
        return;
    }
    novo_no->dado = novo;
    novo_no->prox = *cabeca;
    *cabeca = novo_no;
}

Para o CRUD completo, teremos funções para inserir (em ordem ou no fim), listar todos, buscar por ID, atualizar um registro existente e remover um nó específico. A busca e atualização percorrem a lista comparando o ID. A remoção precisa ajustar os ponteiros, cuidando do caso de remover a cabeça.

Alocação dinâmica

A alocação dinâmica é feita com malloc, calloc ou realloc. No nosso projeto, cada novo nó é alocado com malloc(sizeof(No)). É crucial verificar se o retorno é NULL, indicando falha de alocação. A memória alocada fica no heap e deve ser liberada com free quando não for mais necessária.

Um erro comum é não verificar o retorno de malloc. Em sistemas embarcados ou com pouca memória, isso pode causar falhas. Sempre trate o caso de alocação falha. Além disso, ao inserir um novo nó, copiamos a estrutura Contato por valor (atribuição direta), o que é válido porque não há ponteiros internos.

Vejamos um exemplo de alocação com verificação e inicialização:

No* criar_no(Contato c) {
    No* novo = (No*)malloc(sizeof(No));
    if (novo == NULL) {
        fprintf(stderr, "Falha na alocação!\n");
        exit(1);
    }
    novo->dado = c;
    novo->prox = NULL;
    return novo;
}

Essa função é reutilizável e evita repetição de código. Ela também centraliza a política de erro. No projeto, usaremos essa função para criar nós.

Liberação

Liberação de memória é uma responsabilidade do programador em C. Para cada malloc, deve haver um free correspondente. Em listas ligadas, a liberação deve percorrer todos os nós e liberar um a um, para não perder o acesso aos demais. A função liberar_lista faz isso:

void liberar_lista(No** cabeca) {
    No* atual = *cabeca;
    while (atual != NULL) {
        No* proximo = atual->prox;
        free(atual);
        atual = proximo;
    }
    *cabeca = NULL;
}

É importante definir a lista como NULL após a liberação para evitar ponteiros pendentes. No programa principal, chamamos liberar_lista(&lista) antes de encerrar. Isso evita vazamentos de memória, que podem causar esgotamento da memória em programas longos.

Outro cuidado: ao remover um nó individual, devemos liberar a memória do nó removido. A função de remoção deve capturar o nó a ser removido, ajustar os ponteiros e então dar free.

Menu

O menu é a interface com o usuário. Vamos exibir opções como: 1) Inserir contato, 2) Listar contatos, 3) Buscar por ID, 4) Atualizar contato, 5) Remover contato, 0) Sair. O programa lê a opção e chama a função correspondente. Usaremos um loop do-while que repete até o usuário escolher sair.

Uma boa prática é limpar o buffer do teclado após ler números, para evitar problemas com fgets. Usaremos scanf para números e fgets para strings, com cuidado para remover o newline. O menu pode ser implementado assim:

void exibir_menu() {
    printf("\n=== GERENCIADOR DE CONTATOS ===\n");
    printf("1. Inserir contato\n");
    printf("2. Listar contatos\n");
    printf("3. Buscar por ID\n");
    printf("4. Atualizar contato\n");
    printf("5. Remover contato\n");
    printf("0. Sair\n");
    printf("Escolha: ");
}

void limpar_buffer() {
    int c;
    while ((c = getchar()) != '\n' && c != EOF);
}

No main, teremos uma variável No* lista = NULL; e um loop com switch para tratar cada opção. Ao final, liberamos a lista. Veja um esqueleto:

int main() {
    No* lista = NULL;
    int opcao;
    do {
        exibir_menu();
        scanf("%d", &opcao);
        limpar_buffer();
        switch (opcao) {
            case 1: {
                Contato c;
                printf("ID: "); scanf("%d", &c.id); limpar_buffer();
                printf("Nome: "); fgets(c.nome, MAX_NOME, stdin); c.nome[strcspn(c.nome, "\n")] = 0;
                printf("Telefone: "); fgets(c.telefone, MAX_TEL, stdin); c.telefone[strcspn(c.telefone, "\n")] = 0;
                inserir_ordenado(&lista, c);
                break;
            }
            case 2: listar(lista); break;
            case 3: { int id; printf("ID: "); scanf("%d", &id); limpar_buffer(); buscar(lista, id); break; }
            case 4: atualizar(lista); break;
            case 5: remover(&lista); break;
            case 0: printf("Saindo...\n"); break;
            default: printf("Opção inválida.\n");
        }
    } while (opcao != 0);
    liberar_lista(&lista);
    return 0;
}

Esse menu é simples e direto. Em um projeto real, poderíamos adicionar persistência em arquivo, mas aqui focamos na memória.

Boas práticas e observações finais

Ao desenvolver este projeto, siga boas práticas: modularize as funções, use nomes descritivos, trate erros de alocação, e sempre libere memória. Além disso, considere a possibilidade de usar valgrind para verificar vazamentos de memória. Teste cada operação isoladamente e depois integre. Lembre-se de que a lista ligada é uma base para estruturas mais complexas, como pilhas e filas.

Referências

Exercícios

  1. Implemente uma função inserir_ordenado que insere um novo contato na lista de forma ordenada pelo ID (crescente). Considere os casos: lista vazia, inserção no início, no meio e no fim.

    ✓ Resposta:
    void inserir_ordenado(No** cabeca, Contato novo) {
        No* novo_no = criar_no(novo);
        if (*cabeca == NULL || (*cabeca)->dado.id > novo.id) {
            novo_no->prox = *cabeca;
            *cabeca = novo_no;
            return;
        }
        No* atual = *cabeca;
        while (atual->prox != NULL && atual->prox->dado.id < novo.id) {
            atual = atual->prox;
        }
        novo_no->prox = atual->prox;
        atual->prox = novo_no;
    }
    
  2. Escreva a função listar que percorre a lista e imprime todos os contatos no formato "ID: nome - telefone".

    ✓ Resposta:
    void listar(No* cabeca) {
        if (cabeca == NULL) {
            printf("Lista vazia.\n");
            return;
        }
        No* atual = cabeca;
        while (atual != NULL) {
            printf("%d: %s - %s\n", atual->dado.id, atual->dado.nome, atual->dado.telefone);
            atual = atual->prox;
        }
    }
    
  3. Implemente a função buscar que recebe a lista e um ID, e imprime o contato correspondente ou uma mensagem de não encontrado.

    ✓ Resposta:
    void buscar(No* cabeca, int id) {
        No* atual = cabeca;
        while (atual != NULL) {
            if (atual->dado.id == id) {
                printf("%d: %s - %s\n", atual->dado.id, atual->dado.nome, atual->dado.telefone);
                return;
            }
            atual = atual->prox;
        }
        printf("Contato com ID %d não encontrado.\n", id);
    }
    
  4. Crie a função atualizar que solicita o ID do contato a ser atualizado e, se encontrado, permite alterar nome e telefone (ID permanece).

    ✓ Resposta:
    void atualizar(No* cabeca) {
        int id;
        printf("ID do contato a atualizar: ");
        scanf("%d", &id);
        limpar_buffer();
        No* atual = cabeca;
        while (atual != NULL) {
            if (atual->dado.id == id) {
                printf("Novo nome: "); fgets(atual->dado.nome, MAX_NOME, stdin); atual->dado.nome[strcspn(atual->dado.nome, "\n")] = 0;
                printf("Novo telefone: "); fgets(atual->dado.telefone, MAX_TEL, stdin); atual->dado.telefone[strcspn(atual->dado.telefone, "\n")] = 0;
                printf("Contato atualizado.\n");
                return;
            }
            atual = atual->prox;
        }
        printf("Contato não encontrado.\n");
    }
    
  5. Escreva a função remover que solicita o ID e remove o nó correspondente, liberando a memória. Trate o caso de lista vazia e de remoção da cabeça.

    ✓ Resposta:
    void remover(No** cabeca) {
        if (*cabeca == NULL) {
            printf("Lista vazia.\n");
            return;
        }
        int id;
        printf("ID do contato a remover: ");
        scanf("%d", &id);
        limpar_buffer();
        No* atual = *cabeca;
        No* anterior = NULL;
        while (atual != NULL && atual->dado.id != id) {
            anterior = atual;
            atual = atual->prox;
        }
        if (atual == NULL) {
            printf("Contato não encontrado.\n");
            return;
        }
        if (anterior == NULL) {
            *cabeca = atual->prox;
        } else {
            anterior->prox = atual->prox;
        }
        free(atual);
        printf("Contato removido.\n");
    }