Para entender la programación en C, debes dejar de ver las variables como simples etiquetas para guardar números y empezar a verlas como ubicaciones físicas en la memoria de tu ordenador. Un puntero es una variable cuyo valor no es un dato común (como un int o un float), sino una dirección de memoria.
Si imaginas la memoria RAM como una calle muy larga con miles de casas, cada casa tiene un número único (la dirección) y un contenido (el valor). Una variable normal es la casa misma; un puntero es un trozo de papel donde has anotado el número de una de esas casas.
Este mecanismo existe porque la CPU no se comunica con los nombres de las variables (como nombre o edad), sino con direcciones numéricas. Usar punteros te permite manipular la memoria de forma directa, lo cual es esencial cuando necesitas pasar grandes estructuras de datos a funciones sin copiarlas por completo o cuando necesitas gestionar memoria de forma dinámica durante la ejecución. Sin embargo, si te equivocas y apuntas a una dirección de memoria que no te pertenece o que no existe, el sistema operativo interrumpirá tu programa (lo que comúnmente llamamos segmentation fault) o, peor aún, corromperás datos silenciosamente.
Cuando declaras un puntero, la sintaxis puede ser confusa. Al escribir int *p;, le estás diciendo al compilador: “la variable p es de tipo puntero a int“. El asterisco * aquí forma parte del tipo de la variable. Es común ver estilos como int* p; (enfatizando que el tipo es “puntero a entero”) o int *p; (enfatizando que si desreferencias p, obtienes un int). Ambos son correctos, pero ten cuidado: si escribes int *p, q;, el compilador interpretará que p es un puntero, pero q es un entero común.
Para trabajar con ellos, necesitas dos operadores fundamentales:
1. El operador de dirección de (&): Se usa ante una variable para obtener su ubicación en la memoria.
2. El operador de indirección o desreferencia (*): Se usa ante un puntero para acceder al valor que reside en la dirección que el puntero contiene.
Existe una relación matemática directa: *(&x) es exactamente lo mismo que x. Estás obteniendo la dirección de x y luego accediendo al contenido de esa dirección.
#include <stdio.h>
int main(void) {
int numero = 42;
int *puntero_a_numero = № // Usamos & para obtener la dirección de 'numero'
// Mostramos los valores y las direcciones
// %p es el especificador de formato para direcciones de memoria (void *)
printf("Valor de 'numero': %d\n", numero);
printf("Dirección de 'numero' (&numero): %p\n", (void*)&numero);
printf("Valor guardado en 'puntero_a_numero': %p\n", (void*)puntero_a_numero);
printf("Valor al desreferenciar 'puntero_a_numero' (*puntero_a_numero): %d\n", *puntero_a_numero);
// Modificamos el valor de 'numero' a través del puntero
*puntero_a_numero = 100;
printf("\nTras modificar mediante el puntero:\n");
printf("Nuevo valor de 'numero': %d\n", numero);
printf("Nueva equivalencia: *(&numero) es %d\n", *(&numero));
return 0;
}
Desglose del ejemplo
En el código anterior, declaramos int numero = 42;, que reserva un espacio en la memoria para un entero y le asigna ese valor. Luego, la línea int *puntero_a_numero = № hace dos cosas: primero, el operador & recupera la dirección de memoria donde vive numero, y segundo, el asterisco en la declaración le dice al compilador que puntero_a_numero no guardará un 42, sino la ubicación de ese 42.
Cuando ejecutamos printf con el especificador %p, el programa imprime la dirección en formato hexadecimal. Es importante usar el cast (void*) al pasar punteros a printf para cumplir estrictamente con el estándar C y evitar advertencias del compilador.
La parte más crítica ocurre en *puntero_a_numero = 100;. Aquí, el asterisco no es una declaración, sino el operador de desreferencia. Le estamos diciendo a la CPU: “ve a la dirección de memoria que tienes guardada en puntero_a_numero y escribe un 100 en esa posición”. Como esa dirección es la misma donde vive numero, el valor de numero cambia sin que hayamos usado su nombre directamente.
El error frecuente
Un error clásico y extremadamente peligroso es intentar usar un puntero que no ha sido inicializado.
int *p; // Puntero con valor indeterminado (apunta a cualquier sitio) *p = 10; // ¡ERROR! Comportamiento indefinido (Undefined Behavior)
Cuando declaras int *p; dentro de una función sin asignarle una dirección (como p = &variable;), el puntero contiene “basura”: una dirección de memoria aleatoria que tu programa obtuvo de la pila. Al intentar escribir 10 en esa dirección con *p, podrías estar sobrescribiendo una dirección de memoria crítica del sistema operativo o de otra variable de tu programa. Esto causará que el programa se cierre abruptamente (crash) o que se comporte de manera errática. Herramientas como Valgrind o AddressSanitizer detectarán este error como un “invalid write” de inmediato.
La seguridad de tu programa depende de que cada puntero apunte a un bloque de memoria válido antes de intentar acceder a su contenido.
N° 43