Con las referencias C++ se convierte en un lenguaje que puede pasar un objeto como argumento a una función, y puede hacerlo de dos maneras: por valor o por referencia. No hay que llevarse a engaños, en C en C++ o en cualquier lenguaje de programación las cosas siempre se pasan por valor, el paso por referencia es una excusa que utiliza el paso del valor de la dirección de memoria para utilizar el objeto. Veamos el siguiente código de ejemplo escrito en C:
void main() {
int a = 5;
imprimeValor(a);
}
void imprimeValor (int valor) {
printf (“valor: %d”, valor);
}
Cuando se pasa la variable «a» se está pasando el valor de la variable, en este caso 5. Ahora veamos la siguiente porción de código.
void main() {
int a = 5;
imprimeValor(&a);
}
void imprimeValor (int* valor) {
printf (“valor: %d”,*a);
}
Esta es la forma que tiene C de pasar las referencias. En realidad se está pasando algo por valor, pero lo que se pasa es el valor de la dirección de memoria de la variable «a». Por este motivo hay que usar el operador * para acceder al valor en sí.
C++ trae consigo una forma de hacer un paso por referencia de modo más directo sin necesidad de utilizar los operadores de punteros para acceder a la zona de memoria. Se hace declarando la variable con el operador &. Este operador ya existía en C pero siempre se usaba anteponiéndolo a la variable y daba la dirección de memoria de la misma, C++ reutiliza este operador en la declaración de una variable para indicar que se está creando un referencia de ese tipo.
Una versión del código anterior usando las referencias en C++ es la siguiente:
void main() {
int a = 5;
imprimeValor(a);
}
void imprimeValor (int& valor) {
printf (“valor: %d”, valor);
}
Una vez llegado a este punto se puede pensar que el paso por referencia en C++ está para evitar el incómodo uso del operador *. Bueno, si de verdad se desea utilizarlo de esta manera allá cada uno, pero existen otros motivos que deben considerarse.
Imaginemos que se desea implementar un sistema que recibe mensajes, los cambia de formato y los encamina hacia otra red de forma asíncrona. Esto quiere decir que se sabe cuando el mensaje entra en el sistema, pero no cuando es enviado. Este sistema va a estar compuesto por tres clases. La clase que recibe y procesa el mensaje generando el mensaje de red, la clase que envía el nuevo mensaje por la red y la clase que lee la configuración de la conexión de envío y la ofrece.
Una primera implementación utilizando la forma de utilización de objetos con punteros (por añoranza del C) puede ser la siguiente:
class ConfiguracionRed {
int getPuertoConexion();
std::string getDireccionConexion();
}
class EnviadorRed {
void abrirConexion(std::string* direccion, int puerto);
void envia(std::string* mensajeRedFinal);
}
class PuenteEntreRedes {
void setConfiguracionEnvio(ConfiguracionRed* configuracionRed);
void setEnviadorRed(EnviadorRed* enviadorRed);
void enviaMensajeRed(MensajeRedInicial* mensajeRedInicial);
}
Como se puede observar todas las inyecciones de dependencias que se establecen se hacen mediante el uso de punteros. Ahora bien, se puede dar el caso (Dios no lo quiera) que dentro de la clase PuenteEntreRedes se haga una liberación de memoria de cada uno de los objetos que se le inyectan; nada lo impide. Esta mala práctica de programación seguro que no se le ha ocurrido a nadie porque todo el mundo aplica la máxima de «quien crea un objeto tiene la responsabilidad de liberarlo», en este caso hacer la liberación tiene que ser el cometido del contenedor de la aplicación; aunque como se ha indicado antes nada impide que no se libere la memoria en otro lado.
Hemos de caer en la cuenta cada vez que usemos punteros para manejar objetos, que al pasar el puntero al objeto se está dando permiso al receptor del puntero para liberar la memoria asignada, esto si no se encuentra bajo un control exhaustivo que se está convirtiendo en una fuente de «violaciones de segmento» en potencia. En el sistema que estamos modelando no se va a liberar la memoria de los objetos en ningún caso excepto en el contenedor de la aplicación y en MensajeRedInicial.
MensajeRedInicial es el mensaje que se quiere enviar entre redes, al ser el envío de forma asíncrona el sistema pierde el control del ciclo de vida del cuando se le pasa como parámetro al método enviaMensajeRed(). Matizando un poco lo de envío asíncrono, quiere decir que cuando se devuelve el control a quien llama al método enviaMensajeRed() el mensaje puede no haber sido enviado todavía. Es por este motivo por el que se le pasa el puntero del objeto, para que cuando se termine de enviar el mensaje se libere la memoria del mismo.
Cuando se pasa un puntero de un objeto se está pasando implícitamente el permiso de liberación del objeto en sí, para evitar este hecho es preferible pasar una referencia del objeto.
Veamos una nueva versión del código corrigiendo los permisos de liberación de memoria:
class ConfiguracionRed {
int getPuertoConexion();
std::string getDireccionConexion();
}
class EnviadorRed {
void abrirConexion(std::string& direccion, int puerto);
void envia(std::string& mensajeRedFinal);
}
class PuenteEntreRedes {
void setConfiguracionEnvio(ConfiguracionRed& configuracionRed);
void setEnviadorRed(EnviadorRed& enviadorRed);
void enviaMensajeRed(MensajeRedInicial* mensajeRedInicial);
}
Bien, con este pequeño paso se evita que se liberen los objetos excepto en las zonas permitidas explícitamente.
Si miramos un poco más la implementación se puede ver que las referencias de los objetos se utilizan de dos maneras muy distintas. Hay una referencia como es la de EnviadorRed que al ser usada puede modificar el estado del objeto EnviadorRed; por el contrario la referencia al objeto ConfiguracionRed no realiza ninguna modificación sobre el objeto. ConfiguracionRed se utiliza básicamente para obtener información del objeto que no produce ninguna modificación sobre él. Estos métodos de ConfiguracionRed bien podrían estar marcados como const. Por tanto la referencia al objeto ConfiguracionRed debe marcarse como una regencia constante que no va a modificar en ningún caso el estado del objeto.
Al marcar una referencia como constante se está diciendo de forma implícita que se van a utilizar métodos de ese objeto que no van a modificar su estado. Esto se consigue marcando los métodos como const. Por tanto una referencia constante solo puede hacer llamadas a métodos const del objeto.
Pero ¿por qué no marcar la referencia a EnviadorRed como constante? Pues por lo mismo que se ha dicho anteriormente, porque al utilizar este objeto su estado va a ser modificado (puede que esté sincronizado, se ponga en estado enviando,…) por lo que la referencia se deja como no constante.
En fin, que si un objeto va a ser utilizado de manera que su estado no es modificado es mejor marcar la referencia como constante.
A continuación se presenta el código después de aplicar este nuevo criterio:
class ConfiguracionRed {
int getPuertoConexion() const;
std::string getDireccionConexion() const;
}
class EnviadorRed {
void abrirConexion(const std::string& direccion, int puerto);
void envia(const std::string& mensajeRedFinal);
}
class PuenteEntreRedes {
void setConfiguracionEnvio(const ConfiguracionRed& configuracionRed);
void setEnviadorRed(EnviadorRed& enviadorRed);
void enviaMensajeRed(MensajeRedInicial* mensajeRedInicial);
}
Se debe destacar que se han ido cambiando los tipos de referencia a los objetos string, pero si se ha seguido el razonamiento que ha conducido a cada cambio se ve que cambiar esperar cambiar el tipo de referencia.
En resumen, se debe utilizar paso de punteros cuando se desee delegar la responsabilidad del ciclo de vida del objeto que se pasa, se debe pasar una referencia en caso de que se quiera utilizar un objeto y evitar que pueda destruirse el objeto y por último se debe pasar una referencia constante cuando se desean utilizar de un objeto todos aquellos métodos que no modifiquen su estado.
Referencias:
Meyers, S. Effective C++: 50 Specific Ways to Improve Your Programs and Designs, Addison-Wesley, Reading, MA 1992.
Descargar articulo en pdf