El lenguaje de programación C++: ¿cuándo usar referencias?

Seguimos con referencias. Ya sabemos qué son pero ¿cuándo usar las referencias?

La respuesta corta es siempre… que puedas.

La respuesta larga tiene que ver con el uso de punteros y se podría replantear de la siguiente manera ¿cuándo debo usar un puntero y cuándo una referencia?

Veamos algunos casos típicos y recomendaciones:

Referencias en el paso de parámetros

Las referencias suelen usarse en el paso de parámetros para evitar la copia de una estructura de datos grande. El paso de un objeto cualquiera a una referencia actúa como el enlace del que hablábamos en posts anteriores. Considera el siguiente código:

struct mi_gran_estructura estructura;
void f(struct mi_gran_estructura &r) {/* Utilizamos r... */}
f(estructura); // No hay copia de memoria, sino que r queda enlazado con estructura

No querer pasar el parámetro por valor puede tener dos causas:

  1. Que nos interese modificar el objeto por lo que también podríamos haber optado por un puntero.
  2. Que queramos ahorrarnos el esfuerzo de copiar el parámetro

Si además de ahorrarnos el copiar el parámetro, queríamos asegurar que no se modificase, podríamos haber especificado el parámetro como constante:

struct mi_gran_estructura estructura;
void f(const struct mi_gran_estructura &r) {/* Utilizamos r (pero no lo cambiamos)... */}
f(estructura); // No hay copia de memoria, sino que r queda enlazado con estructura

El paso de parámetro puede equipararse a saltarse la protección del alcance de una variable. Pasar un parámetro como referencia puede tratarse como si estuviéramos insertando el parámetro en un nuevo alcance: el de la función.

Así, mi consejo es:

Si vamos a pasar un parámetro grande o si vamos a pasar un parámetro para modificaro y, en cualquiera de los casos, no necesitamos aritmética de punteros, mejor usar una referencia. Además, en el caso de que no necesitemos modificar el parámetro, mejor dejarlo claro añadiendo el calificativo const a la referencia.

Referencias al devolver valores

Una función puede devolver una referencia. Estaremos enlazando la expresión de llamada de la función con la expresión de su valor de retorno. ¿Qué significa esto?

Considera el siguiente ejemplo:

struct vector {double x; double y;};
struct vector zero = {0.0, 0.0};
double &obtenY(struct vector &v)
{
    return v.y;
}
printf("(%f,%f)\n", zero.x, zero.y); // Imprime (0,0)
obtenY(zero)++;
printf("(%f,%f)\n", zero.x, zero.y); // Imprime (0,1)

Según lo dicho obtenY(zero) (expresión de llamada de la función) queda enlazada con v.y (expresión de retorno de la función), de esta manera, al hacer obtenY(zero)++, realmente estamos haciendo v.y++. Y lo que es mejor, como dentro de la función v está enlazado a zero, realmente estamos haciendo zero.y++

Este ejemplo es un poco tonto pero devolver referencias es extremadamente útil cuando trabajamos con colecciones. Cuando buscamos un elemento en una colección, nos interesa tener acceso al elemento en sí y no a una copia del mismo. El siguiente ejemplo cuenta las apariciones de los números de 0 a 9 en la entrada estándar (usando el número 99 como terminador):

#include <iostream>

using namespace std;

extern int contador = 0;

int repeticiones[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
int &numeroRepeticiones(int i)
{
    return repeticiones[i];
}

int main()
{
    // Cuenta las apariciones de números del 0 al 9
    int indice;
    cin >> indice;

    // Usa el número 99 para terminar la lista de números
    while(indice != 99) {
        if (0 <= indice && indice < 10)
           numeroRepeticiones(indice)++;
        cin >> indice;
    }

    // Imprime la colección
    for(int i=0; i < 10; i++)
        printf("El número %i se ha repetido %i vece(s)\n", i, numeroRepeticiones(i));
}

La función númeroRepeticiones localiza la posición del vector donde se guarda la cantidad de repeticiones del número indicado y no sólo devuelve este valor sino que, realmente, devuelve un acceso a esta posición concreta del vector por lo que al hacer numeroRepeticiones(indice)++, realmente estamos haciendo repeticiones[indice]++ e incrementando el valor dentro de la colección.

Ten en cuenta también que poner una referencia como expresión del return de una función no hará que se devuelva la referencia, sino el objeto al que se refiere. Por ejemplo en el siguiente fragmento:

struct vector &normalizar(struct vector &v)
{
    // Esto normaliza el vector
    double x = v.x;
    double y = v.y;
    double m = sqrt(x*x+y*y);
    v.x = x/m;
    v.y = y/m;

   return v;
}

la función no devolverá la referencia v sino el objeto al que se refería v.

Con todo esto en mente, mi recomendación es la siguiente:

Usa referencias como retorno de una función si el objetivo de esa función es exponer un miembro de una estructura de datos que quieras modificar. Este miembro puede ser desde la posición de un vector hasta una propiedad de una clase.

Advertencia en el uso de referencias y punteros

Un error muy común es ver las referencias como una especie de «salvación» o alternativa al uso de new y delete. Considera el siguiente ejemplo:

struct vector {int x; int y;} v, w;

stuct vector &nuevoVector(int x, int y)
{
    struct vector nuevo;
    nuevo.x = x; nuevo.y = y;
    return nuevo;
} // Al salir de la función, nuevo será destruido.

v = nuevoVector(0, 0);
w = nuevoVector(1, 1);

Esto no funcionará. Probablemente, lo que pase es que tras la segunda llamada a nuevoVector, v acabe valiendo lo mismo que w. Y si llamamos a otra función entonces puedes ir despidiénte del valor de ambas variables.

Lo que pasa es sencillamente que «nuevo» era una variable temporal ligada a la ejecución de la función y muere al terminar ésta por lo que ese espacio de la memoria donde estaba queda a disposición de otros objetos temporales.

Con punteros pasa exáctamente lo mismo:

struct vector {int x; int y;} *v, *w;

struct vector *nuevoVector(int x, int y)
{
    struct vector nuevo;
    nuevo.x = x; nuevo.y = y;
    return &nuevo;
} // Al salir de la función, nuevo será destruido y la dirección de nuevo no guardará nada útil

v = nuevoVector(0, 0);
w = nuevoVector(1, 1);

Así que la regla es:

No se debe devolver jamás una referencia o puntero a una variable definida en una función.

Esta norma tiene, no obstante y engañosamente, una excepción, como se ilustra en el siguiente ejemplo:

struct vector &normalizar(struct vector &v)
{
    // Esto normaliza el vector
    double x = v.x;
    double y = v.y;
    double m = sqrt(x*x+y*y);
    v.x = x/m;
    v.y = y/m;

   return v;
}

Aquí, la referencia-parámetro v se crea al comienzo de la función y se destruye al final de la misma pero ojo, lo que se destruye es la referencia, no el objeto al que se refiere que debe venir de afuera de la función. Así, estrictamente, no se está contraviniendo la regla puesto que el objeto al que se refiere v no está definido dentro de la función.

Punteros a partir de referencias

Para terminar, un resultado inmediato: como una referencia sólo sustituye al nombre con el que está enlazado, basta hacer:

&referencia

para conocer la dirección del objeto al que representa y obtener un puntero de la referencia.

Pues ya está: se acabó el tema de las referencias hasta nuevo aviso. Espero que os haya resultado útil y si quereis contribuir de cualquier manera, ¡pues a los comentarios!

3 comentarios en “El lenguaje de programación C++: ¿cuándo usar referencias?

Deja una respuesta

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Salir /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Salir /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Salir /  Cambiar )

Conectando a %s