En C++ cuando pasamos los argumentos en un procedimiento o función decimos que el paso puede ser por valor o por referencia. Antes de continuar vamos a clarificar nuestra terminología recordando que en la definición de una función hablamos de los parámetros que recibe la función. Es decir si defino la función suma en C++
void suma(int x, int y, int z){
z=x+y;
}
entonces x, y, z son los parámetros o argumentos formales de la función en los que además se ha indicado el tipo de dato al que corresponden, que en este caso es un entero (int). Cuando dentro de cualquier programa utilizo la función suma pasándole como argumentos distintos variables o valores como por ejemplo
suma(a,b,c); suma(2,3,c);
tanto a,b,c como 2 y 3 son argumentos de la función que se corresponderán dentro del cuerpo de la función a sus parámetros según el orden indicado. Por tanto a se corresponderá con x, b con y, c con z.
Paso por valor
El paso por valor es el mecanismo por omisión en C++. En el paso por valor, las variables que se pasan en una llamada a la función son copiadas a nuevas variables que se corresponden a los parámetros formales y que existirán durante el tiempo que se ejecuta la función. Por ejemplo en el siguiente código
http://pastebin.com/1z8AJvz9
en la línea 6 la variable c tiene valor 7. Cuando se llama a la función suma pasándole como tercer argumento la variable c, lo que ocurre es que la función suma copia el valor de c en una nueva variable que se corresponde con el parámetro z. Todo lo que se le haga a z le ocurre a la copia de c no a c. Lo mismo podemos decir de las variables a y b. Por tanto al ejecutarse suma(a,b,c) la copia de c que llamamos z pasa a ser la suma de las copias de a y b es decir z = x+y = 0+0 = 0. Pero cuando salimos de la función la copia se destruye y la variable c sigue teniendo su valor 7. Debido a que en la llamada suma(a,b,c) no se pasan las variables a, b y c sino una copia de su valor, hablamos de paso por valor.
Paso por referencia
En este caso lo que se le pasa a la función no es una copia del valor de la variable sino una refeencia a esa misma variable. Decimos que el parámetro se convierte en un alias para el argumento, es decir, es la misma variable pero con otro nombre. Como ahora el valor que toman los parámetros de la función no es una copia sino que corresponden a las variables externas a la función los cambios que ocurran dentro de la función tendrán alcance fuera de ella (algo que no ocurría en el paso por valor). Siguiendo con el ejemplo anterior si cambiamos la manera de expresar los parámetros en la definición de suma de modo que en vez de
void suma(int x, int y, int z)
escribimos
void suma(int x, int y, int& z)
lo que le estamos indicando al compilador de C++ es que el parámetro z se pase por referencia. Si volvemos al ejemplo anterior, al llamar a suma con el argumento c ya no se haría una copia de c sino que sería la misma variable c que dentro dela función tendría un segundo nombre (como un apodo o alias) llamado z y por tanto al tomar dentro de la función el valor z = 0+0=0 al salir de la función c valdría 0.
Biología
Veamos ahora un detalle importante relacionado con la eficiencia computacional en relación al paso por valor o por referencia. Imaginemos que tenemos que modelar una población biológica y deseamos programar una simulación donde los individuos se reproducen de modo que una pareja de padres tienen un hijo. Si quisiéramos programar una función reproducción podríamos a priori pensar en algo como
void reproduce(Individuo padre, Individuo madre, Individuo& hijo){..código de la función..}
Donde el tipo de los parámetros es una clase llamada Individuo. Además hemos sido hábiles y nos dimos cuenta de que podemos pasar el padre y la madre por valor puesto que no necesitan cambiar al pasar por la función. Sin embargo el hijo lo vamos a calcular dentro y presumiblemente querremos conservar el valor calculado al salir de la función, por tanto hemos decidido, sabiamente, pasar el parámetro hijo por referencia. Esto hecho así, es correcto y posiblemente obtengamos el comportamiento deseado.
Hay sin embargo un posible problema relacionado con la eficiencia. Imaginemos que nuestra clase Individuo es una abstracción de un organismo diploide, es decir con dos series de cromosomas y que además de los cromosomas le queremos asignar un valor constante para el carácter que sea (por ejemplo color) y otros valores variables como altura, edad etc. Respecto a los cromosomas, vamos a considerar 10 pares de cromosomas cada uno con un millón de posiciones que pueden tener valor 0 o 1. Es decir, el Individuo padre consta de 20 vectores de longitud un millón de posiciones cada uno además de una constante y algunas variables. Si recordamos lo anteriormente dicho para el paso por valor, cuando llamemos a la función reproduce el primer argumento de la llamada se copiará en el primer parámetro (padre). Esta copia exigirá copiar cada una de las millones de posiciones de los vectores así como las constantes, variables y cualquier procedimiento que contengan los objetos de la clase Individuo. Total para usar, sin cambiarlos, unos valores que ya existían fuera de la función. Incurrimos por tanto en un gasto innecesario de memoria y tiempo de computación. En modelos de simulación con miles de individuos y procesos que se repiten durante muchos miles de generaciones el coste puede ser prohibitivo. ¿Cuál es la solución? Pues obviamente es el paso por referencia. La definición adecuada de la función sería
void reproduce(const Individuo& padre, const Individuo& madre, Individuo& hijo){..código de la función..}
Donde todos los argumentos se pasan por referencia de modo que no se realiza la copia de los millones de posiciones cromosómicas sino que simplemente se pasa una referencia al comienzo de la dirección del objeto Individuo correspondiente. El modificador const delante de padre y madre indica que estos parámetros no se modifican dentro de la función e impiden que se haga por error puesto que el compilador se quejará si intentamos cambiarlo en el código.
Otra alternativa que quizás genera un código más claro sería no pasar ningún argumento que se modifique y hacer que la función devuelva un nuevo individuo que será el hijo:
Individuo reproduce(const Individuo& padre, const Individuo& madre){ Indiv hijo; /* codigo....*/ return hijo; }
Sin embargo esta última opción no será posible si es necesario cambiar más de un objeto (si queremos por ejemplo generar dos hijos de cada pareja de padres) y tampoco es lo más razonable cuando queremos modificar vectores. En estos casos el paso por referencia no constante es más conveniente.
Punteros (paso por dirección)
Una última opción para el paso de argumentos en C++ es que el parámetro de la función sea un puntero. En este caso estamos pasando la dirección de la variable y cualquier cambio en el contenido de esa dirección afectará al valor de la variable. En el paso por dirección obtenemos por tanto un comportamiento igual al del paso por referencia. Pero ahora el nombre del parámetro ya no es un alias de la variable sino que corresponde a la dirección de la variable y por tanto hay que desreferenciarla para acceder al contenido de esa dirección (usando el operador *). Dejamos aquí está discusión pues queda más allá de la intención del post. Para saber más sobre referencias y punteros y cuando sería más conveniente el uso de unos u otros se puede consultar el libro de Stroustrup citado en la bibliografía. Personalmente suelo usar siempre referencias puesto que simplifica el código de la función ya que no es necesario desreferenciar la variable (una referencia equivale a un puntero que se desreferencia automáticamente). El paso de punteros puede generar problemas adicionales si ocurren llamadas a la función usando punteros nulos.
Bibliografía
Lenguajes de programación. Principios y práctica. K.C. Louden.
Programming: Principles and Practice Using C++. B. Stroustrup
Object-Oriented Programming in C++. R. Lafore.
Effective C++. S. Meyers.
Gracias Antonio!! un buen post para mejorar la eficiencia
ResponderEliminar