C

C

Tutorial C: Punteros a punteros

Aprende a declarar y usar punteros a punteros en C para manipular memoria y modificar punteros desde funciones. Ejemplos prácticos y explicaciones claras.

Aprende C y certifícate

Declarar puntero a puntero con **

En C, ya hemos visto que un puntero es una variable que almacena la dirección de memoria de otra variable. Pero, ¿qué pasaría si quisiéramos tener una variable que almacene la dirección de un puntero? Para esto existen los punteros a punteros.

Un puntero a puntero es exactamente lo que su nombre indica: una variable que contiene la dirección de memoria de otro puntero. Para declarar un puntero a puntero, utilizamos dos asteriscos (**) antes del nombre de la variable.

La sintaxis básica para declarar un puntero a puntero es:

tipo **nombre_variable;

Donde:

  • tipo es el tipo de dato al que apuntará el puntero final
  • ** indica que estamos declarando un puntero a puntero
  • nombre_variable es el identificador que usaremos para referirnos a esta variable

Veamos un ejemplo sencillo:

int num = 42;       // Variable normal
int *ptr = #    // Puntero a num
int **ptrptr = &ptr; // Puntero a puntero (apunta a ptr)

En este ejemplo:

  1. num es una variable entera con valor 42
  2. ptr es un puntero que almacena la dirección de num
  3. ptrptr es un puntero a puntero que almacena la dirección de ptr

Podemos visualizar esta estructura como una cadena de referencias:

  • ptrptr → contiene la dirección de → ptr → contiene la dirección de → num → contiene el valor 42

Para entender mejor cómo funciona la memoria en este caso, veamos un diagrama conceptual:

Memoria:
+--------+      +--------+      +--------+
| ptrptr | ---> |  ptr   | ---> |  num   |
| (0x300)|      | (0x200)|      | (0x100)|
+--------+      +--------+      +--------+
  Contiene       Contiene        Contiene
   0x200          0x100            42

Podemos comprobar estos valores con un programa sencillo:

#include <stdio.h>

int main() {
    int num = 42;
    int *ptr = &num;
    int **ptrptr = &ptr;
    
    printf("Valor de num: %d\n", num);
    printf("Dirección de num: %p\n", &num);
    printf("Valor de ptr: %p\n", ptr);
    printf("Dirección de ptr: %p\n", &ptr);
    printf("Valor de ptrptr: %p\n", ptrptr);
    
    return 0;
}

Los punteros a punteros no están limitados a dos niveles. Podríamos tener punteros a punteros a punteros (usando ***), aunque en la práctica rara vez se necesitan más de dos niveles.

int num = 10;
int *ptr1 = &num;      // Puntero a entero
int **ptr2 = &ptr1;    // Puntero a puntero a entero
int ***ptr3 = &ptr2;   // Puntero a puntero a puntero a entero

Es importante tener en cuenta que cada nivel de indirección debe ser inicializado correctamente. No podemos hacer:

int **ptrptr;  // ¡Peligro! No está inicializado
*ptrptr = 10;  // Error: intentamos escribir en una dirección desconocida

Este código provocaría un comportamiento indefinido, probablemente un error de segmentación (segmentation fault), porque ptrptr no apunta a ninguna dirección válida.

La forma correcta de inicializar un puntero a puntero es asignándole la dirección de otro puntero:

int num = 10;
int *ptr = &num;     // Primero inicializamos un puntero normal
int **ptrptr = &ptr; // Luego inicializamos el puntero a puntero

Los punteros a punteros son especialmente útiles cuando necesitamos:

  • Modificar un puntero desde dentro de una función
  • Trabajar con matrices bidimensionales (arreglos de arreglos)
  • Implementar estructuras de datos complejas como listas enlazadas o árboles

En la siguiente sección, veremos cómo acceder al valor final a través de un puntero a puntero.

Acceder al valor final con **ptr

Una vez que tenemos un puntero a puntero correctamente declarado e inicializado, necesitamos saber cómo acceder al valor final al que apunta. Para esto utilizamos el operador de desreferencia * aplicado dos veces.

Cuando trabajamos con punteros a punteros, tenemos tres elementos diferentes a los que podemos acceder:

  • La dirección almacenada en el puntero a puntero
  • El puntero al que apunta
  • El valor final almacenado en la variable original

Veamos cómo acceder al valor final con un ejemplo sencillo:

#include <stdio.h>

int main() {
    int numero = 42;        // Variable original
    int *ptr = &numero;     // Puntero a la variable
    int **ptrptr = &ptr;    // Puntero al puntero
    
    // Accediendo al valor final (42)
    printf("Valor final: %d\n", **ptrptr);
    
    return 0;
}

En este ejemplo, **ptrptr nos permite acceder al valor 42 almacenado en numero. Esto ocurre porque:

  1. El primer * desreferencia ptrptr para obtener el valor de ptr
  2. El segundo * desreferencia ptr para obtener el valor de numero

Podemos visualizar este proceso de desreferencia doble como un camino que seguimos para llegar al valor final:

ptrptr → *ptrptr → **ptrptr
(dirección de ptr) → (dirección de numero) → (valor 42)

También podemos modificar el valor final a través del puntero a puntero:

**ptrptr = 100;  // Cambia el valor de 'numero' a 100

Esta instrucción sigue el mismo camino de desreferencia, pero en lugar de leer el valor, lo modifica.

Veamos un ejemplo más completo que muestra las diferentes formas de acceso:

#include <stdio.h>

int main() {
    int numero = 42;
    int *ptr = &numero;
    int **ptrptr = &ptr;
    
    printf("Dirección almacenada en ptrptr: %p\n", ptrptr);
    printf("Valor de ptr (obtenido con *ptrptr): %p\n", *ptrptr);
    printf("Valor de numero (obtenido con **ptrptr): %d\n", **ptrptr);
    
    // Modificando el valor final
    **ptrptr = 100;
    printf("Nuevo valor de numero: %d\n", numero);
    
    return 0;
}

Es importante entender que cada nivel de desreferencia nos lleva un paso más cerca del valor final:

  • ptrptr - Es la dirección del puntero ptr
  • *ptrptr - Es el valor almacenado en ptr (la dirección de numero)
  • **ptrptr - Es el valor almacenado en numero (42 inicialmente, luego 100)

Un error común es confundir los niveles de desreferencia. Por ejemplo:

int valor = 5;
int *p = &valor;
int **pp = &p;

*pp = &valor;   // Correcto: asigna la dirección de 'valor' a 'p'
**pp = 10;      // Correcto: cambia el valor de 'valor' a 10
*pp = 10;       // ¡INCORRECTO! Intenta asignar el valor 10 como dirección

En el último caso, estamos intentando asignar el valor 10 (que no es una dirección válida) a p, lo que probablemente causará un error cuando intentemos usar p más adelante.

Para evitar confusiones, podemos pensar en los asteriscos como "capas" que debemos atravesar para llegar al valor final:

  • pp - Puntero a puntero (dos capas de indirección)
  • *pp - Atravesamos una capa, obtenemos p
  • **pp - Atravesamos dos capas, obtenemos el valor de valor

Veamos un ejemplo práctico donde modificamos valores a través de diferentes niveles:

#include <stdio.h>

int main() {
    int a = 10;
    int b = 20;
    
    int *p1 = &a;
    int *p2 = &b;
    
    int **pp = &p1;  // pp apunta a p1
    
    printf("*p1 = %d\n", *p1);  // Muestra 10
    printf("*p2 = %d\n", *p2);  // Muestra 20
    printf("**pp = %d\n", **pp); // Muestra 10 (valor de a)
    
    // Cambiamos pp para que apunte a p2
    pp = &p2;
    
    printf("**pp ahora = %d\n", **pp); // Muestra 20 (valor de b)
    
    // Modificamos el valor a través de pp
    **pp = 30;
    
    printf("b ahora = %d\n", b); // Muestra 30
    
    return 0;
}

En este ejemplo, primero accedemos al valor de a a través de **pp, luego cambiamos pp para que apunte a p2 y finalmente modificamos el valor de b a través de **pp.

La capacidad de acceder y modificar valores a través de múltiples niveles de indirección es lo que hace que los punteros a punteros sean una herramienta poderosa en C, especialmente cuando necesitamos manipular estructuras de datos complejas o pasar punteros a funciones para que puedan ser modificados.

Uso básico para modificar punteros

Uno de los usos principales de los punteros a punteros es la capacidad de modificar el valor de un puntero desde dentro de una función. En C, cuando pasamos argumentos a una función, estos se pasan por valor, lo que significa que la función recibe una copia del valor original. Esto presenta un desafío cuando queremos que una función modifique el valor de un puntero.

Veamos primero qué ocurre cuando intentamos modificar un puntero usando solo un nivel de indirección:

#include <stdio.h>

void intentar_cambiar_puntero(int *ptr) {
    int otra_variable = 100;
    ptr = &otra_variable;  // Esto solo modifica la copia local de ptr
}

int main() {
    int numero = 42;
    int *ptr = &numero;
    
    printf("Antes de la función: ptr apunta a %d\n", *ptr);
    intentar_cambiar_puntero(ptr);
    printf("Después de la función: ptr sigue apuntando a %d\n", *ptr);
    
    return 0;
}

Si ejecutamos este código, veremos que el puntero ptr en main() no cambia después de llamar a la función. Esto ocurre porque la función recibe una copia del puntero, no el puntero original.

Para solucionar este problema, necesitamos usar un puntero a puntero. Veamos cómo:

#include <stdio.h>

void cambiar_puntero(int **ptr_al_puntero) {
    static int otra_variable = 100;  // Variable estática para que exista fuera de la función
    *ptr_al_puntero = &otra_variable;  // Modifica el puntero original
}

int main() {
    int numero = 42;
    int *ptr = &numero;
    
    printf("Antes: ptr apunta a %d en la dirección %p\n", *ptr, ptr);
    cambiar_puntero(&ptr);  // Pasamos la dirección del puntero
    printf("Después: ptr apunta a %d en la dirección %p\n", *ptr, ptr);
    
    return 0;
}

En este ejemplo:

  1. Pasamos la dirección del puntero ptr a la función usando &ptr
  2. La función recibe esta dirección como un puntero a puntero (int **ptr_al_puntero)
  3. Dentro de la función, usamos *ptr_al_puntero para acceder y modificar el puntero original

Este patrón es muy común cuando necesitamos que una función modifique un puntero o cuando trabajamos con estructuras de datos dinámicas.

Ejemplo práctico: Asignación dinámica de memoria

Un caso de uso común para punteros a punteros es cuando queremos que una función asigne memoria dinámicamente:

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

// Función que crea un array dinámico de enteros
void crear_array(int **ptr, int tamanio) {
    // Asignamos memoria para el array
    *ptr = (int*)malloc(tamanio * sizeof(int));
    
    // Inicializamos el array con valores
    if (*ptr != NULL) {
        for (int i = 0; i < tamanio; i++) {
            (*ptr)[i] = i * 10;
        }
    }
}

int main() {
    int *mi_array = NULL;  // Inicialmente no apunta a nada
    int tamanio = 5;
    
    crear_array(&mi_array, tamanio);
    
    // Verificamos si la asignación fue exitosa
    if (mi_array != NULL) {
        printf("Array creado con éxito:\n");
        for (int i = 0; i < tamanio; i++) {
            printf("mi_array[%d] = %d\n", i, mi_array[i]);
        }
        
        // Liberamos la memoria cuando ya no la necesitamos
        free(mi_array);
    } else {
        printf("Error al crear el array\n");
    }
    
    return 0;
}

En este ejemplo, la función crear_array recibe un puntero a puntero (int **ptr) y modifica el puntero original para que apunte al bloque de memoria recién asignado.

Intercambio de punteros

Otro uso común es intercambiar punteros:

#include <stdio.h>

void intercambiar_punteros(int **a, int **b) {
    int *temp = *a;
    *a = *b;
    *b = temp;
}

int main() {
    int x = 10, y = 20;
    int *p1 = &x;
    int *p2 = &y;
    
    printf("Antes del intercambio:\n");
    printf("p1 apunta a %d\n", *p1);
    printf("p2 apunta a %d\n", *p2);
    
    intercambiar_punteros(&p1, &p2);
    
    printf("\nDespués del intercambio:\n");
    printf("p1 ahora apunta a %d\n", *p1);
    printf("p2 ahora apunta a %d\n", *p2);
    
    return 0;
}

En este ejemplo, la función intercambiar_punteros recibe las direcciones de dos punteros y los intercambia, haciendo que cada uno apunte a la variable a la que apuntaba el otro.

Implementación de estructuras de datos

Los punteros a punteros son fundamentales para implementar estructuras de datos como listas enlazadas. Por ejemplo, para eliminar un nodo de una lista enlazada:

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

// Definición de un nodo de lista enlazada
typedef struct Nodo {
    int dato;
    struct Nodo *siguiente;
} Nodo;

// Función para eliminar el primer nodo que contiene un valor específico
void eliminar_nodo(Nodo **cabeza, int valor) {
    // Puntero para recorrer la lista
    Nodo *actual = *cabeza;
    Nodo *anterior = NULL;
    
    // Buscar el nodo con el valor especificado
    while (actual != NULL && actual->dato != valor) {
        anterior = actual;
        actual = actual->siguiente;
    }
    
    // Si encontramos el nodo
    if (actual != NULL) {
        // Caso especial: es el primer nodo
        if (anterior == NULL) {
            *cabeza = actual->siguiente;  // Modificamos el puntero original
        } else {
            anterior->siguiente = actual->siguiente;
        }
        
        free(actual);  // Liberamos la memoria del nodo
        printf("Nodo con valor %d eliminado\n", valor);
    } else {
        printf("Valor %d no encontrado en la lista\n", valor);
    }
}

// Función auxiliar para crear un nuevo nodo
Nodo* crear_nodo(int dato) {
    Nodo* nuevo = (Nodo*)malloc(sizeof(Nodo));
    if (nuevo != NULL) {
        nuevo->dato = dato;
        nuevo->siguiente = NULL;
    }
    return nuevo;
}

// Función para imprimir la lista
void imprimir_lista(Nodo *cabeza) {
    printf("Lista: ");
    while (cabeza != NULL) {
        printf("%d -> ", cabeza->dato);
        cabeza = cabeza->siguiente;
    }
    printf("NULL\n");
}

int main() {
    // Creamos una lista simple: 10 -> 20 -> 30 -> NULL
    Nodo *lista = NULL;
    
    // Agregamos nodos al inicio (para simplificar)
    Nodo *nuevo = crear_nodo(30);
    nuevo->siguiente = lista;
    lista = nuevo;
    
    nuevo = crear_nodo(20);
    nuevo->siguiente = lista;
    lista = nuevo;
    
    nuevo = crear_nodo(10);
    nuevo->siguiente = lista;
    lista = nuevo;
    
    printf("Lista original:\n");
    imprimir_lista(lista);
    
    // Eliminamos un nodo
    eliminar_nodo(&lista, 20);
    
    printf("\nLista después de eliminar:\n");
    imprimir_lista(lista);
    
    // Liberamos la memoria de la lista
    while (lista != NULL) {
        Nodo *temp = lista;
        lista = lista->siguiente;
        free(temp);
    }
    
    return 0;
}

En este ejemplo, la función eliminar_nodo recibe un puntero a puntero a la cabeza de la lista (Nodo **cabeza). Esto le permite modificar el puntero original si es necesario eliminar el primer nodo.

Consideraciones importantes

Cuando trabajamos con punteros a punteros, debemos tener en cuenta algunas consideraciones de seguridad:

  • Siempre verificar que los punteros no sean NULL antes de desreferenciarlos
  • Tener cuidado con los niveles de indirección para evitar accesos incorrectos a memoria
  • Asegurarse de liberar correctamente la memoria asignada dinámicamente para evitar fugas de memoria

Los punteros a punteros son una herramienta poderosa en C, pero requieren un manejo cuidadoso para evitar errores difíciles de depurar.

CONSTRUYE TU CARRERA EN IA Y PROGRAMACIÓN SOFTWARE

Accede a +1000 lecciones y cursos con certificado. Mejora tu portfolio con certificados de superación para tu CV.

30 % DE DESCUENTO

Plan mensual

19.00 /mes

13.30 € /mes

Precio normal mensual: 19 €
63 % DE DESCUENTO

Plan anual

10.00 /mes

7.00 € /mes

Ahorras 144 € al año
Precio normal anual: 120 €
Aprende C online

Todas las lecciones de C

Accede a todas las lecciones de C y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Accede GRATIS a C y certifícate

En esta lección

Objetivos de aprendizaje de esta lección

  • Comprender qué es un puntero a puntero y cómo declararlo en C.
  • Aprender a acceder y modificar el valor final a través de múltiples niveles de desreferencia.
  • Entender la utilidad de punteros a punteros para modificar punteros dentro de funciones.
  • Aplicar punteros a punteros en ejemplos prácticos como asignación dinámica, intercambio de punteros y estructuras de datos enlazadas.
  • Reconocer las consideraciones de seguridad y buenas prácticas al trabajar con punteros a punteros.