28 de octubre de 2019

Funciones de biblioteca.

   Desde el primer ejemplo de este blog se ha estado haciendo uso de funciones: main es una función, printf, scanf, getchar, rand y srand también son funciones, y al uso de una función se le denomina invocación o llamado de función.

   Considere el problema de calcular las raíces de una ecuación de segundo grado utilizando la fórmula general mostrada en la siguiente fórmula:

Fórmula general para encontrar las raíces de una ecuación cuadrática.
 
    Una posible solución a dicho problema es el programa que se muestra en el Ejemplo 4.1, el cual es el ejemplo más elaborado hasta ahora en cuanto se refiere a los elementos que contiene, y con excepción de las líneas 6, 26 y 27, debería ser completamente comprendido. La línea 6 le indica al compilador que incluya la biblioteca de funciones matemáticas (math.h), ya que en las líneas 26 y 27 se hará uso de la función sqrt (square root), la cual calcula la raíz cuadrada de su argumento.

   Las líneas 26 y 27 muestran el uso de una función de biblioteca (sqrt), y a dicho uso se le denomina invocación o llamado de función. Piense en la invocación o llamado de función como el equivalente a presionar el botón correspondiente de una calculadora para obtener el cálculo de la raíz cuadrada de un número por ejemplo.

   Sin la función sqrt, el Ejemplo 4.1 no podría ser resuelto tan sencillamente, a menos que esas líneas fueran substituidas por el algoritmo correspondiente para calcular la raíz cuadrada de un número y no sólo eso, sino que el código en C de dicho algoritmo tendría que ser repetido dos veces al menos, en base a como está escrita la propuesta de solución del Ejemplo 4.1.

   Las funciones de las bibliotecas del lenguaje son cajas negras: se puede saber lo que realizan, los datos que necesitan para trabajar, y la salida que proporcionan, pero no se sabe con certeza cómo es que lo hacen porque no se tiene acceso a su código fuente.

   Al igual que las funciones matemáticas, las funciones en C operan sobre un conjunto de datos mismos que procesan o transforman y finalmente regresan un valor; pero tome en cuenta que no todos los problemas se basan en, o modelan funciones matemáticas, y que por otro lado, no todas las funciones que se pudieran necesitar para resolver un problema se encuentran disponibles en las bibliotecas de funciones del lenguaje, por lo que en muchos casos, se necesitarán construir funciones de elaboración propia, con servicios y funcionalidades específicas y para ello, es preciso conocer su estructura y cómo es que se definen.

   Una posible salida para el programa del Ejemplo 4.1 se muestra en la siguiente figura:

Una posible salida del Ejemplo 4.1.
 
    Tome en consideración que, dependiendo del compilador o de la distribución de GNU/Linux que utilice (si es el caso), es probable que el programa del Ejemplo 4.1 requiera del argumento "-lm" para su compilación. Para más detalles al respecto, puede consultar la entrada respecto a la compilación en línea utilizando el compilador de GNU/Linux.
 

Ejemplos de definición de funciones.

   Considere el Ejemplo 4.2. Las líneas 7 y 8 constituyen los prototipos de funciones y constituyen la firma de la función, en cuanto a que le indican al compilador que se definirán dos funciones: mayor y menor, mismas que recibirán tres parámetros enteros (int) y regresarán un valor entero.

   Tanto los tipos de datos como el número de parámetros así como el identificador de la función, están en relación directa con las necesidades del problema a resolver. Para el caso del Ejemplo 4.2, se definirán dos funciones que determinan (regresan), el mayor y menor respectivamente, de tres números enteros.

   Note que en las líneas 18 y 19 se realiza el llamado de las funciones mayor y menor respectivamente, y que el valor regresado por cada una de ellas se utiliza en el contexto del especificador de formato “%d” de la función printf correspondiente, el cual está asociado al tipo de dato de retorno de la función (int).

   Note también en las líneas 18 y 19, que a las funciones mayor y menor se les están pasando los datos a, b y c, los cuales son variables de la función main (línea 11), que a su vez contienen valores específicos (línea 15), y que son transferidos o enviados a las funciones mayor y menor respectivamente como sus argumentos.

   Por otro lado, las líneas 24-46 corresponden a la definición de las funciones mayor y menor. Las líneas 24-34 definen el grupo de sentencias que componen a la función mayor (note las llaves que definen el inicio y fin del bloque de función). Observe la estructura que sigue esta función, compárela con la estructura general de una función en C, y asegúrese de comprender la equivalencia.

   La línea 24 establece (define) que la función identificada como mayor, el cual es el identificador o nombre de la función, regresa un valor de tipo entero (int), y recibe tres parámetros de tipo entero (int) identificados por x, y y (x, y y z constituyen la lista de parámetros de la función).

   Ahora bien, la línea 25 define una variable local a la función mayor identificada como max. En C es posible definir variables al inicio de cada bloque ( { ... } ), y los bloques de función no son la excepción. Se pueden definir tantas variables locales como se necesiten y del tipo que se necesiten, y se siguen las mismas reglas de declaración de variables que para la función main.

   Las líneas 27-31 son sentencias de comparación que, a través de estructuras de selección, sirven para determinar el mayor de tres números distintos. La idea subyacente es la siguiente:
  1. Se selecciona a una de las variables como la mayor, supongamos que es x, y por lo tanto la asignamos a max (línea 25).
  2. Se compara la segunda variable y con el valor de la variable supuestamente mayor (max), si y > max (línea 27), entonces max no es la mayor y se almacena en max el valor mayor hasta ese momento (línea 28). En caso contrario, max tiene el valor mayor entre x y y y no hay necesidad de intercambiar.
  3. Se compara la tercera variable z con el valor de la variable hasta el momento mayor (max), si z > max (línea 30), entonces max no es la mayor y se almacena en max el valor mayor (línea 31). En caso contrario, no hay necesidad de intercambiar, max tiene el valor mayor de x, y y z.
   La línea 33 contiene la palabra reservada return seguida de la variable max, y al igual que en main, regresa el valor contenido en max a quien llamó a la función mayor, que para el caso del Ejemplo 4.2 ocurrió en la línea 18.

   Las líneas 36-46 para la función menor, se describen de manera análoga a las de la función mayor de las líneas 24-34.

   Una posible salida para el Ejemplo 4.2 se muestra en la siguiente figura:

Una posible salida del Ejemplo 4.2.
 
    Finalmente, note que es posible definir más de una función en un programa, de hecho no hay un límite en ese sentido, por lo que se pueden definir tantas funciones como sean necesarias, y todas siguen las mismas reglas descritas hasta aquí.

   Por otro lado, el Ejemplo 4.3 implementa una calculadora básica con las cuatro operaciones aritméticas. Las líneas 6-10 muestran los prototipos de las funciones que se definirán en las líneas 37-58. Dichos prototipos deberían ser claros en lo que expresan, con excepción quizá del prototipo de la línea 6.

   El prototipo de función de la línea 6, le indica al compilador que se definirá una función que no regresa nada (void). En en lenguaje de programación C, las funciones que sólo realizan alguna labor (como la presentación de un mensaje por ejemplo), pero que no necesitan regresar algún valor, usan éste tipo de dato. El tipo void es un tipo de dato muy importante, y su utilidad se retomará en la entrada referente a apuntadores, por ahora, basta con saber que ésta es una de sus utilidades.

   Ahora bien, el bloque (cuerpo) de la función principal main se encarga de leer los datos (línea 19), de verificar que sean los apropiados o que cumplan con ciertas características (ciclo do-while), y de tomar la decisión de qué servicio (función) solicitar (llamar), en respuesta (estructura switch) al operador op leído (línea 19), para finalmente presentar el resultado (línea 32).

   La definición de funciones está de las líneas 37-58, y se describen de la siguiente manera:
  • La función instrucciones (líneas 37-42), se utiliza para mostrar un mensaje en la salida estándar que contiene instrucciones básicas del uso del programa. 
  • La función suma (líneas 44-46) es bastante simple, ya que regresa a quien la llame, el valor de la expresión de suma (a + b) de los parámetros a y b. Observe que aunque es posible, no es necesario guardar el resultado de dicha expresión en una variable local y después regresarla, de hecho, es muy común en C  escribir este tipo de expresiones como valor de retorno. 
  • Las funciones resta (líneas 48-50), multiplica (líneas 52-54), y divide (líneas 56-58), se describen de manera análoga a la función suma.
   Note que la función divide no hace ningún tipo de verificación del denominador para realizar la operación de división, debido a que asume que ya se ha realizado dicha verificación en alguna otra parte del programa (main línea 20), y que el denominador es distinto de cero. El programa del Ejemplo 4.3 es sólo un posible diseño a dicha situación, mismo que no es el único ni necesariamente el mejor, sin embargo funciona y refuerza el concepto de programación modular que se ha venido mencionado.

   Finalmente, tome en cuenta que para el programa del Ejemplo 4.3 ninguna de sus funciones, excepto main, hace uso de otra función para completar su tarea debido a su simplicidad, sin embargo, es posible hacerlo y se mostrará en ejemplos posteriores. Una posible salida para el Ejemplo 4.3 se muestra en la siguiente figura:
Una posible salida del Ejemplo 4.3.

Estructura para las funciones en C.

   La estructura general de la definición de una función en el lenguaje de programación C, es la siguiente:

tipo_de_dato_de_retorno   nombre_de_función (lista_de_parámetros) {

       tipo_de_dato variables_locales;

           sentencia1; 
                 .
                 .
                 .

           sentenciaN;

           return variable;
}


donde:
  • tipo_de_dato_de_retorno: es el tipo de dato del valor que la función regresará, y debe ser congruente con el de variable.
  • nombre_de_función: es un identificador válido en C para nombrar a la función. A partir de él se harán los llamados respectivos. Se recomienda un nombre representativo que dé idea de la labor que realiza la función.
  • lista_de_parámetros: es una lista de variables separada por comas ",”, cada una con su propio identificador y tipo de dato; es el mecanismo de comunicación de la función con el exterior, su interfaz. Esta lista, junto con las variables_locales, constituyen las variables locales a la función.
  • tipo_de_dato: es alguno de los tipos de datos del lenguaje de programación C.
  • variables_locales: es una lista de variables locales a la función, mismas que serán utilizadas por la función en el grupo de sentencia(s).
  • sentencias: son el grupo de sentencias que representan, definen, describen y detallan la funcionalidad o utilidad de la función. Las sentencias en conjunto con las variables locales, conforman el algoritmo que encapsula la función.
  • return: es una palabra reservada, y se utiliza para regresar al invocador de la función el valor determinado por variable.

   Al trabajar con funciones de biblioteca, la directiva #include es sumamente importante ya que incluye, entre otras cosas, los prototipos de función de las funciones de biblioteca que se utilizan en el programa que la contiene.

   Por otro lado, el llamado de función ocurre cuando se escribe el nombre de la función que se va a utilizar, pero el código fuente (la definición de la función) no es accesible debido a que se encuentra en formato binario, listo para ser asociado y vinculado al programa que lo necesite durante el proceso de compilación y vinculación. El llamado de función sería el equivalente a presionar un botón de una calculadora para obtener un cálculo, como por ejemplo, obtener la raíz cuadrada de un número.



21 de octubre de 2019

Repetición controlada por centinela.

   No en todos los problemas es posible conocer de manera anticipada cuantas veces se repetirá un grupo de sentencias. Es en este tipo de situaciones cuando tenemos el tipo de repetición controlada por centinela.

   El centinela es un valor que se utiliza para determinar la continuidad o no del ciclo a partir de la evaluación de la expresión condicional del mismo, es decir, el centinela determina si el grupo de sentencias asociadas se procesarán o no nuevamente. Por lo general, el valor del centinela forma parte de la expresión condicional de la estructura de control de repetición, y puede estar dado de manera explícita o implícita.

    Considere el ejercicio de la división y su solución del Ejemplo 3.12, ahí el centinela está implícito, debido a que la descripción del problema no contempla la designación del cero como centinela, sino que la naturaleza misma del problema lo requiere de esa manera ante la posible indeterminación de la división.

   En otros casos, el centinela se describe de manera explícita, como en los siguientes ejemplos:
  • Determinar el promedio de una lista de calificaciones, en donde cada calificación será introducida desde el teclado una a la vez. El fin de la lista de calificaciones sera indicado con -1, debido a que la lista es variable.
  • Determinar la suma de los números enteros proporcionados desde el teclado, hasta que el número proporcionado sea cero.
  • Dada una lista de números enteros positivos, determine el mayor y el menor de ellos, la lista termina al proporcionar cualquier número negativo.
   Aunque la repetición controlada por centinela es menos frecuente, no es menos necesaria ni menos importante que la repetición controlada por contador, de ahí la importancia de presentarla, conocerla y dominarle como parte de su repertorio como programador y de las técnicas utilizadas en la resolución de problemas.

   Considere el Ejemplo 3.19, el cual resuelve el mismo problema que el Ejemplo 3.18 pero utilizando un enfoque diferente: el de la repetición controlada por centinela.

   Note que en la repetición controlada por centinela, podría ser que el primer dato procesado fuera el centinela, por lo que no habría calificaciones a promediar, lo cual no es posible de hacer en la repetición controlada por contador, en donde se está obligado a proporcionar las N (diez por ejemplo) calificaciones; sin embargo, es posible simular el mismo tipo de ejecución, tal y como se muestra en la siguiente figura:

Una posible salida para el Ejemplo 3.19.
 
   Compare los ejemplos y sus correspondientes salidas, y asegúrese de entenderlos.

Arreglos de apuntadores a funciones.

   En diferentes tipos de programas y aplicaciones es frecuente encontrar menús de opciones para seleccionar una determinada acción u operación a realizar.

   Cuando muchas o todas las opciones presentadas en un menú comparten características similares, una excelente opción, aunque no la única, es implementar dichas opciones a través de un arreglo de apuntadores a función.

   En el Ejemplo 4.3 se desarrolló una calculadora con las operaciones aritméticas básicas de suma, resta, multiplicación y división. En esta entrada se retomará dicho ejemplo para reescribirlo utilizando un arreglo de apuntadores a funciones.

   El Ejemplo 7.13 muestra que las cuatro funciones de suma, resta, multiplica y divide comparten un patrón común: todas reciben dos argumentos de tipo float y regresan un valor de tipo float.

   El patrón identificado se aprovecha en la línea 15, la cual declara el arreglo funciones de cuatro apuntadores a funciones que reciben dos argumentos de  tipo float y regresan un valor de tipo float. El arreglo es inicializado en la declaración con una lista de identificadores: suma, resta, multiplica y divide.

   El uso del arreglo de apuntadores funciones se presenta en la línea 29, en donde el valor de la variable op se utiliza como índice para invocar a la función correspondiente que realizará la operación seleccionada en base al menú de opciones (líneas 37-51).

   En este sentido, si op tiene el valor tres por ejemplo, la expresión:

( * funciones[op - 1]) ( a, b )

se transforma en la expresión:

multiplica(a, b)

debido a que el identificador que se encuentra en la posición dos (op - 1) del arreglo de apuntadores a funciones funciones, es precisamente multiplica. La resolución de los otros identificadores ocurre de manera análoga.

   El llamado a la función a invocar se resuelve en tiempo de ejecución, ya que el llamado a través del arreglo de apuntadores a funciones de la línea 29 es un llamado implícito.

   Finalmente, la función menu indica con void que no recibirá ningún tipo de argumento. Todos los demás detalles del programa de ejemplo ya deberían resultarle familiares al lector. Una posible salida para el Ejemplo 7.13 se muestra en la siguiente figura:

Una posible salida del Ejemplo 7.13.
   Cabe mencionar por último que la forma de utilizar los arreglos de apuntadores a funciones mostrada en el Ejemplo 7.13 no es su única posibilidad de uso, sino que es una de tantas en el amplio abanico de opciones en la programación estructurada; en este sentido, la limitante respecto al uso de apuntadores a funciones estará en general más en función de la imaginación y capacidad del programador, que de las múltiples posibilidades de su uso y aplicación.