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ícateDeclarar 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 punteronombre_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:
num
es una variable entera con valor 42ptr
es un puntero que almacena la dirección denum
ptrptr
es un puntero a puntero que almacena la dirección deptr
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 = #
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 = # // 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 = # // 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 = № // 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:
- El primer
*
desreferenciaptrptr
para obtener el valor deptr
- El segundo
*
desreferenciaptr
para obtener el valor denumero
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 = №
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 punteroptr
*ptrptr
- Es el valor almacenado enptr
(la dirección denumero
)**ptrptr
- Es el valor almacenado ennumero
(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, obtenemosp
**pp
- Atravesamos dos capas, obtenemos el valor devalor
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 = №
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 = №
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:
- Pasamos la dirección del puntero
ptr
a la función usando&ptr
- La función recibe esta dirección como un puntero a puntero (
int **ptr_al_puntero
) - 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.
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.
Introducción A C
Introducción Y Entorno
Primer Programa En C
Introducción Y Entorno
Estructura Básica De Un Programa En C
Sintaxis
Operadores Y Expresiones
Sintaxis
Control De Flujo
Sintaxis
Arrays Y Manejo De Cadenas
Sintaxis
Arrays Unidimensionales
Sintaxis
Ámbito De Variables
Sintaxis
Paso De Parámetros
Sintaxis
Entrada Y Salida Básica
Sintaxis
Variables Y Tipos De Datos
Sintaxis
Recursividad
Sintaxis
Control Iterativo
Sintaxis
Control Condicional
Sintaxis
Funciones
Punteros
Punteros
Punteros
Gestión De Memoria Dinámica
Punteros
Aritmética De Punteros
Punteros
Punteros Y Arrays
Punteros
Punteros A Punteros
Punteros
Punteros Y Funciones
Punteros
Memoria Estática Vs Dinámica
Gestión De Memoria
Gestión Segura De Memoria
Gestión De Memoria
Arrays Dinámicos
Gestión De Memoria
Estructuras En C
Estructuras Y Uniones
Uniones Y Enumeraciones
Estructuras Y Uniones
Typedef Y Y Organización De Código
Estructuras Y Uniones
Uniones
Estructuras Y Uniones
Creación De Structs
Estructuras Y Uniones
Enumeraciones
Estructuras Y Uniones
Estructuras Anidadas
Estructuras Y Uniones
Archivos
Io Y Archivos
E/s Binaria Y Formateo
Io Y Archivos
Manipulación Avanzada De Cadenas
Io Y Archivos
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.