Antes de escribir la primera función de una lista enlazada necesitamos manejar con soltura nociones clave del lenguaje C: cómo se construye un nodo, qué significa manipular punteros, cuándo aplicar memoria dinámica y cómo evitar errores que corrompen la estructura. Cada subsección profundiza estos pilares para que, al llegar a la implementación, nada resulte misterioso.
El nodo es la unidad mínima de una lista. Contiene datos y enlaces que definen su posición relativa. Visualmente se puede pensar como dos campos contiguos: a la izquierda el valor y a la derecha el puntero al siguiente nodo (denominado sig). Independientemente del tipo de lista (simple, doble o circular) todos comparten este concepto; en variantes dobles se agrega un puntero ant para retroceder.
typedef struct Nodo {
int valor;
struct Nodo *sig; /* apunta al nodo siguiente o NULL */
} Nodo;
La definición anterior declara el tipo Nodo y deja listo el campo sig para crearse dinámicamente según la cantidad de elementos. Si necesitáramos una lista doble, bastaría con agregar un puntero ant que apunte al nodo anterior.
El lenguaje C permite agrupar datos heterogéneos mediante struct. Al definirlo debemos pensar en visibilidad y reutilización: declarar el struct en un archivo de cabecera (.h) facilita que otras unidades de compilación conozcan la forma del nodo sin duplicar código.
/* lista.h */
#ifndef LISTA_H
#define LISTA_H
#include <stdbool.h>
typedef struct Nodo {
int valor;
struct Nodo *sig;
} Nodo;
Nodo *nodo_crear(int valor);
void nodo_destruir(Nodo *nodo);
bool lista_insertar_inicio(Nodo **cabeza, int valor);
void lista_imprimir(const Nodo *cabeza);
void lista_limpiar(Nodo **cabeza);
#endif
En el archivo lista.c implementamos las funciones declaradas y las usamos para encapsular la creación y destrucción de nodos. Esta separación hace que la lógica de memoria quede en un solo lugar, lo cual simplifica la depuración.
Una lista enlazada depende al 100% de los punteros. Cada campo sig almacena una dirección de memoria, no una copia del nodo. Por eso, al reasignar un puntero estamos conectando o desconectando nodos reales. Entender cómo se declara, inicializa y utiliza un puntero es vital para evitar referencias colgantes.
Nodo *cabeza = NULL;
Nodo *nuevo = nodo_crear(42);
if (nuevo) {
nuevo->sig = cabeza; /* enlaza con la antigua cabeza */
cabeza = nuevo; /* ahora el nuevo nodo es la cabeza */
}
El ejemplo muestra dos punteros: cabeza (puntero doblemente indirecto si lo pasamos a funciones) y nuevo. Moverlos en el orden correcto garantiza que ningún nodo quede perdido. Siempre conviene inicializar los punteros a NULL para detectar usos indebidos durante la depuración.
El crecimiento de la lista depende de la memoria dinámica. En C se obtiene un bloque con malloc, se usa mientras sea necesario y luego se libera con free. Cada inserción debería llamar a malloc (directa o indirectamente) y cada eliminación debe liberar el bloque asociado.
Nodo *nodo_crear(int valor) {
Nodo *n = malloc(sizeof(Nodo));
if (!n) {
return NULL; /* sin memoria disponible */
}
n->valor = valor;
n->sig = NULL;
return n;
}
void nodo_destruir(Nodo *nodo) {
free(nodo);
}
Es buena práctica envolver las llamadas a malloc para validar el resultado y concentrar la inicialización en un solo lugar. De esta forma evitamos olvidar campos o propagar el manejo de errores por todo el código.
El trabajo con punteros trae consigo riesgos comunes que conviene dominar antes de avanzar:
free dos veces sobre el mismo puntero genera errores severos en el asignador.Para prevenir estos problemas es recomendable implementar funciones auxiliares que centralicen la administración de punteros y habilitar herramientas como sanitizadores o analizadores estáticos cuando sea posible.
Con los conceptos anteriores podemos crear un pequeño proyecto listo para compilar en CLion o en cualquier entorno compatible con CMake. La idea es tener un lista.h con el contrato, un lista.c que implementa las funciones y un main.c que las ejercita.
/* lista.h */
#ifndef LISTA_H
#define LISTA_H
#include <stdbool.h>
typedef struct Nodo {
int valor;
struct Nodo *sig;
} Nodo;
Nodo *nodo_crear(int valor);
void nodo_destruir(Nodo *nodo);
bool lista_insertar_inicio(Nodo **cabeza, int valor);
void lista_imprimir(const Nodo *cabeza);
void lista_limpiar(Nodo **cabeza);
#endif
/* lista.h */#ifndef LISTA_H / #define LISTA_H ... #endif#include <stdbool.h>bool para declarar funciones que devuelven verdadero/falso.typedef struct Nodo { ... } Nodo;valor y el puntero sig; el typedef permite escribir Nodo sin repetir struct./* lista.c */
#include "lista.h"
#include <stdio.h>
#include <stdlib.h>
Nodo *nodo_crear(int valor) {
Nodo *n = malloc(sizeof(Nodo));
if (!n) return NULL;
n->valor = valor;
n->sig = NULL;
return n;
}
void nodo_destruir(Nodo *nodo) {
free(nodo);
}
bool lista_insertar_inicio(Nodo **cabeza, int valor) {
Nodo *n = nodo_crear(valor);
if (!n) return false;
n->sig = *cabeza;
*cabeza = n;
return true;
}
void lista_imprimir(const Nodo *cabeza) {
const Nodo *reco = cabeza;
while (reco) {
printf("%d -> ", reco->valor);
reco = reco->sig;
}
puts("NULL");
}
void lista_limpiar(Nodo **cabeza) {
while (*cabeza) {
Nodo *tmp = (*cabeza)->sig;
nodo_destruir(*cabeza);
*cabeza = tmp;
}
}
/* lista.c */#include "lista.h"#include <stdio.h> / #include <stdlib.h>printf/puts y malloc/free, respectivamente.Nodo *nodo_crear(int valor) { ... }malloc), valida el resultado, asigna valor y deja sig en NULL.void nodo_destruir(Nodo *nodo)free para liberar un nodo.bool lista_insertar_inicio(Nodo **cabeza, int valor)sig con la antigua cabeza y actualiza el puntero externo.void lista_imprimir(const Nodo *cabeza)reco y recorre con un while para mostrar cada valor seguido de una flecha.void lista_limpiar(Nodo **cabeza)/* main.c */
#include "lista.h"
#include <stdio.h>
int main(void) {
Nodo *cabeza = NULL;
lista_insertar_inicio(&cabeza, 30);
lista_insertar_inicio(&cabeza, 20);
lista_insertar_inicio(&cabeza, 10);
puts("Lista actual:");
lista_imprimir(cabeza);
lista_limpiar(&cabeza);
return 0;
}
/* main.c */#include "lista.h" / #include <stdio.h>int main(void)Nodo *cabeza = NULL;lista_insertar_inicio(...)puts("Lista actual:"); / lista_imprimir(cabeza);lista_limpiar(&cabeza);return 0;