Lista Genérica
Nesta unidade, iremos implementar uma lista genérica. As funções de callback serão armazenadas no construtor da lista, dando um toque mais profissional para nosso programa.
Interface
list.h
#include <stdbool.h>
#include <stddef.h>
typedef void(*cb_free)(void *);
typedef int(*cb_compare)(void *a, void *b);
typedef struct node {
void* data; // Ponteiro genérico para o dado
struct node* next;
} Node;
typedef struct {
site_t data_size;
Node *head;
// Funções de callback para o tipo de dado
cb_free free_data;
cb_compare compare_data;
} List;
List* list_create (size_t data_size, cb_free free_data, cb_compare compare_data);
void list_destroy (List *list);
void list_add (List *list, void *data);
bool list_contains (List *list, void *data);Nossa lista é bastante simples. Cada nó possui um dado e um ponteiro para o próximo nó. Já a estrutura lista é composta pelo tamanho do dado, um ponteiro para a cabeça da lista e duas funções de callback. As funções declaradas serão usadas para criar uma lista, liberar a memória da lista, adicionar um novo nó e determinar se um nó pertence a uma lista.
Implementação
Importando as bibliotecas.
list.c
Criar Lista
A primeira função que iremos implementar é o construtor da lista, list_create. O primeiro paramêtro é o tamanho em bytes do dado que iremos armazenar na lista. Depois as funções de callback para liberar a memória desse dado e como comparar dois dados.
list.c
Como a lista inicia vazia, logo a cabeça da lista será NULL.
Adicionar Elementos
Após criar a lista, temos de adicionar nós a ela. A função list_add aloca memória duas vezes, uma para o nó e outra para o dado. Não podemos fazer um simples atribuição com = para passar o dado para o campo data do nó. Portanto, usamos a função memcpy que estudamos na seção sobre gerenciamento de memória.
list.c
Essa função adiciona elementos na cabeça da lista: o novo nó aponta para a cabeça da lista (linha 9) e depois a cabeça da lista é atualizada para o novo nó (linha 10).
Apagar Lista
Precisamos criar o destrutor da lista. Mas antes temos de implementar a free_node_data. Ela serve para liberar a memória alocada para o dado que é armazenado na lista. Esse dado pode ser mais complexo do que pensamos (algum struct com alocação interna). Por isso, passamos um função callback para tratar esses casos. Por outro lado, para uma lista de inteiros ou outro tipo simples, um free é suficiente.
list.c
Mote que essa função não foi declarada no arquivo .h e isso significa que ela é privada. O modificador static indica isso para nós.
A lógica do destrutor é simples. Após a verificação de segurança do assert, criamos um nó que aponta para a cabeça da lista. Em seguida, caminhamos na lista em direção a cauda. Nesse percurso, liberamos a memória do dado e depois do nó. Quando acabamos o percurso, liberaramos a estrutura lista.
list.c
Note que usamos três liberações de memória. Uma para o dado (linha 8), uma para o nó (linha 9) e outra para a lista (linha 13). Isso porque usamos a alocação de memória três vezes, uma em list_create e duas em list_add. Lembre-se do que estudamos, para cada alocação deve haver um liberação correspondente.
Procurar Elementos
Uma das funções que declaramos em nossa interface foi list_contains. Ela será usada para saber se algum elemento existe dentro da lista. Ela só retorna sim ou não.
list.c
Se liga! É uma boa prática implementar a função de comparação da seguinte forma:
- Se a < b, retorne um número negativo
- Se a = b, retorne zero
- Se a > b, retorne um número positivo
Note que para tipos primitivos, como inteiros, a igualdade é simples (a == b). Contudo, nossa lista armazena dados de forma genérica (void *data), o que significa que o operador == só compararia os endereços de memória dos dados, e não o seu conteúdo lógico. Por isso, o callback compare_data é essencial: ele permite que o usuário defina a lógica de comparação correta para tipos complexos, como um struct de aluno (onde a igualdade pode depender, por exemplo, do ID ou Matrícula).
Exemplo de Uso
Importando as bibliotecas e API da lista que criamos.
Função de callback para comparar dois inteiros.
int_list.c
Se liga! Não vamos precisar de um função callback para liberar um inteiro, pois nossa função free_node_data já trata esse caso se passarmos NULL como argumento para list_create.
Lembre-se, o padrão é que retorne zero se ambos são iguais.
Função principal:
int_list.c
int main() {
// Criar uma lista de inteiros
List *list = list_create(sizeof(int), NULL, compare_int);
int item[5] = {2, 5, 3, 8, 9};
// Adicionar elementos
for (int i = 0; i < 5; i++) {
int *data = malloc(sizeof(int));
*data = item[i];
list_add(list, data);
free(data);
}
// Verificar se um valor está na lista
int search_key = 8;
printf("Procurando nó com chave: %d...\n", search_key);
if (list_contains(list, &search_key))
printf("-> Sucesso: Chave %d encontrada!\n", search_key);
else
printf("-> Falha: Chave %d não encontrada.\n", search_key);
printf("----------------------------------------\n");
// Destruir a lista
list_destroy(list);
list = NULL;
return 0;
}Para demonstrar o poder da nossa lista genérica, vamos criar uma lista de anulos, onde cada aluno é uma struct.
student_list.c
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include "list.h"
typedef struct student {
char* name;
int id;
} Student;
int compare_student(void *a, void *b) {
Student *student_a = (Student *)a;
Student *student_b = (Student *)b;
return student_a->id - student_b->id;
}
void free_student(void *data) {
Student *student_ptr = (Student *)data;
if (student_ptr == NULL)
return;
if (student_ptr->name != NULL)
free(student_ptr->name);
free(student_ptr);
}Função auxiliar para criar um aluno.
student_list.c
Student *create_student(const char *name, int id) {
Student *new_student = (Student *)malloc(sizeof(Student));
if (new_student == NULL) {
fprintf(stderr, "Erro ao alocar Student\n");
return 1;
}
new_student->id = id;
// strdup faz o malloc interno e a cópia.
new_student->name = strdup(name);
if (new_student->name == NULL) {
free(new_student); // Limpeza em caso de falha de alocação do nome
fprintf(stderr, "Erro ao alocar nome\n");
return 1;
}
return new_student;
}Se liga! Internamente, strdup faz:
char *new_name = malloc(strlen(name) + 1);strcpy(new_name, name);
Note que o free correspondente ao malloc de strdup está no callback free_student, no segundo if.
Agora estamos prontos para simular uma lista de alunos.
student_list.c
int main() {
// Criar um lista de alunos
List *list = list_create(sizeof(Student), free_student, compare_student);
// Nomes e IDs
const char *names[] = {"Alice", "Bob", "Charlie", "Diana"};
int ids[] = {101, 205, 303, 408};
// Adicionar elementos
for (int i = 0; i < 4; i++) {
Student *s = create_student(names[i], ids[i]);
list_add(list, s);
}
// Verificar se um aluno está na lista
Student search_key = {.name = NULL, .id = 303};
printf("Procurando aluno com ID: %d...\n", search_key.id);
if (list_contains(list, &search_key))
printf("-> Sucesso: Aluno ID %d encontrado!\n", search_key.id);
else
printf("-> Falha: Aluno ID %d não encontrado.\n", search_key.id);
printf("----------------------------------------\n");
// Destruir a lista
list_destroy(list);
list = NULL;
return 0;
}A implementação desta lista genérica garante alta modularidade. Graças ao design baseado em funções callback, evitamos a reimplementação de funções centrais (como criação, adição ou destruição). Para adaptar a lista a qualquer novo tipo de dado, basta fornecer os callbacks específicos para a comparação e liberação de memória daquele tipo.
// Callback para iterar e imprimir
bool print_int(int index, void *data) {
int value = *(int*)data;
printf("[%d]: %d\n", index, value);
return true; // continuar a iteração
}Conclusão
Com a lista genérica implementada, concluímos com sucesso a exploração de um dos temas mais poderosos em programação C: o design de estruturas de dados independentes de tipo.
O verdadeiro valor deste exercício não está apenas no código da lista, mas nos conceitos transferíveis que você aprendeu. A utilização estratégica de ponteiros para funções (callbacks) e a rigorosa gestão da memória via Heap são os pilares que sustentam a criação de qualquer estrutura avançada. Você agora tem o conhecimento necessário para replicar essa arquitetura em pilhas, filas, árvores genéricas e além.
Este trabalho estabelece a base para o desenvolvimento profissional, alinhando seu código com os padrões utilizados nas bibliotecas de software de código aberto mais robustas. Você não apenas criou uma lista; você adquiriu um modelo de design para resolver problemas complexos com elegância.
Nesta unidade, você aprendeu
✅ a criar uma lista ligada genérica simples