Referencias
Hay quien considera que las referencias de C++ no son más que punteros constantes, que se des-referencian automáticamente. Según esto estos códigos son prácticamente equivalentes:
Con referencia:
int x = 1; int &r = x; r = 2; std::cout << r << std::endl;
Con puntero:
int x = 1; int * const r = &x; *r = 2; std::cout << *r << std::endl;
Y es cierto, prácticamente todos los usos de una referencia se pueden sustituir por un puntero. Pero esa es una visión limitada y superficial del lenguaje, pues el verdadero valor de las referencias se aprecia su uso indirecto con templates y sobrecarga de funciones y operadores.
Definición
En general es preferible considerar que una referencia es, no un puntero, sino un alias de otra expresión, que debe ser un L-valor. Es decir:
int x = 1; int &r = x;
Aquí r es un alias de x, es decir ambos nombres se refieren exactamente al mismo L-valor, y son intercambiables prácticamente en cualquier contexto. Es importante destacar que el signo = en la inicialización de una referencia no equivale en ningún caso a una asignación, sino que es la vinculación de la referencia. Y naturalmente, esta vinculación permanece inmutable durante toda la vida de la referencia.
El lado derecho de la inicialización de una referencia solo se evalúa una vez, en el instante en el que se vincula. Así:
int a[5], i=0; int &r = a[i]; i = 2; //r sigue siendo un alias de a[0], no de a[2]
Restricciones
Una referencia debe tener exactamente el mismo tipo que la expresión con la que se inicializa, posiblemente con un const añadido.
int a; int &r1 = a; //bien const int &r2 = a; //bien long &r3 = a; //error: el tipo no coincide
La expresión con la que se inicializa debe ser un L-valor, es decir, no puede ser un literal ni el resultado de un cálculo ni un objeto temporal.
Este último punto es importante, así que insistiré:
Una referencia no puede vincularse a un objeto temporal.
A no ser, claro… que la referencia sea constante. En ese caso las reglas se relajan un poco: se permite vincularla a un R-valor: si es un objeto temporal se vincula directamente, y si no lo es se crea uno inicializado con la expresión dada y la referencia se vincula a él. Incluso puede vincularse a una expresión de un tipo distinto, pero convertible, y se crea el objeto temporal automáticamente.
Veamos unos los ejemplos (cualquiera de ellos, sin const, no sería válido):
const std::string &str = "hola"; //Se crea un temporal y se vincula a él const std::string &dos = str + " mundo"; //El operator+() devuelve un temporal y se vincula directamente const double &x = 3; //De nuevo un temporal implícito
Si una referencia se vincula a un objeto temporal, la vida de este se prolonga a la de la referencia.
Usos
El uso más inmediato de las referencias es para usarlos en el paso de parámetros, ya sea para evitar la copia potencialmente costosa de un argumento, o para utilizar un parámetro como valor de salida. En el primer caso es habitual declarar el parámetro como referencia constante, para evitar modificar el argumento inadvertidamente:
void ProcesaTextos(const std::vector<std::string> &textos); void LeeFichero(const std::string &nombre, std::vector<std::string> &res);
En ambos ejemplos se evita la copia, innecesaria, de los argumentos. Algunos expertos argumenta que retornar valores usando referencias resulta confuso, pues la llamada a la función no indica de ninguna manera que el argumento va a ser modificado, y sugieren que los parámetros de retorno deben ser siempre punteros. Compárese:
void LeeFichero1(const std::string &nombre, std::vector<std::string> &res); void LeeFichero2(const std::string &nombre, std::vector<std::string> *res); //... std::vector<std::string> textos; LeeFichero1("foo", textos); //modifica textos por sorpresa LeeFichero1("foo", &textos); //el operador & hace obvia la intención
De hecho es costumbre que las funciones que reciben strings como argumentos lo hagan siempre como const std::string &.
Otro uso que me gusta especialmente es el de las referencias como abreviaturas, para simplificar expresiones complicadas. Por ejemplo, el siguiente código hipotético:
for (int i=0; i < max; ++i) { for (int j=0; j < lista[i].Usuario().NumFicheros(); ++j) std::cout << lista[i].Usuario().Fichero(j).Nombre() << std:.endl; }
Sería mucho más fácil de entender, modificar y depurar de la siguiente manera (además se pueden añadir consts a discreción):
for (int i=0; i < max; ++i) { const Usuario &usuario = lista[i].Usuario(); for (int j=0; j < usuario.NumFicheros(); ++j) { const Fichero &fichero = usuario.Fichero(j); std::cout << fichero.Nombre() << std:.endl; } }
Objetos temporales
Hay una buena razón para prohibir la vinculación de referencias a objetos temporales. Imaginemos el siguiente código:
1 2 3 4 5 | int x = 1; //... int &y = x; //... y = 3; |
Hasta aquí todo va bien, y la asignación de la línea 5 modifica a x. Pero si se cambia el tipo de la declaración de x tenemos:
1 2 3 4 5 | unsigned x = 1; //... int &y = x; //error!!! //... y = 3; |
Si se permitiera crear un temporal en la línea 3, entones la asignación de la línea 5 estaría modificando ese temporal y no el contenido de x. Es decir, la lógica del programa habría cambiado radicalmente de forma inadvertida.
Sin embargo si la referencia se declara const, el hecho de que se cree un temporal o no no resulta tan significativo, porque en cualquier caso no va a poder modificarse:
1 2 3 4 5 | unsigned x = 1; //... const int &y = x; //bien //... y = 3; //error, tanto con temporal como sin él |
Naturalmente esta situación es más habitual con una función que devuelva un valor por un argumento de tipo referencia:
void DameTexto(std::string &txt) { txt = "..."; } //... std::string x; DameTexto(x); //bien DameTexto("xxx"); //mal
El futuro
La nueva versión de C++, C++0x, incuirá una nueva variante de referencias, llamadas referencias a R-valor, que posibilitan muchas técnicas interesantes. Pero dejemos eso para más adelante…
Artículos relacionados:
- Referencias a referencias ¿Es posible en C++ declarar una referencia a referencia? Y...
- Referencias a R-valor (1ª parte) En un post anterior comentaba que en C++ no se...
- Referencias a R-valor (2ª parte) En el post anterior describía cómo las referencias a R-valor...
- L-valores y R-valores error: se requiere un l-valor como operando izquierdo de la...
- Tipos automáticos en C++0x con auto La palabra reservada auto es casi con toda seguridad la...
RSS
Hola. He oído por ahí que tus reflexiones son bastante interesantes, así que aquí tienes un seguidor
. Con tu permiso voy a enlazarte en mi blog, de lo contrario avísame y no lo hago.
Saludos!
¡Por suspuesto! Siempre es bueno saber que hay alguien al otro lado, y que no estoy escribiendo para el vacío.
Saludos.