Bienvenidos sean a este post, hoy hablaremos sobre un tema relacionado al post anterior.
Esto es muy similar a lo visto en el post anteriror porque al igual que un puntero este no deja de ser un alias a nuestra direccion de memoria que usaremos. Veamos como es su sintaxis:
tipo_dato & rRef_nombre = elemento;
Es muy similar a como definimos un puntero pero en lugar de usar el asterisco usamos a ampersand (&) y en lugar de asignar una direccion de memoria le pasamos un elemento, como una variable o un objeto, y por convencion se utiliza la letra r en minuscula para indicar que es una referencia, aunque esto no es obligatorio, para entender como trabaja veamos un ejemplo:
# include <iostream>
int main()
{
int intUno;
int & rUnaRef = intUno;
intUno = 5;
std::cout << "intUno:\t" << intUno << std::endl;
std::cout << "rUnaRef:\t" << rUnaRef << std::endl;
std::cout << "&intUno:\t" << &intUno << std::endl;
std::cout << "&rUnaRef:\t" << &rUnaRef << std::endl;
int intDos = 8;
rUnaRef = intDos;
std::cout << "\nintDos:\t" << intDos << std::endl;
std::cout << "intUno:\t" << intUno << std::endl;
std::cout << "rUnaRef:\t" << rUnaRef << std::endl;
std::cout << "&intUno:\t" << &intUno << std::endl;
std::cout << "&intDos:\t" << &intDos << std::endl;
std::cout << "&rUnaRef:\t" << &rUnaRef << std::endl;
return 0;
}
Codigo simple para ver como funciona. Primero, establecemos una variable y luego una referencia a este y le asignamos la variable anterior. Lo siguiente es asignar un valor a la variable. Luego mostraremos primero los valores de la variable y la referencia a este, y despues las direcciones de memoria de ambas. Definimos una nueva variable con un valor y este lo asignamos a la referencia anterior. Volvemos a repetir lo mismo que antes donde mostraremos los valores de todas las variables y la referencias. Como asi tambien las direcciones de memoria de todas. Compilemos y veamos como es su salida:
$ ./ref
intUno: 5
rUnaRef: 5
&intUno: 0x7ffd763e12c4
&rUnaRef: 0x7ffd763e12c4
intDos: 8
intUno: 8
rUnaRef: 8
&intUno: 0x7ffd763e12c4
&intDos: 0x7ffd763e12c0
&rUnaRef: 0x7ffd763e12c4
$
Aqui podemos observar en el primer caso como apuntan a la misma variable nos devuelve el mismo valor y la misma direccion de memoria. En el segundo caso vemos como uno afecto al valor del otro por la asignacion de la nueva variable a la referencia pero si observamos las asignaciones de memoria, la referencia sigue apuntando a la primer variable a pesar de que le asignamos una nueva variable. Vamos a analizar la primera ventaja de trabajar con referencias y para ello analizaremos el siguiente codigo:
#include <iostream>
void Intercambiar(int &, int &);
int main()
{
int x=5,y=10;
std::cout << "Main. Antes del intercambio, ";
std::cout << "x: " << x << " y: " << y << std::endl;
Intercambiar(x, y);
std::cout << "Main. Despues del intercambio, ";
std::cout << "x: " << x << " y: " << y << std::endl;
return 0;
}
void Intercambiar(int & rx, int & ry)
{
int temp;
std::cout << "Intercambio. Antes del mismo, ";
std::cout << "rx: " << rx << " ry: " << ry << std::endl;
temp=ry;
ry=rx;
rx=temp;
std::cout << "Intercambio. Despues del mismo, ";
std::cout << "rx: " << rx << " ry: " << ry << std::endl;
}
Este codigo se encarga de intercambiar los valores entre dos variables. Primero declaramos un prototipo de una funcion, antes de hablar del main veamos como es la definicion del prototipo.
La funcion tendra como argumentos dos referencias y sera de las variables que le pasemos al llamarla. En esta funcion primero declaramos una variable que sera la encargada de realizar el intercambio. Mostramos un mensaje indicando que esto es antes del intercambio y mostramos los valores recibidos. Nuestro siguiente paso sera asignar el valor de ry a la variable declarada al inicio. Despues a ry le asignamos el valor de rx y a rx el valor de temp, realizando el intercambio entre las variables. Para finalmente, mostrar un mensaje indicando el intercambio y los valores. En este texto indicaremos que solamente son las referencias. Pasemos a hablar sobre el main.
Lo primero que haremos es definir dos variables con un valor a cada una. Luego indicamos que estamos en el main y mostraremos los valores antes del intercambio. Llamamos a la funcion anterior y le pasamos las dos variables que definimos en main. Para mostrar nuevamente los valores intercambiados. Compilemos y veamos como es su salida:
$ ./ref
Main. Antes del intercambio, x: 5 y: 10
Intercambio. Antes del mismo, rx: 5 ry: 10
Intercambio. Despues del mismo, rx: 10 ry: 5
Main. Despues del intercambio, x: 10 y: 5
$
Observen como no solo se realizo el intercambio en la funcion sino entre las variables del main. Cual es la ventaja de trabajar de esta forma? La principal es que nos evitamos el tener que copiar la variable para pasarla a la funcion. Principalmente ahorrando espacio en memoria y tambien mejorando la performance porque nos ahorramos ciclos de computos. Pasemos a analizar el siguiente codigo:
#include <iostream>
class Gato
{
public:
Gato();
Gato(Gato &);
~Gato();
int ObtenerEdad() const { return suEdad; }
void AsignarEdad(int edad) { suEdad = edad; }
private:
int suEdad;
};
Gato::Gato()
{
std::cout << "Constructor de Gato....\n";
suEdad = 1;
}
Gato::Gato(Gato &)
{
std::cout << "Contructor de copia de Gato...\n";
}
Gato::~Gato()
{
std::cout << "Destructor de Gato...\n";
}
const Gato & FuncionDos(const Gato &);
int main()
{
std::cout << "Crear un Gato...\n";
Gato Pelusa;
std::cout << "Pelusa tiene ";
std::cout << Pelusa.ObtenerEdad();
std::cout << " años de edad.\n";
int edad = 5;
Pelusa.AsignarEdad(edad);
std::cout << "Pelusa tiene " << Pelusa.ObtenerEdad() << " años de edad.\n";
std::cout << "Llamando a FuncionDos...\n";
FuncionDos(Pelusa);
std::cout << "Pelusa tiene " << Pelusa.ObtenerEdad() << " años de edad.\n";
return 0;
}
const Gato & FuncionDos(const Gato & elGato)
{
std::cout << "FuncionDos regresando ...\n";
std::cout << "Ahora Pelusa tiene " << elGato.ObtenerEdad() << " años de edad.\n";
return elGato;
}
Primero tenemos una clase que posee una propiedad en privado y en la parte publica tenemos dos metodos para asignarle un valor y obtenerlo. Asi como tambien tenemos dos prototipos de connstructores y uno de destructor. Uno de los constructores tiene una referencia y lo convierte en algo particuular. Tanto el constructor como el destructor predeterminado tienen un mensaje que inician y eliminan al Gato, y en el caso del constructor iniciamos el valor de la propiedad suEdad. El otro constructor como lo indica es de copia. Esto es asi porque al recibir referencias del mismo tipo de la clase solo hara copias de este tipo. Antes de hablar sobre el main, veamos al prototipo y su definicion.
Esta funcion sera del tipo de la clase y sera tambien una referencia. Este recibe un argumento tambien de tipo Gato pero que sera una referencia del objeto que informemos. En el bloque mostaremos un mensaje indicando que estamos aqui y devuelve el valor obtenido mediante ObtenerEdad sobre el objeto referenciado. Y por ultimo devolvemos el objeto recibido, simplemente porque las funciones siempre devuelven el mismo tipo de dato.
En el main, creamos un objeto de tipo Gato (lo indicamos tambien). Lo siguiente es indicar el valor que tiene suEdad en el objeto anteriorrmente creado. Definimos una nueva variable con un valor y la pasamos medainte AsignarEdad para establecer un nuevo valor a suEdad. Luego mostramos el nuevo valor, llamamos a FuncionDos (la funcion anteriormente comentada), tambien mostramos un mensaje indicando el llamado, y por ultimo volvemos a mostrar el valor de suEdad en el objeto. Compilemos y veamos como es su salida:
$ ./ref
Crear un Gato...
Constructor de Gato....
Pelusa tiene 1 años de edad.
Pelusa tiene 5 años de edad.
Llamando a FuncionDos...
FuncionDos regresando ...
Ahora Pelusa tiene 5 años de edad.
Pelusa tiene 5 años de edad.
Destructor de Gato...
$
Como pueden ver siempre pudo trabajar con el objeto creado en el main, a pesar de estar referenciado en FuncionDos, y con la particularidad de que en ningun momento se utilizo al constructor agregado. Esto es asi porque la referencia que usamos nos evita la necesiadad de tener que usar constructores o destructores para manejar los distintas formas de utilizar al objeto. Esta es una clara ventaja, asi como otras que mencionamos, sobre los punteros pero las referencias tienen dos inconvenientes. La primera es que no pueden ser nulas, y la segunda es que no se pueden reasignar. Por lo tanto, si los datos a manejar pueden recibir algunas de estas situaciones no podremos usarlos, y ahi seria mas conveniente mas el uso de los punteros. Por lo general, se tiende a hacer un equilibrio en el uso entre las referencias y punteros. Tambien recuerden que el mal uso de referencias puede dejar elementos huerfanos, a diferencia de los punteros que nos brinda herramientas para eliminarlos. Antes de finalizar les dejo unos errores tipicos:
int & rUnaRef = int; // para un tipo de variable.
Gato & rGatoRef = Gato; // para un tipo de clase
Cuando debemos crear una referencia de una variable u objeto no debemos pasar el tipo de la misma sino de la siguiente manera:
int unaVariable = 200;
int & rUnaRef = unaVariable; // para un tipo
Gato Pelusa;
Gato & rGatoRef = Pelusa; // para una clase
Siempre debemos pasarle el elemento a referenciar, y como mencionamos anteriormente no debe ser null. Podemos asignar un variable vacia pero debemos asegurarnos que no sea null, y en el caso de los objetos es mas simple porque nunca sera null.
En resumen, hoy hemos visto referencias, que es, para que sirve, como se implementa, sus ventajas, los cuidados que debemos tener, asi como una serie de ejemplos para ver sus distintas conductas, desde la mas basica hasta mas complejas. Espero les haya sido de utilidad sigueme en tumblr, Twitter o Facebook para recibir una notificacion cada vez que subo un nuevo post en este blog, nos vemos en el proximo post.


Donatión
It’s for site maintenance, thanks!
$1.50
