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.

27 de noviembre de 2019

Arreglos de n dimensiones.

   Con base en lo descrito en las entradas anteriores, es posible realizar una generalización para declarar y manipular arreglos n-dimensionales o de n dimensiones ya que:
  • Un arreglo es una sucesión contigüa de variables de un mismo tipo de datos.
  • Un arreglo de dos dimensiones o matriz es un arreglo de arreglos.
  • Un arreglo de tres dimensiones o cubo (si las tres dimensiones son iguales) es un arreglo de arreglos de dos dimensiones, o un arreglo de matrices.
  • Un arreglo de cuatro dimensiones o hipercubo (si las cuatro dimensiones son iguales) es un arreglo de arreglos de tres dimensiones, o un arreglo de cubos para el caso de los hipercubos.
  • Un arreglo de n dimensiones es un arreglo de arreglos de n-1 dimensiones.
   En general, la declaración de un arreglo de n dimensiones tiene la siguiente estructura en el lenguaje de programación C:

tipo_de_dato   arreglo_n_dimensional[TAMAÑO1] [TAMAÑO2] . . . [TAMAÑO_N];

en donde:
  • tipo_de_dato es cualquier tipo de dato válido en C.
  • arreglo_n_dimensional es un identificador válido en C.
  • TAMAÑO1 es la longitud para la primera dimensión.
  • TAMAÑO2 es la longitud para la segunda dimensión.
  • TAMAÑO_N es la longitud para la n-ésima dimensión.
   Las siguientes figuras muestran dos posibles representaciones para un arreglo c de tres dimensiones de 4 x 4 x 4. La figura (a) muestra dicho arreglo representado como un cubo; mientras que la figura (b) muestra el mismo arreglo como una representación de malla o arreglo de matrices. Asegúrese de entender la ubicación de los elementos del arreglo tridimensional en ambas representaciones.

(a) Representación de un arreglo de tres dimensiones en forma de cubo.
(b) Representación de un arreglo de tres dimensiones en forma de malla.
 
    El Ejemplo 6.9 muestra cómo representar en C (línea 11) la abstracción ilustrada en las figuras anteriores.

   Observe cómo para la función imprimeCubo, en la línea 24 se especifica el tamaño de la segunda y tercera dimensión del arreglo c, ya que para arreglos de n dimensiones en general, se debe especificar el tamaño de todas las n-1 dimensiones a partir de la segunda, la única opcional es la primera.

   Note también cómo por cada dimensión se tiene un ciclo que controla el recorrido de la misma, y por consiguiente, cada ciclo tiene su propia variable de control. En este orden de ideas, es posible deducir que un arreglo de cuatro dimensiones requerirá de cuatro ciclos y de cuatro variables de control, un arreglo de cinco dimensiones cinco ciclos y cinco variables de control, y así sucesivamente.

   Finalmente, es importante hacer notar que aunque el Ejemplo 6.9 presenta las tres dimensiones iguales para el arreglo cubo esto no tiene por qué ser siempre así; de hecho, cada una de las tres dimensiones que definen un arreglo tridimensional pueden ser distintas.

   La salida del Ejemplo 6.9 se  muestra en la siguiente figura. Note cómo la impresión de cubo se ha hecho en la forma de un arreglo de matrices. Apóyese en la figura anterior (b) para visualizarlo mejor.

Salida del Ejemplo 6.9.

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.

7 de enero de 2019

Abstracción de datos.

   La abstracción es una característica, al menos hasta donde sabemos, inherente a la especie humana. Ahora bien, desde el punto de vista de la programación, por medio de la abstracción se puede comprender o describir un concepto sin tener que entrar en los detalles de su representación o especificación.

   En todo el blog se ha utilizado dicho concepto en forma de diagramas, imágenes o figuras de las representaciones de algunos de los tipos de datos en la memoria de la computadora, o de las estructuras de datos presentadas (como los arreglos y las matrices).

   Al escuchar conceptos como casa, automóvil, etcétera, se genera de manera casi inconsciente en nuestra mente una representación de ellos, y ésta podría variar de persona a persona, pero en esencia, la idea sería esencialmente la misma.

   En este sentido, si alguien nos solicitara dibujar en una hoja de papel nuestro concepto de casa o de automóvil, con toda seguridad habría unos dibujos más elaborados que otros, algunos con más detalles y otros con menos, pero sería poco probable que, en condiciones "normales" alguien dibujara una casa con una vaca en lugar de un techo o una pared; alguien podría quizá colocar en su dibujo la silueta de una chimenea, pero sería poco probable que alguien dibuje un piano sobre el techo.

   Para el caso de un automóvil, alguna persona podría dibujar un auto deportivo o quizá algo más modesto, pero al igual que antes, sería poco probable que alguien dibujara un automóvil que, en lugar de ruedas, tuviera unas guitarras eléctricas. La abstracción es en este sentido lo que nos permite comunicarnos con otras personas o colegas a un nivel conceptual.

   En las entradas subsecuentes se echará mano de un par de mecanismos utilizados en C para reforzar la abstracción en la programación a través de la representación de "nuevos" tipos de datos: struct y typedef. Cabe mencionar desde ahora que en realidad no se generarán nuevos tipos de datos, ya que un tipo de dato requiere de una representación, una interpretación, y un conjunto de operaciones bien definidas y establecidas asociadas a él; de ahí que sólo se utilizarán, y en todo caso se agruparán, los tipos de datos existentes de manera conveniente, para proporcionar o dotar al programador de una mejor abstracción.

Conceptos, representación y estructura.
   Una estructura (struct) es una colección de una o más variables de tipos de datos posiblemente diferentes, agrupadas bajo un solo nombre para un mejor y más conveniente manejo de las mismas.

   Las estructuras ayudan a organizar mejor los datos mejorando así la abstracción y la representación de información para el programador.

   Un ejemplo clave de una estructura es la representación de los empleados de una nómina. Un empleado se describe en dicho contexto por un conjunto de atributos o características, tales como (entre otros):
  • Número de empleado.
  • Nombre(s).
  • Apellido paterno.
  • Apellido materno.
  • Clave Única de Registro de Población (CURP).
  • Número de seguro social.
  • Puesto.
  • Domicilio.
  • Teléfono.
  • Salario.
   En C no es posible declarar una variable de tipo empleado o persona por ejemplo, ya que no existen dichos tipos de datos; sin embargo es posible, con los tipos de datos existentes, abstraer de manera conveniente variables que permitan representar objetos que sirvan como un tipo de datos.

   Una estructura (struct) en C tiene la siguiente estructura (la redundancia es necesaria):

         struct identificador{
               tipo_de_dato1 lista_de_identificadores1;
               tipo_de_dato2 lista_de_identificadores2;
                             .
                             .
                             .
               tipo_de_datoN lista_de_identificadoresN;
         };


donde:
  • identificador es un identificador válido en C y denota el nombre de la estructura.
  • tipo_de_dato es alguno de los tipos de datos de C.
  • lista_de_identificadores es una lista de identificadores separada por comas, para cada una de las variables definidas.
   Observe que una estructura en C puede contener diferentes tipos de datos y distintas variables de cada uno de ellos.

   A cada una de las variables que se declaran dentro de una estructura se les denomina: elemento miembro de la estructura, y cada elemento miembro se distingue por su respectivo identificador, el cual fue definido en la lista_de_identificadores.

   Al igual que antes, no puede haber identificadores repetidos para los elementos miembro de una estructura determinada. Los identificadores o nombres de las variables de los elementos miembro de una estructura son locales a ella y sólo pueden accederse a través de una variable del tipo correspondiente, tal y como se mostrará en el Ejemplo 8.1 de la entrada Abstracción en acción.

   Los elementos miembro de una estructura e1 pueden ser arreglos, apuntadores, e incluso variables cuyo tipo de dato se derive de otra estructura e2 por ejemplo, el único requisito será que e2 esté definida antes que e1.

   El número de estructuras que se pueden definir está limitado por las necesidades del programador, C no impone ninguna restricción en este sentido.

   Una estructura sólo define una plantilla o molde que agrupa otras variables. Es importante recalcar que por sí misma no crea las variables de sus elementos miembro, sólo las declara. Los elementos miembro de la estructura se crean cuando se define una variable cuyo tipo es una estructura, de ahí que se diga que las estructuras definen nuevos tipos de datos. En C una variable puede declararse varias veces pero sólo puede definirse una vez, la definición de una variable se da cuando ésta se crea y existe propiamente, mientras que la declaración sólo determina su existencia.

   Las estructuras en C se pueden copiar, asignar, pasar a funciones, y ser regresadas como valor de retorno de una función; sin embargo, no existe una relación de orden predefinida, es decir, no pueden ser comparadas.

   Sean e1 y e2 dos variables derivadas de la misma estructura, las operaciones:

e1 < e2, e1 > e2, e1 <= e2, e1 >= e2, e1 == e2, e1 != e2 

son inválidas.