Un puntero a puntero, técnicamente llamado doble indirección, es una variable que almacena la dirección de memoria de otra variable que, a su vez, es un puntero. Si un puntero estándar guarda la dirección de un int, un puntero a puntero (int **) guarda la dirección de ese puntero. En la arquitectura de la CPU, esto significa que para alcanzar el valor final, el procesador debe realizar dos accesos a memoria: uno para obtener la dirección contenida en el primer puntero y otro para acceder al valor en la dirección resultante.
Esta mecánica es fundamental para gestionar la memoria de forma dinámica y flexible. Cuando necesitas que una función no solo devuelva un valor, sino que modifique la dirección a la que apunta un puntero en la función llamada (como al redimensionar un array o asignar un nuevo string), debes pasar la dirección de ese puntero, es decir, un puntero a puntero. También es la base para estructuras de datos complejas como matrices dinámicas de filas de distintos tamaños o listas enlazadas donde la “cabeza” de la lista debe ser actualizada.
Debes usar la doble indirección siempre que la identidad del puntero original deba cambiar en un ámbito distinto al actual. Si intentas pasar un puntero simple (char *) a una función para que esta le asigne memoria mediante malloc, la función trabajará sobre una copia de la dirección, y el puntero original en la función main permanecerá inalterado (o apuntará a basura), provocando fugas de memoria o fallos de segmentación. Si lo haces mal, perderás el control sobre la dirección original y el programa perderá la capacidad de liberar la memoria asignada anteriormente.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* Cambia la dirección a la que apunta el puntero original 'ptr' */
void asignar_nuevo_string(char **ptr_a_string, const char *nuevo_contenido) {
if (ptr_a_string == NULL || nuevo_contenido == NULL) return;
/* Liberamos la memoria actual si el puntero apunta a algo */
free(*ptr_a_string);
/* Asignamos nueva memoria al puntero original */
*ptr_a_string = (char *)malloc(strlen(nuevo_contenido) + 1);
if (*ptr_a_string != NULL) {
strcpy(*ptr_a_string, nuevo_contenido);
}
}
/* Crea una matriz dinámica: un array de punteros a arrays de ints */
int **crear_matriz_dinamica(int filas, int columnas) {
int **matriz = (int **)malloc(filas * sizeof(int *));
if (matriz == NULL) return NULL;
for (int i = 0; i < filas; i++) {
matriz[i] = (int *)malloc(columnas * sizeof(int));
if (matriz[i] == NULL) {
/* Limpieza en caso de fallo parcial de malloc */
for (int j = 0; j < i; j++) free(matriz[j]);
free(matriz);
return NULL;
}
}
return matriz;
}
void liberar_matriz(int **matriz, int filas) {
if (matriz == NULL) return;
for (int i = 0; i < filas; i++) {
free(matriz[i]);
}
free(matriz);
}
int main(void) {
/* Caso 1: Modificar un puntero en una función */
char *nombre = strdup("Original");
printf("Nombre inicial: %s\n", nombre);
asignar_nuevo_string(&nombre, "Actualizado via doble puntero");
printf("Nombre final: %s\n", nombre);
free(nombre);
printf("\n");
/* Caso 2: Matriz dinámica (Array de punteros) */
int filas = 3, cols = 4;
int **m = crear_matriz_dinamica(filas, cols);
if (m != NULL) {
for (int i = 0; i < filas; i++) {
for (int j = 0; j < cols; j++) {
m[i][j] = i * 10 + j;
printf("%2d ", m[i][j]);
}
printf("\n");
}
liberar_matriz(m, filas);
}
return 0;
}
Análisis del código
En la función asignar_nuevo_string, recibimos char **ptr_a_string. El uso de *ptr_a_string es crucial: estamos desreferenciando el puntero a puntero para acceder a la dirección almacenada en la variable original del main. Si hubiéramos pasado char *ptr, el malloc asignaría memoria en una dirección local a la función, y al terminar la función, el main seguiría teniendo su puntero original apuntando a la memoria vieja o a NULL.
En crear_matriz_dinamica, la declaración int **matriz define un array de punteros. Fíjate que la memoria no es un bloque contiguo único como en una matriz estática int m[3][4]. En la versión dinámica, primero reservamos un bloque para los punteros (sizeof(int *)) y luego, en un bucle, cada posición de ese bloque apunta a un bloque de int independiente. Esto permite que las filas no sean necesariamente contiguas en la memoria física, aunque para el programador se acceda mediante la sintaxis matriz[i][j].
La expresión m[i][j] es, en esencia, azúcar sintáctica para *(*(m + i) + j). Primero sumamos i a la dirección base de m para llegar al puntero de la fila, lo desreferenciamos para obtener la dirección del inicio de la fila, le sumamos j y finalmente desreferenciamos para obtener el int.
El error frecuente
Un error clásico ocurre al intentar inicializar un puntero dentro de una función de utilidad, pensando que pasar el puntero es suficiente.
/* ERROR: El cambio de puntero no persistirá fuera de la función */
void error_de_asignacion(char *s) {
s = malloc(20);
strcpy(s, "Hola");
}
int main(void) {
char *mi_string = NULL;
error_de_asignacion(mi_string);
// mi_string sigue siendo NULL. La memoria asignada en la función
// es inaccesible y causará un leak.
return 0;
}
Este error es difícil de detectar porque el código compila sin advertencias si no se usa -Wextra. mi_string recibe una copia del valor NULL. La función modifica esa copia, pero el main nunca se entera. Para corregirlo, es imperativo pasar &mi_string, lo que requiere que la firma de la función acepte char **. Herramientas como Valgrind detectarán el memory leak inmediatamente, ya que el bloque asignado en la función no tiene un puntero válido desde el ámbito de main para ser liberado.
N° 46