Lo que me hubiera gustado saber sobre punteros en C++ cuando era junior
Lo que me hubiera gustado saber sobre punteros en C++ cuando era junior
Resumen: En este post te cuento, en lenguaje humano, qué son realmente los punteros en C++, por qué asustan tanto al principio y qué cosas me hubiera encantado entender desde el día uno para evitar segfaults y dolores de cabeza.
Nivel: Junior
Introducción
Si estás empezando con C++, es muy probable que la palabra puntero te produzca una mezcla de respeto y miedo.
Y no te culpo: casi todos hemos roto programas (y paciencia) por culpa de un puntero mal usado.
Cuando yo era junior, pensaba que los punteros eran algo “mágico” que había que memorizar,
en vez de entender de verdad qué estaba pasando en memoria.
En este post quiero contarte lo que me hubiera gustado saber desde el principio:
- Qué es realmente un puntero (sin humo).
- La diferencia entre una variable normal, una referencia y un puntero.
- Por qué
new,deletey el temidosegmentation faultno son brujería. - Errores típicos que todos cometemos al empezar (y cómo evitarlos).
Qué es de verdad un puntero
Un puntero no es más que una variable que guarda una dirección de memoria.
Nada más. No es un monstruo, no es una entidad oscura del compilador.
Es como un papelito donde apuntas: “el valor que buscas está en la dirección X”.
Ejemplo básico:
int x = 42; // variable normal
int* p = &x; // p es un puntero a int, guarda la dirección de x
xguarda el valor42.pguarda “dónde está x en memoria”.&xsignifica “la dirección de x”.
Para acceder al valor apuntado por p, usas el operador * (dereferencia):
std::cout << *p << std::endl; // imprime 42
Lo que me hubiera gustado que me dijeran es:
el puntero es un número (dirección), la estrella (*) es cómo vas desde esa dirección al valor real.
Variable, referencia y puntero: misma historia, distintas interfaces
Cuando eres junior, de pronto te tiran esto encima:
int x = 10;
int& ref = x;
int* ptr = &x;
Y tu cabeza dice: “¿Por qué tres formas de hacer lo mismo?”. La clave:
- Variable normal → guarda un valor.
- Referencia (
&) → es un alias de la variable, no puedes cambiar a qué “apunta”. - Puntero (
*) → guarda una dirección que SÍ puedes cambiar en tiempo de ejecución.
Ejemplo muy simplificado:
int x = 10;
int y = 20;
int& r = x; // referencia a x
int* p = &x; // puntero a x
r = 15; // modifica x, ahora x = 15
*p = 30; // también modifica x, ahora x = 30
p = &y; // ahora p apunta a y
*p = 99; // modifica y, ahora y = 99
// r no puede "reasignarse" para referenciar y, se queda con x
Lo que me hubiera ayudado mucho: pensar que las referencias son un “puntero constante implícito”,
más seguro y cómodo, mientras que los punteros son más flexibles pero también más peligrosos.
Memoria dinámica: por qué existen new y delete
Otra cosa que me hubiera encantado entender pronto:
por qué necesitamos memoria dinámica. ¿No basta con hacer int x = 5; y ya?
Las variables “normales” suelen vivir en la pila (stack):
- Se crean cuando entras a una función.
- Se destruyen automáticamente cuando sales de la función.
Pero a veces quieres que algo viva más allá de esa función:
int* crearNumero() {
int* p = new int(42);
return p; // devolvemos un puntero a memoria dinámica
}
int main() {
int* numero = crearNumero();
std::cout << *numero << std::endl; // 42
delete numero; // importante: liberar la memoria
}
Aquí la memoria se reserva en el heap con new, y no se libera sola.
Esa es la razón de ser de delete (y de las fugas de memoria cuando se nos olvida).
El clásico terror: segmentation fault
Uno de los mejores “profesores” que vas a tener en C++ es el temido segmentation fault.
Cada vez que te aparece, es básicamente el sistema operativo diciéndote:
“estás tocando memoria que no es tuya”.
Las causas típicas que me hubiera gustado tener claras desde el día uno:
- Dereferenciar punteros nulos:
int* p = nullptr; *p = 10; // BOOM - Acceder fuera de rangos en arrays.
- Usar punteros colgantes (que apuntan a memoria ya liberada).
Ejemplo de puntero colgante:
int* peligrosito() {
int x = 10;
return &x; // MALA IDEA: x se destruye al salir de la función
}
int main() {
int* p = peligrosito();
std::cout << *p << std::endl; // comportamiento indefinido
}
Aquí el puntero p parece válido, pero en realidad apunta a algo que ya no existe.
Este tipo de bug es de los más difíciles de depurar.
Lo que me hubiera gustado saber sobre buenas prácticas con punteros
1. Inicializa SIEMPRE tus punteros
No dejes punteros “a lo que haya por ahí en memoria”.
int* p = nullptr; // mejor que dejarlo sin inicializar
Y antes de usarlo:
if (p != nullptr) {
// usa p con tranquilidad razonable
}
2. Cada new debería tener su delete
Como junior, yo hacía new por todos lados y me olvidaba de delete.
Resultado: fugas de memoria como si no hubiera un mañana.
int* p = new int(5);
// ...
delete p; // si no lo haces, memoria perdida
p = nullptr; // buena práctica: evitar punteros colgantes
Más adelante, lo ideal es usar smart pointers (std::unique_ptr, std::shared_ptr)
para no tener que preocuparte tanto de esto, pero entender la base de new/delete ayuda muchísimo.
3. No uses punteros si una referencia o una variable normal es suficiente
Algo que me habría ahorrado muchos bugs:
no todo tiene que ser un puntero.
A veces una referencia o pasar un objeto por valor es más simple y más seguro.
// Mucho más legible y seguro:
void incrementar(int& x) {
x++;
}
En lugar de:
void incrementar(int* x) {
if (x != nullptr) {
(*x)++;
}
}
Los punteros tienen sentido cuando realmente necesitas esa flexibilidad:
memoria dinámica, estructuras de datos, interoperabilidad con C, etc.
4. Dibujar la memoria en papel ayuda muchísimo
Una de las cosas que más me ayudó (y que ojalá hubiera hecho antes) fue dibujar lo que está pasando:
cuadritos para variables, flechas para punteros, tachar cuando algo se destruye, etc.
Por ejemplo, para este código:
int x = 10;
int* p = &x;
int* q = p;
*q = 20;
Visualmente:
x: 20
p ----┐
v
q ----┘
Entender que p y q apuntan al MISMO sitio de memoria hace que el comportamiento del programa deje de ser “mágico”.
Punteros y arrays: otro clásico que confunde
Otra cosa que me confundió muchísimo al principio es que los arrays y los punteros están muy relacionados,
y la sintaxis no ayuda.
int arr[3] = {1, 2, 3};
int* p = arr; // arr "decáe" a puntero a su primer elemento
Aquí, p apunta a arr[0].
Entonces:
*pesarr[0].*(p + 1)esarr[1].*(p + 2)esarr[2].
Y esta es la parte divertida (y confusa):
p[0] == *p;
p[1] == *(p + 1);
Es decir, p[i] es solo azúcar sintáctico para *(p + i).
Cuando entendí esto, todo el tema de arrays + punteros empezó a tener más sentido.
Resumen: lo que realmente me hubiera ahorrado tiempo
- Un puntero solo guarda una dirección. La magia está en la dereferencia (
*). - Referencia vs puntero:
la referencia es un alias seguro; el puntero es flexible pero peligroso. - Memoria dinámica: si usas
new,
alguien tiene que hacerdelete. Si no, fuga de memoria. - Segmentation fault casi siempre es:
estás tocando memoria que no deberías (puntero nulo, colgante o fuera de rango). - No todo necesita ser un puntero: usa referencias y valores cuando puedas.
- Dibuja la memoria: papel y lápiz son tus amigos cuando estás aprendiendo.
Conclusión
Los punteros son una de las partes más temidas de C++, pero también una de las más poderosas.
Entenderlos bien te abre la puerta a estructuras de datos, optimización y a entender qué pasa “debajo del capó”
cuando programas en otros lenguajes de más alto nivel.
Si estás ahora mismo peleándote con punteros, es normal.
Nos pasa a todos. La clave es practicar con ejemplos pequeños, dibujar lo que pasa en memoria
y no tener miedo a romper cosas en tus pruebas.
En próximos posts podemos ver:
- Introducción a
std::unique_ptrystd::shared_ptr. - Cómo implementar estructuras de datos (listas, árboles) usando punteros.
- Errores típicos con punteros inteligentes y cómo evitarlos.
Recursos recomendados
- Documentación de cppreference sobre punteros y referencias.
- Tutoriales visuales de memoria en C++ en YouTube — los diagramas ayudan mucho.