16 de diciembre de 2019

Apuntadores a funciones.

    En la entrada Ordenamiento por burbuja se presentó, describió e implementó dicho ordenamiento, en esta entrada se retomará ese algoritmo para abordar la siguiente problemática: se desea ahora poder ordenar los datos de manera ascendente o descendente, ¿cómo lo solucionaría con lo que se ha discutido hasta ahora?

   Una posible solución sería hacer dos funciones: una que ordene los datos de manera  ascendente y otra que los ordene de manera descendente, tal y como se muestra en el Ejemplo 7.10.

   La función burbujaAscendente ordena los datos de forma ascendente, mientras que la función burbujaDescendente los ordena de forma descendente tal y como sus identificadores lo sugieren.

   Compare las funciones y notará que son exactamente iguales excepto por las líneas 14 y 29 respectivamente, que es en donde se realiza la comparación de los elementos para determinar el orden en que están e intercambiarlos, si fuera necesario, en base al tipo de ordenamiento.

   La solución presentada, aunque es útil y válida, no es una solución eficiente en espacio ni elegante. El lenguaje de programación C proporciona un mecanismo para abordar de mejor forma este tipo de situaciones: los apuntadores a funciones.

   Un apuntador a función almacena la dirección en memoria del inicio de las instrucciones de una función, la cuál está representada por su identificador.

   De manera informal puede decirse entonces, que un apuntador a función es un mecanismo que permite hacer referencia a una función de manera implícita.

   Los apuntadores a funciones pueden asignarse, ser almacenados en arreglos, pasados a funciones, regresados por funciones, etcétera. El Ejemplo 7.11 muestra la forma de definir un apuntador a funciones en la función burbuja de las líneas 17-30.

   La explicación de los ejemplos siguientes se centrará exclusivamente en el tema de la entrada: apuntadores a funciones, no en el ordenamiento por burbuja.

   El tercer parámetro de la función burbuja (línea 17) indica que se recibirá un apuntador a funciones que tengan la siguiente estructura:
  • Reciban dos argumentos de tipo int.
  • Regresen un valor de tipo int.
   Cualquier función que cumpla con esta plantilla, podrá ser referida por el apuntador a función orden.

   Note que en la expresión:

int (*orden) (int, int)

los paréntesis que circundan a *orden son muy importantes, debido a que le indican al compilador que orden es un apuntador a funciones que reciben dos argumentos de tipo int, y que regresen un valor de tipo int.

   Por otro lado, la expresión:

int *orden(int, int)

le indicaría al compilador que orden es una función que recibe dos argumentos de tipo int, y regresa un apuntador a int, lo cual es definitiva y completamente diferente.

   Observe ahora la línea 23 del Ejemplo 7.11, en la cual ocurre la des referencia del apuntador a función, es decir, en ella se invocará a la función cuyo identificador coincida con el tercer argumento enviado a la función burbuja en su invocación (líneas 16 y 19 del Ejemplo 7.12).

   Ahora bien, el Ejemplo 7.12 muestra la forma de pasar el apuntador denotado por el identificador respectivo, a la función burbuja en las líneas 16 y 19. Note que en la línea 6 se está incluyendo la biblioteca de funciones del Ejemplo 7.11.

   Observe que el apuntador a funciones orden hace que la línea 23 del Ejemplo 7.11 se transforme en un llamado explícito a las funciones ascedente o descedente; es decir, convierte la expresión:

if ( (*orden) (a[ i ], a[ i + 1 ]) )

en la expresión:


if ( ascendente (a[ i ], a[ i + 1 ]) )

para el llamado a la función burbuja (línea 16) del Ejemplo 7.12. Mientras que la expresión:

if ( (*orden) (a[ i ], a[ i + 1 ]) )
es convertida en la expresión:

if ( descendente (a[ i ], a[ i + 1 ]) )

para el llamado a la función burbuja (línea 19) del Ejemplo 7.12.

   Por último, la función ascendente (líneas 9-11) del Ejemplo 7.11, regresa 1 (verdadero) si a es mayor que b, y 0 (falso) si no lo es, lo cual corresponde al criterio requerido para un ordenamiento ascendente. Observe que la expresión de la línea 10:

return a > b;

es lógicamente equivalente a la estructura de selección if-else:

                    if(a > b)
                             return 1;
                       else
                             return 0;

y que la función descendente (líneas 13-15) se comporta de manera análoga a la función ascendente. La salida del Ejemplo 7.12 se muestra en la siguiente figura:

Salida del Ejemplo 7.12.
   En resumen, en el contexto de apuntadores a variables los apuntadores almacenan las direcciones de memoria de otras variables, mientras que en el contexto de funciones los apuntadores almacenan la dirección de memoria del inicio de las instrucciones de una determinada función. Note que en ambos casos lo que el compilador y la computadora controlan son direcciones de memoria, mientras que el programador controla identificadores, lo cual es una abstracción sumamente conveniente.

3 de diciembre de 2019

Consideraciones finales con arreglos.

Regresar un arreglo estático como valor de retorno de una función.

   Probablemente en la entrada referente a los arreglos bidimensionales se haya preguntado ¿Por qué la función suma del Ejemplo 6.8 recibe también como parámetro a la matriz resultado c?, ¿acaso no sería más conveniente y de mejor diseño regresar dicha matriz como valor de retorno de la función?

   Considere el Ejemplo 6.10 el cual propone, con base en los conceptos hasta ahora discutidos, una posible solución al planteamiento de la pregunta anterior.

   La línea 8 presenta una forma intuitiva en la que se expresaría el deseo de que la función regrese una matriz; note que la lista de parámetros se ha reducido en uno respecto de la función suma del Ejemplo 6.8. Tómese el tiempo necesario para comparar ambas funciones antes de continuar.

   Con la excepción del cambio expresado en el párrafo anterior, las funciones de los Ejemplos 6.8 y  6.10 son esencialmente la misma. El Ejemplo 6.10 no compila, ya que no es posible indicarle al compilador de C que regrese una matriz como el tipo de dato de retorno para una función.



   ¿Por qué no se puede indicar al compilador de C que regrese un arreglo?, además de que la gramática del lenguaje C no considera válida dicha expresión, se debe también a que las variables declaradas dentro de una función son variables automáticas y locales a ella, por lo tanto, sólo existen en el contexto de la propia función, por lo que cuando ésta termina, los datos son desechados antes de regresar el control de la ejecución del programa a la siguiente sentencia después de la invocación de la función, por lo que no tendría sentido regresar como valor de retorno una variable que ya fue desechada.

   Ahora bien, aún cuando dicha variable fuera afectada por el modificador static (véase la entrada referente a ámbitos de variables), la variable sólo sería accesible dentro del ámbito de la función.

   Finalmente, la respuesta a la pregunta expresada al inicio de esta entrada es: en general sería más conveniente y también un mejor diseño el enfoque del Ejemplo 6.10, pero en el lenguaje de programación C los arreglos no se pueden regresar como valor de retorno de función, al menos no con los arreglos estáticos declarados en tiempo de compilación y con las herramientas hasta ahora estudiadas; hace falta algo más, y ese algo más son los apuntadores, los cuales se estudiarán en entradas posteriores.

Otros algoritmos de ordenamiento.

   En la entrada Ordenamiento por burbuja se describe dicho algoritmo por su simplicidad en su funcionamiento y compresión, y porque es un algoritmo básico y elemental que debe conocerse por cualquier programador, sin embargo, no es el único algoritmo y dista mucho de ser el mejor en cuanto a eficiencia se refiere.

   Existen otros algoritmos de ordenamiento, en este sentido, se recomienda al lector revisar al menos esta información: Algoritmos de ordenamiento, para ampliar su perspectiva al respecto. En este sentido, resulta particularmente recomendable revisar y comprender los algoritmos de shell sort, merge sort y quick sort como parte del repertorio de herramientas que todo programador debería conocer.


Parámetros por valor y por referencia.

   En el lenguaje de programación C cuando se hace el llamado a una función y se le envían argumentos, dichos argumentos son copiados en los parámetros de la función; lo cual quiere decir que por cada argumento transferido existe una copia almacenada en el parámetro correspondiente. A este esquema de copia de valores se le conoce en el argot computacional como parámetros por valor.

   Los parámetros por valor son el esquema estándar de C, es decir, es el que se utiliza normalmente si no se especifica otra cosa; sin embargo, existe otro enfoque para el envío de argumentos y su respectiva recepción como parámetros. En dicho enfoque, no hay una copia de valores sino una referencia a los valores originales, y la forma de implementarlo es a través de apuntadores. Cuando se utiliza dicho esquema se habla de parámetros por referencia.

   El Ejemplo 7.2 muestra el uso, funcionamiento, y diferencia de los parámetros por valor y los parámetros por referencia.

   La principal diferencia se observa en los prototipos de las funciones. La línea 7 indica que se definirá una función de nombre llamadaValor que no regresará nada y que recibirá un int, es decir, un parámetro por valor.

   Por otro lado, la línea 8 indica que se definirá una función de nombre llamadaReferencia que no regresará nada y que recibirá un apuntador a int, es decir, un parámetro por referencia.

   La función main define e inicializa en la línea 11 a la variable entera argumento, misma que será enviada a las funciones llamadaValor y llamadaReferencia en las líneas 14 y 16 respectivamente. Así mismo, se imprime en la salida estándar el valor de argumento en las líneas 13, 15 y 17, para determinar si éste ha sido afectado por los llamados de función correspondientes.

   Sólo resta explicar las funciones:
  • llamadaValor: recibe en parametro una copia de argumento justo en el llamado de la función (línea 14). La siguiente figura muestra los contextos de las funciones mainllamadaValor separados por una línea y un recuadro, el cual contiene la asignación implícita que ocurre en el llamado de la función.
(a) Representación de argumentos y parámetros en llamada por valor.
  • llamadaReferencia: recibe en parametro la referencia a argumento justo en el llamado de la función (línea 16) (note que en la línea 16 se está enviando la referencia o dirección en memoria de argumento a parametro. La siguiente figura muestra los contextos de las funciones main y llamadaReferencia separados por una línea y un recuadro, el cual contiene la asignación implícita que ocurre en el llamado de la función.

(b) Representación de argumentos y parámetros en llamada por referencia.

   Una posible salida del Ejemplo 7.2 se muestra en la siguiente figura:

Una posible salida del Ejemplo 7.2.