Menu

Punteros en C++: operador de dirección, desreferencia y nullptr

Los punteros de C++ explicados desde cero: cómo declarar un puntero, los operadores & (dirección de) y * (desreferencia), nullptr, punteros a arreglos y los errores con punteros colgantes y no inicializados que provocan fallos.

Esta página incluye editores ejecutables: edita, ejecuta y ve el resultado al instante.

Una variable que guarda una dirección

Cada variable vive en algún lugar de la memoria, en una posición numerada llamada su dirección. La mayoría de las veces no te importa dónde: simplemente usas el nombre de la variable. Un puntero le da la vuelta a esto: es una variable cuyo valor es una dirección. En lugar de guardar 42, guarda "el lugar donde está almacenado 42".

Esta indirección es lo que hace poderosos a los punteros. Las funciones pueden modificar una variable del llamador a través de uno, estructuras de datos como las listas enlazadas encadenan nodos con ellos y (como verás en memoria dinámica) son la forma de acceder a la memoria que reservas en tiempo de ejecución.

El & en &score es el operador dirección de: produce la ubicación de score. El * en *p es el operador desreferencia: sigue la dirección hasta el valor que vive allí.

Los dos operadores: & y *

Lo más confuso para quienes empiezan es que * significa dos cosas distintas según dónde aparezca. Ten esto claro:

int* p;     // DECLARACIÓN: "p es un puntero a int"
p = &x;     // & = dirección de: guarda la dirección de x en p
int y = *p; // * = desreferencia: lee el valor al que apunta p
*p = 99;    // desreferencia a la izquierda: escribe a través del puntero

En una declaración, * forma parte del tipo. En una expresión, * hace trabajo. Una vez configurado un puntero, desreferenciarlo te da acceso completo de lectura/escritura a la variable original:

Fíjate en que nunca tocaste health por su nombre después de la línea 1, y sin embargo su valor siguió cambiando. Esa es la idea central: hp es un alias del mismo almacenamiento. El espaciado (int* p, int *p, int*p) es cosmético e idéntico para el compilador; esta guía usa int* p.

nullptr: apuntar a nada

Un puntero que no apunta a ningún sitio debería ponerse a nullptr (C++11). Es una forma clara y con seguridad de tipos de decir "todavía sin destino", y te da algo contra lo que comprobar antes de desreferenciar.

Prefiere nullptr sobre la antigua macro NULL o un 0 pelado. Como nullptr tiene un tipo de puntero real, nunca se interpreta mal como el entero 0 durante la resolución de sobrecargas, un error sutil que el estilo antiguo podía provocar.

Cuidado: la desreferencia nula. Leer o escribir a través de un puntero nulo (o sin inicializar) es comportamiento indefinido, normalmente un fallo instantáneo:

int* p = nullptr;
cout << *p;   // FALLO - desreferenciar nulo es comportamiento indefinido

Protégete siempre con if (p) (o if (p != nullptr)) antes de desreferenciar cualquier cosa que pueda ser nula.

Punteros y arreglos

El nombre de un arreglo decae a un puntero a su primer elemento, por lo que punteros y arreglos están profundamente entrelazados. Sumar 1 a un puntero no añade un byte: avanza un elemento, que es lo que hace funcionar la aritmética de punteros:

p[i] y *(p + i) son literalmente la misma expresión; esa equivalencia es la razón por la que los arreglos empiezan en el índice cero. El error clásico aquí es pasarse del final: nums + 4 es un marcador válido de uno-más-allá-del-final con el que comparar, pero desreferenciar *(nums + 4) lee fuera de los límites. Los errores de uno en uno con punteros son una de las causas principales de fallos y corrupción silenciosa, así que sé deliberado con tu condición de parada.

const y punteros

const puede aplicarse a lo que el puntero apunta, al puntero en sí, o a ambos. Lee la declaración de derecha a izquierda para descifrarla:

const int* p;        // puntero a const int  - no se puede cambiar *p, se puede reapuntar p
int* const p = &x;   // puntero const a int  - se puede cambiar *p, no se puede reapuntar p
const int* const p = &x; // ambos bloqueados

Esto importa constantemente en el código real. Una función que promete no modificar tus datos recibe un puntero a const:

Marcar como const aquello a lo que se apunta documenta la intención y permite al compilador detener escrituras accidentales: seguridad gratis y sin coste en tiempo de ejecución.

La gran trampa: punteros colgantes

Un puntero colgante apunta a memoria que ya no contiene el valor que esperas: la variable salió de su ámbito o la memoria se liberó. Desreferenciarlo es comportamiento indefinido, y lo peor es que a menudo parece funcionar hasta que deja de hacerlo.

int* makeBad() {
    int local = 5;
    return &local;   // ERROR: local muere cuando la función retorna
}                    // el puntero devuelto ahora queda colgante

La dirección sigue siendo un número válido, pero apunta a un hueco de la pila que ya fue reclamado: leerlo da basura o provoca un fallo. Lo mismo ocurre si conservas un puntero a un objeto del heap deleteado o a un elemento de un vector que después se realoja.

Tres reglas te mantienen a salvo:

  • Nunca devuelvas la dirección de una variable local. Devuelve por valor, o haz que el llamador sea dueño del almacenamiento.
  • Pon un puntero a nullptr después de que desaparezca aquello a lo que apunta, y comprueba antes de usarlo.
  • Para la propiedad y los tiempos de vida, recurre a los punteros inteligentes en lugar de new/delete crudos: liberan la memoria automáticamente y reducen toda esta clase de errores.

Siguiente: referencias frente a punteros

Los punteros no son la única forma de referirse indirectamente a otra variable. C++ también tiene referencias, que se sienten parecidas pero no pueden ser nulas, no pueden reasignarse y usan una sintaxis más limpia. A continuación las pondremos lado a lado en referencias frente a punteros para que sepas exactamente qué herramienta elegir, y por qué la mayor parte del C++ moderno prefiere las referencias cuando puede usarlas.

Preguntas frecuentes

¿Qué es un puntero en C++?

Un puntero es una variable que almacena la dirección de memoria de otro valor en lugar del valor en sí. Lo declaras con * (por ejemplo, int* p), obtienes una dirección con el operador & (p = &x) y lees o escribes el valor al que apunta desreferenciándolo con *p.

¿Cuál es la diferencia entre & y * en los punteros de C++?

En un contexto de punteros, & es el operador dirección de: &x te da la dirección de x. * cumple dos funciones: en una declaración (int* p) marca la variable como puntero, y en una expresión (*p) desreferencia el puntero para llegar al valor almacenado en esa dirección.

¿Qué es nullptr en C++ y por qué usarlo en lugar de NULL?

nullptr es un literal de puntero nulo con seguridad de tipos añadido en C++11. Significa "no apunta a nada". Prefiérelo sobre el antiguo NULL o 0 porque nullptr tiene un tipo de puntero real, así que nunca se confunde con un entero durante la resolución de sobrecargas. Comprueba siempre if (p) antes de desreferenciar: desreferenciar un puntero nulo es comportamiento indefinido.

Coddy programming languages illustration

Aprende a programar con Coddy

COMENZAR