Una referencia (&) es como un puntero constante que se de-referencia automáticamente. Normalmente se utiliza en la lista de argumentos y en el valor de retorno de de las funciones. Pero también se puede hacer una referencia que apunte a algo que no ha sido asignado. 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 linea (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
lineas (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. La ventaja de este
«puntero» es que nunca hay que preguntarse si ha
sido inicializado (el compilador lo impone) o si hay que
destruirlo (el compilador lo hace).
Hay que seguir unas determinadas reglas cuando se utilizan referencias:
La referencia de 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 una 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 es, nada más y nada menos, más conveniente sintácticamente).
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 ventaja ni la
claridad que la utilización 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á las
constancia 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 modificara 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 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
, iniciánlizá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 cambiarlo: no puede cambiarse de
nuevo. Puede 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 de una referencia a un puntero, y así no ha de manejar la dirección del 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.