Dos formas de referirse a algo
Ya conoces los punteros: variables que almacenan una dirección y te permiten alcanzar el objeto que vive ahí. Las referencias son la otra herramienta que C++ te ofrece para trabajar de forma indirecta con un objeto existente. Se solapan lo suficiente como para que los principiantes no sepan cuál elegir, así que esta página las pone una al lado de la otra.
La versión corta: una referencia es un alias. Una vez que se ejecuta int& r = x;, r es x: el mismo objeto, otro nombre. Un puntero es un objeto independiente que casualmente guarda la dirección de otro. Esa única diferencia condiciona todo lo demás.
Una referencia es un alias
Una referencia debe quedar ligada a un objeto en el momento en que se crea, y a partir de ahí cada uso de la referencia toca el original.
Fíjate en que no hay ningún * para desreferenciar ni ningún & para "tomar la dirección" en el punto de uso: lees y escribes alias exactamente como un int normal. El & en int& alias forma parte del tipo, no es el operador de dirección.
En qué se diferencian
Los comportamientos de abajo son la razón misma por la que existen ambas herramientas. Esta es la tabla que hay que memorizar.
// reference pointer
// must be initialized? yes no (but should be)
// can be null? no yes (nullptr)
// can be reseated? no yes
// pointer arithmetic? no yes
// syntax to use it just the name *p or p->member
// taking address &ref == &original &p is the pointer's own address
Dos de estas son las que más confunden. Primero, una referencia nunca puede reasignarse: asignarle algo copia un valor dentro del objeto referido, no apunta la referencia a algo nuevo.
Un puntero, en cambio, es libre de apuntar a otro lugar en cualquier momento:
Segundo, una referencia nunca puede ser legalmente nula, mientras que un puntero sí. Eso hace que "ningún valor" sea representable con un puntero pero no con una referencia, una propiedad en la que te apoyarás constantemente.
Elegir en los parámetros de función
Aquí es donde más aparece la elección. Cuando una función necesita leer o modificar el objeto de quien la llama, ambas funcionan, pero transmiten una intención distinta.
La versión con referencia (addTax(cart)) es imposible de llamar con "nada", así que dentro de la función nunca compruebas si es nula: el objeto está garantizado. La versión con puntero (applyDiscount(&cart)) anuncia en el punto de llamada, mediante el &, que el argumento podría cambiar, y permite a quien llama pasar nullptr para indicar "no aplica". Elige la que tenga la garantía que encaje con tu función.
Para parámetros de solo lectura de tipos grandes, la opción idiomática es const T&: evita una copia y promete no modificar. Consulta parámetros de función para más sobre paso por valor frente a paso por referencia.
Una regla práctica sencilla
Cuando tengas dudas, opta por defecto por una referencia y solo pásate a un puntero cuando necesites una capacidad que la referencia no tiene:
- Usa una referencia cuando el objeto siempre exista y su identidad nunca cambie: el caso común de los parámetros de función y los alias.
- Usa un puntero cuando se cumpla cualquiera de estas condiciones:
- "Nada" es un estado válido (argumento opcional, una búsqueda que puede no encontrar coincidencia): un puntero puede ser
nullptr. - Necesitas apuntar a distintos objetos con el tiempo: un puntero puede reasignarse.
- Estás gestionando memoria del montón que liberarás con
delete, o recorriendo un arreglo con aritmética de punteros.
- "Nada" es un estado válido (argumento opcional, una búsqueda que puede no encontrar coincidencia): un puntero puede ser
Si nada de eso aplica, una referencia es la opción más limpia y segura, porque el compilador garantiza por ti "siempre válida, nunca reasignada".
Errores comunes que evitar
- Esperar que
ref = otherreasigne. En su lugar asigna un valor dentro del objeto referido. Las referencias quedan ligadas de por vida; si necesitas reasignar, usa un puntero. - Devolver una referencia (o puntero) a una variable local.
int& f() { int x = 5; return x; }devuelve una referencia colgante:xmuere cuandofretorna y usar el resultado es comportamiento indefinido. La misma trampa afecta a los punteros (return &x;). - Falsificar una "referencia nula". Escribir
int& r = *p;cuandopesnullptres comportamiento indefinido en el instante en que desreferencias, no una referencia "vacía" segura. Expresa la opcionalidad con un puntero ostd::optional. - Recurrir a un puntero por costumbre. Si el argumento siempre existe y nunca lo reasignarás, una referencia elimina toda una categoría de comprobaciones de nulo y de fallos. No pagues por capacidades que no usas.
Siguiente: Memoria dinámica
Hasta ahora cada objeto al que has referenciado o apuntado se creaba automáticamente en la pila. La siguiente página, memoria dinámica, cubre new y delete: pedir memoria al sistema operativo en tiempo de ejecución, por qué son los punteros (no las referencias) quienes la poseen, y cómo olvidarse de liberarla provoca fugas.
Preguntas frecuentes
¿Cuál es la diferencia entre una referencia y un puntero en C++?
Una referencia es un alias de un objeto existente: debe inicializarse, nunca puede ser nula y nunca puede pasar a referirse a un objeto distinto después. Un puntero es una variable independiente que guarda una dirección: puede ser nulo, puede reasignarse para apuntar a otro lugar y admite aritmética de punteros. Usa la sintaxis & con las referencias y */-> con los punteros.
¿Cuándo debería usar un puntero en lugar de una referencia en C++?
Usa un puntero cuando "nada" sea un estado válido (un argumento opcional, un resultado no encontrado), cuando necesites reasignarlo para apuntar a distintos objetos con el tiempo, o cuando seas dueño de memoria del montón que vas a liberar con delete. Usa una referencia cuando el objeto siempre exista y nunca cambie de identidad, lo que cubre la mayoría de los parámetros de función.
¿Puede una referencia ser nula en C++?
No. Una referencia válida siempre se refiere a un objeto real, así que nunca compruebas si es nula. Si creas una referencia a partir de un puntero nulo desreferenciado (int& r = *p; donde p es nulo), obtienes comportamiento indefinido, no una referencia nula. Cuando necesites expresar "quizá nada", usa un puntero o std::optional.