Una referencia (&) es como un puntero constante que se dereferencia automáticamente. Normalmente se utiliza en la lista de argumentos y en el valor de retorno de las funciones. Pero también se puede crear una referencia independiente. Por ejemplo:
//: C11:FreeStandingReferences.cpp #include <iostream> using namespace std; // Ordinary free-standing reference: int y; int& r = y; // When a reference is created, it must // be initialized to a live object. // However, you can also say: const int& q = 12; // (1) // References are tied to someone else's storage: int x = 0; // (2) int& a = x; // (3) int main() { cout << "x = " << x << ", a = " << a << endl; a++; cout << "x = " << x << ", a = " << a << endl; } ///:~
Listado 11.1. C11/FreeStandingReferences.cpp
En la línea (1) el compilador asigna la cantidad necesaria de memoria, la inicializa con el valor 12, y liga la referencia a esa porción de memoria. Lo importante es que una referencia debe estar ligada a la memoria de alguien. Cuando se accede a una referencia, se está accediendo a esa memoria. Así pues, si escribe las líneas (2) y (3) incrementará x cuando se incremente a, tal como se muestra en el main(). Lo más fácil es pensar que una referencia es como un puntero de lujo. Una ventaja de este «puntero» es que nunca hay que preguntarse si ha sido inicializado (el compilador lo impone) ni cómo dereferenciarlo (el compilador lo hace).
Hay que seguir unas determinadas reglas cuando se utilizan referencias:
La referencia debe ser inicializada cuando se crea. (Los punteros pueden inicializarse en cualquier momento).
Una vez que se inicializa una referencia, ligándola a un objeto, no se puede ligar a otro objeto. (Los punteros pueden apuntar a otro objeto en cualquier momento).
No se pueden tener referencias con valor nulo. Siempre ha de suponer que una referencia está conectada a un trozo de memoria ya asignada.
El lugar más común en el que verá referencias es en los argumentos y valor de retorno de las funciones. Cuando se utiliza una referencia como un argumento de una función, cualquier cambio realizado en la referencia dentro de la función se realizará realmente sobre el argumento fuera de la función. Por supuesto que podría hacer lo mismo pasando un puntero como argumento, pero una referencia es sintácticamente más clara. (Si lo desea, puede pensar que una referencia no es más que una comodidad sintáctica).
Si una función retorna una referencia, ha de tener el mismo cuidado que si la función retornara un puntero. La referencia que se devuelva debe estar ligada a algo que no sea liberado cuando la función retorne. Si no, la referencia se referirá a un trozo de memoria sobre el que ya no tiene control.
He aquí un ejemplo:
//: C11:Reference.cpp // Simple C++ references int* f(int* x) { (*x)++; return x; // Safe, x is outside this scope } int& g(int& x) { x++; // Same effect as in f() return x; // Safe, outside this scope } int& h() { int q; //! return q; // Error static int x; return x; // Safe, x lives outside this scope } int main() { int a = 0; f(&a); // Ugly (but explicit) g(a); // Clean (but hidden) } ///:~
Listado 11.2. C11/Reference.cpp
La llamada a f() no tiene la comodidad ni la claridad del uso de referencias, pero está claro que se está pasando una dirección mediante un puntero. En la llamada a g(), también se pasa una dirección (mediante una referencia), pero no se ve.
El argumento referencia en Reference.cpp funciona solamente en caso de que el argumento no sea un objeto constante. Si fuera un objeto constante, la función g() no aceptaría el argumento, lo cual es positivo porque la función modificaría el argumento que está fuera de la función. Si sabe que la función respetará la constancia de un objeto, el hecho de que el argumento sea una referencia constante permitirá que la función se pueda utilizar en cualquier situación. Esto significa que para tipos predefinidos, la función no modificará el argumento, y para tipos definidos por el usuario, la función llamará solamente a métodos constantes, y no modificará ningún atributo público.
La utilización de referencias constantes en argumentos de funciones es especialmente importante porque una función puede recibir un objeto temporal. Éste podría haber sido creado como valor de retorno de otra función o explícitamente por el usuario de la función. Los objetos temporales son siempre constantes. Por eso, si no utiliza una referencia constante, el compilador se quejará. Como ejemplo muy simple:
//: C11:ConstReferenceArguments.cpp // Passing references as const void f(int&) {} void g(const int&) {} int main() { //! f(1); // Error g(1); } ///:~
Listado 11.3. C11/ConstReferenceArguments.cpp
La llamada a f(1) provoca un error en tiempo de compilación porque el compilador debe crear primero una referencia. Lo hace asignando memoria para un int, inicializándolo a uno y generando la dirección de memoria para ligarla a la referencia. La memoria debe ser constante porque no tendría sentido cambiarla: nunca podrá volver a acceder a ella. Debe hacer la misma suposición para todos los objetos temporales: son inaccesibles. Es importante que el compilador le diga cuándo está intentando cambiar algo de este estilo porque podría perder información.
En C, si desea modificar el contenido del puntero en sí en vez de modificar a lo que apunta, la declaración de la función sería:
void f(int**);
y tendría que tomar la dirección del puntero cuando se llamara a la función:
int i = 47; int* ip = &i; f(&ip);
La sintaxis es más clara con las referencias en C++. El argumento de la función pasa a ser una referencia a un puntero, y ya no hay que tomar la dirección de ese puntero.
//: C11:ReferenceToPointer.cpp #include <iostream> using namespace std; void increment(int*& i) { i++; } int main() { int* i = 0; cout << "i = " << i << endl; increment(i); cout << "i = " << i << endl; } ///:~
Listado 11.4. C11/ReferenceToPointer.cpp
Al ejecutar este programa se observa que el puntero se incrementa en vez de incrementar a lo que apunta.