30 de agosto de 2016

Arreglos de caracteres (cadenas).

   Todo lo que hasta este momento se ha comentado para los arreglos respecto a su declaración, inicialización, recorrido, y uso con funciones, es aplicable a arreglos de cualquiera de los tipos de datos de C incluyendo char; sin embargo, este tipo de arreglos poseen características particulares, empezando porque es común denominar a un arreglo de caracteres (char) como cadena, de ahí que su tratamiento y discusión se hayan separado en una entrada especial.

   Si se han revisado las entradas anteriores, en este momento el lector cuenta con algo de experiencia en la declaración, inicialización, recorrido, y uso de arreglos con funciones, por lo que en el Ejemplo 6.6 se presentan estos conceptos en el contexto de cadenas, es decir: arreglos de caracteres.

   Las líneas 12, 13 y 14 muestran la declaración de tres cadenas: cadena1, cadena2 y cadena3. Note que las dos primeras no tienen un tamaño explícito y que han sido inicializadas de forma distinta.

   La cadena1 se ha inicializado con una frase contenida entre comillas dobles. En el lenguaje de programación C, cualquier sucesión de elementos contenida entre comillas dobles es una cadena, y dicha cadena está compuesta por todos los caracteres entre comillas (incluyendo los espacios en blanco), más un carácter no visible, pero que delimita el fin de la cadena. Este carácter recibe el nombre de fin de cadena y es '\0', que aunque es un carácter compuesto por dos símbolos (\ y 0), se considera como uno solo y se denota entre comillas simples: '\0'. Por lo tanto la cadena1 tiene un tamaño implícito o longitud no de 18 caracteres como podría pensarse en un principio, sino de 19.

   Por otro lado, la cadena2 (línea 13) tiene una inicialización más parecida a lo que se vio en su momento para arreglos de tipo int y float. Note que cada unos de los elementos de la cadena, incluyendo el fin de cadena, se encuentran entre comillas simples. Para este tipo de inicialización el terminador de cadena '\0' debe proporcionarse de manera explícita, ya que de otra forma, la cadena estaría mal formada, es decir, sin terminar, y esto podría llevar a efectos o resultados desde inesperados (impresión de caracteres extraños más allá de lo que en principio conforma la cadena) hasta catastróficos (intentos de acceso a memoria no reservada y la consecuente anulación del programa). Esta cadena tiene una longitud implícita de seis caracteres.

   La cadena3 no ha sido inicializada y se utilizará para leer una cadena desde la entrada estándar a través de la función gets (la cual recibe una cadena almacena en ella lo que se procesa de la entrada estándar, hasta que se introduzca un ENTER, (\n) (línea 18).

   El ciclo for de la línea 21 muestra el recorrido clásico de una cadena. Observe que la expresión condicional depende del carácter de fin de cadena ('\0'), y que el cuerpo del ciclo (línea 22) imprime la cadena cadena1 carácter por carácter con un espacio entre ellos.

   La línea 23 muestra el uso de la función putchar para imprimir el carácter de avance de línea y un retorno de carro. Dicha función es equivalente a la sentencia:

printf("\n");

   Note que la secuencia de escape '\n' ha sido escrita entre comillas simples en la línea 23 debido a que la función putchar escribe en la salida estándar el carácter que recibe como argumento.

   El Ejemplo 6.6 también dos funciones que se describen a continuación:
  • longitud (línea 38): recibe una cadena, la cual se identifica como c dentro de la función, y realiza un recorrido sobre c para llegar al final de la misma, de tal forma que la variable de control i contiene la longitud de c cuando la expresión condicional del for se evalúa como 0; el cuerpo del ciclo está vacío porque no hay nada más que hacer y esto se enfatiza con el ";" de la línea 42. Finalmente la función regresa la longitud de la cadena: i (línea 43).
  • imprimeAlReves (línea 30):  recibe una cadena, la cual se identifica como c dentro de la función y realiza un recorrido inverso sobre c, es decir, del último carácter de la cadena (longitud - 1) al primero (0). Dentro del recorrido, la cadena se imprime en la salida estándar carácter por carácter.
   Note que la cadena original (como argumento la cadena original es cadena2 (línea 25), como parámetro es c (línea 30)) no se invierte, sino que únicamente se imprime al revés, por lo tanto la cadena original permanece inalterada; asegúrese de comprender esto antes de continuar.

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

Una posible salida del Ejemplo 6.6.
 
    En la entrada correspondiente a los apuntadores se retomará el tema de cadenas.

29 de agosto de 2016

Búsqueda lineal y búsqueda binaria.

Búsqueda lineal.
   La búsqueda lineal es muy sencilla y natural, dado que es la forma en que los seres humanos realizamos búsquedas.

   La idea es bastante simple: se va recorriendo uno a uno la secuencia de elementos sobre los que se desea realizar la búsqueda (conjunto de datos), y se va comparando cada uno de ellos con el elemento a buscar (clave o llave); si existe coincidencia, entonces la búsqueda ha tenido éxito y se reporta el índice (posición) del elemento dentro del conjunto de datos.

   También puede ocurrir que se haya recorrido todo el conjunto de datos y no se haya encontrado coincidencia con la clave, en cuyo caso se reporta dicha situación con una posición (índice) no válida (-1) respecto al conjunto de datos.

   La función busquedaLineal de las líneas 8-15 del Ejemplo 6.5, recibe un arreglo de enteros constantes (el modificador const le indica al compilador que el arreglo es de elementos constantes, y previene al programador de alguna modificación accidental o incidental sobre los elementos del arreglo afectado por el modificador, es decir, vuelve al arreglo afectado de sólo lectura: no es posible modificar sus elementos dentro de la función) a (conjunto de datos), un entero x (clave o llave), y un número n que representa la cantidad de datos del conjunto sobre el que se realizará la búsqueda (podría ser todo el conjunto de datos o sólo una parte de él).

   El ciclo for de la línea 11 realiza el recorrido sobre el conjunto de datos, mientras que el if de la línea 12 verifica la coincidencia con el elemento clave; en caso de existir, se reporta en la línea 13, pero si el elemento clave no existe, se reporta dicha situación en la línea 14.

Búsqueda binaria.
   La búsqueda binaria se fundamenta en un pre requisito sumamente importante: que el conjunto de datos esté ordenado.

   La importancia de tal pre requisito es tal que, si no se cumple, no se garantiza el buen funcionamiento de la búsqueda binaria.

   La búsqueda binaria capitaliza el hecho de que los datos estén ordenados para ir eliminando en cada iteración a la mitad de los datos, lo cual la hace muy eficiente; sin embargo, el costo a pagar es el previo ordenamiento de los datos.

   Para entender mejor el funcionamiento de la búsqueda binaria utilizaré una analogía con la forma de realizar la búsqueda de una persona en un directorio telefónico (si desconoce por completo lo que estoy diciendo, pregúntele a una persona mayor que no sea de la generación de smartphones lo que era un directorio telefónico antigüo).

   Suponga que se desea buscar en un directorio telefónico el siguiente nombre: Ricardo Ruiz Rodríguez. En México los nombres aparecen registrados en el directorio en base al primer apellido, segundo apellido y nombre(s) de las personas registradas. Supongamos también que se decide abrir el directorio por la mitad (aproximadamente), y que la lista de apellidos que vemos empieza con Murrieta.

   ¿Hacia qué parte del directorio deberíamos dirigir la búsqueda? Debería ser obvio que hacia la segunda mitad, ya que Ruiz se encuentra alfabéticamente después que Murrieta. Pues bien, ésta es en esencia la idea de la búsqueda binaria.

   El Ejemplo 6.5 muestra en las líneas 17-31 la función que implementa el mecanismo de búsqueda binaria.

   La función recibe (línea 17) un arreglo de elementos constantes a (conjunto de búsqueda), el elemento a buscar x (clave o llave), y dos números que representan las posiciones de los límites inferior (lim_inf)  y superior (lim_sup) del conjunto de búsqueda respectivamente. Estos límites en general coinciden con el índice inferior y superior del arreglo pero no necesariamente tiene que ser así, ya que el conjunto de búsqueda podría ser un subconjunto de todos los elementos.

   La expresión condicional de la estructura de repetición while de la línea 20, controla la continuidad o no de la búsqueda del elemento x sobre el arreglo a. En cuanto ya no se cumpla dicha expresión, se sabrá que el elemento clave no existe en el conjunto de búsqueda (¿por qué?), y dicha situación se reporta en la línea 30.

   La línea 20 es la fórmula del punto medio y calcula el elemento central del conjunto de búsqueda. En analogía al ejemplo del directorio telefónico, esta sería la simulación de abrirlo aproximadamente a la mitad.

   La estructura if/else anidada de las líneas 22-27 determina lo siguiente:
  • Si la clave se encontró (línea 22), se regresa su posición dentro del conjunto (línea 23).
  • Si no se encontró y hay que buscarla en la parte izquierda del conjunto (línea 24), se ajusta el límite superior (línea 25) para re definir un nuevo subconjunto de búsqueda.
  • Si la clave no es igual ni menor respecto al elemento actual (a[medio]), por tricotomía de números, no le queda otra (línea 26) más que estar (si existe) en la parte derecha del conjunto de búsqueda, por lo que se ajusta el límite inferior (línea 27) para re definir el nuevo subconjunto de búsqueda.
   Finalmente, si no se ha encontrado la clave y los límites del subconjunto de búsqueda son válidos, se repite nuevamente todo el proceso descrito pero sobre un conjunto de búsqueda con menos datos, de hecho la mitad (aproximadamente) de los datos en cada iteración.

   Probablemente el lector haya tenido una sensación parecida a un deja vu y haya venido a su mente el concepto de recursividad (si revisó las entradas correspondientes a recursividad). La búsqueda binaria puede ser abordada utilizando un enfoque recursivo y se deja como ejercicio explorar dicha situación.


Ordenamiento por burbuja.

   El ordenamiento de elementos es uno de los problemas clásicos de la computación. En esta entrada se presentará un algoritmo clave y sencillo, aunque no eficiente, para el ordenamiento de elementos: el ordenamiento por burbuja.

   Considere la siguiente secuencia de elementos:

15, -4, 0, -9, 7, 10

   Con toda seguridad, si consigue una hoja de papel y un lápiz, incluso quizá mentalmente, podrá ordenarlos ascendentemente obteniendo como resultado:

-9, -4, 0, 7, 10, 15

   Ahora bien, ¿cuál fue el procedimiento que siguió para ordenarlos?, ¿lo podría especificar y detallar para convertirlo en un algoritmo?

   No hay una única forma de ordenar una secuencia de elementos, aunque quizá la más común sea la búsqueda del elemento menor de la lista, colocarlo es su lugar, y buscar el siguiente elemento menor de la lista, colocarlo en su lugar, y continuar así sucesivamente hasta terminar con toda la secuencia de elementos a ordenar.

   El algoritmo de la burbuja se basa en ir comparando elementos contigüos, es decir, tomar el elemento i-ésimo y compararlo con el elemento i-ésimo + 1, si ese par de elementos no está ordenado, entonces se intercambian, si lo están, se dejan tal y como se encontraron.

   Para la secuencia anterior, el algoritmo de la burbuja generaría los siguientes intercambios para el primer recorrido:

15, -4, 0, -9, 7, 10
-4, 15, 0, -9, 7, 10
-4, 0, 15, -9, 7, 10
-4, 0, -9, 15, 7, 10
-4, 0, -9, 7, 15, 10
-4, 0, -9, 7, 10, 15

   Observe que aunque los elementos aún no están ordenados todavía, el mayor de ellos (15) fue intercambiándose y ascendiendo a su posición final; de hecho ésta es la razón por la cual el algoritmo recibe el nombre de burbuja, en analogía al efecto ascendente de las burbujas de aire en el agua.

   La secuencia anterior de intercambios, constituye de hecho el primer recorrido para la lista de elementos, y en principio, se necesitan n - 1 recorridos para una lista de n elementos y n - 1 comparaciones dentro de cada recorrido para saber si realiza o no el intercambio.

   Siguiendo la idea planteada hasta ahora termine de hacer los recorridos restantes, es decir: los cuatro recorridos que faltan con sus cinco iteraciones de comparación en cada uno.

   El Ejemplo 6.4 muestra un archivo de biblioteca personalizada (.h) que contiene dos versiones para el ordenamiento por burbuja. La versión que nos compete por el momento es la función definida en las líneas 23-34, la cual implementa la versión tradicional del ordenamiento por burbuja, también conocido como burbuja clásico o ingenuo.

   La estructura de repetición for de la línea 26 controla el número de veces que se recorre el arreglo, que como ya se mencionó es de n - 1 veces, donde n es el número de elementos a ordenar. Ahora bien, para una n muy grande se tiene:


   Por lo que se puede decir que un arreglo de n elementos se recorre n veces.

   Por otro lado, el ciclo for de la línea 27 realiza en general n - 1 iteraciones para comparar parejas de números candidatas a intercambio para los n elementos del arreglo, pero por la misma razón que antes establecida por el límite, es posible decir que se repite n veces. Esto quiere decir que por cada recorrido se realizan n comparaciones.

   En base a lo anterior, el ordenamiento por burbuja tiene el siguiente comportamiento general:


donde n es el número de elementos a ordenar.

   La notación de la expresión anterior se lee como ''o grande de n cuadrada", y su explicación queda fuera de los alcances de este blog; por ahora, basta con saber que el ordenamiento por burbuja tiene un comportamiento cuadrático.

Burbuja mejorado.
   Si terminó de realizar las iteraciones propuestas en párrafos anteriores para ordenar la secuencia de números que se presentó, probablemente se haya dado cuenta de que la secuencia está ordenada después de la primera iteración de comparación del tercer recorrido.

   Se deriva entonces una pregunta más que pertitente: ¿Tiene sentido realizar los recorridos e iteraciones de comparación restantes? Con toda seguridad su respuesta será negativa.

   El ordenamiento por burbuja clásico no hace ningún tipo de verificación en éste sentido, por lo que realizaría siempre todos los recorridos e iteraciones de comparación basándose única y exclusivamente en el número de elementos a ordenar aún cuando los datos estuvieran ordenados desde el principio, de ahí que al ordenamiento por burbuja clásico también se le denomine ingenuo.

   Para subsanar esta deficiencia, se pueden hacer esencialmente dos cosas basadas en las siguientes consideraciones:
  1. Después de cada recorrido, el elemento mayor de serie de elementos se encontrará ya en su posición final; en el segundo recorrido el segundo elemento mayor y así sucesivamente, ¿tiene sentido seguir comparando estos elementos que sabemos ya ordenados? La respuesta es no, por lo que si después de cada recorrido reducimos también el número de iteraciones de comparación, se mejorará la eficiencia. Ésta es precisamente la mejora que se implementa en el ciclo for de la línea 13 del Ejemplo 6.4, en donde la expresión condicional se ha modificado para que dependa también del número de veces que se ha realizado el recorrido.
  2. Ahora bien, si la serie de elementos se encuentra ya ordenada en alguna parte del proceso de ordenamiento antes de las n x n iteraciones, tampoco tendría sentido continuar. Para dicho caso se ha utilizado una bandera (línea 9), la cual asumirá que después de cada recorrido la serie de elementos estará probablemente ordenada, por lo que la bandera se apaga  (línea 12) para que la expresión condicional del ciclo for de la línea 11 sea falsa y los recorridos sobre la serie de elementos terminen; sin embargo, si la expresión condicional del if de la línea 14 se cumple para al menos una de las iteraciones de comparación, entonces la serie de elementos no está ordenada todavía y se enciende nuevamente la bandera (línea 18) para procesar al menos un nuevo recorrido.
   En el argot computacional una bandera es un indicador (centinela), y es una variable que sirve para indicar una determinada situación.

   Las banderas más comunes son las banderas binarias o booleanas, las cuales toman únicamente dos valores: 1 (verdadero) o 0 (falso).

   El lenguaje de programación C no tiene tipos de datos booleanos, por lo que tienen que ser simulados o implementados con variables de tipo entero. Cuando a una bandera booleana se le asigna verdadero, se dice que se enciende la bandera, y cuando se le asigna falso, se dice que se apaga la bandera en analogía a un interruptor (switch) de encendido/apagado.


26 de agosto de 2016

Arreglos y funciones.

   Los datos almacenados en los arreglos no siempre provienen de inicializaciones, ya que es muy usual por ejemplo, el procesar un conjunto de datos de la entrada estándar y almacenarlos en un arreglo, y dado que es una tarea muy común, convendría tener una forma de encapsular dicha labor y solicitarla en donde se necesite. El mecanismo de abstracción proporcionado por las funciones cumplen con dicho cometido.

   Con base en lo descrito con anterioridad, lo que se necesita entonces es saber cómo enviarle un arreglo a una función y cómo decirle a la función que lo que va a recibir como parámetro es un arreglo. El Ejemplo 6.3 muestra cómo hacer esto para un arreglo de enteros.

   Observe primero las líneas 9 y 10, y note que cada una de las funciones tienen en su lista de parámetros int [ ] y int. Estas declaraciones le indican al compilador que las funciones recibirán un arreglo de enteros como primer argumento, y un número entero como segundo argumento. Lo mismo sucede en las líneas 29 y 38 respectivamente.

   Continuando con los prototipos (líneas 9 y 10) y los encabezados de las definiciones de función (líneas 29 y 38), note que en ambos casos los corchetes están vacíos, esto es así debido a que no se está declarando un arreglo ni designado un tamaño, sino que se está indicando que la función recibirá un arreglo que ha sido declarado y dimensionado en alguna otra parte (comúnmente en main).

   Observe también que el nombre (identificador) del arreglo dentro de la función leeArregloInt es a, el cual coincide con el nombre del arreglo declarado en la función main en la línea 13, mientras que para la función imprimeArregloInt el nombre del arreglo es b; esto se ha hecho con la intención de mostrar que el nombre del (los) arreglo(s) dentro de las funciones puede(n) coincidir o no con el nombre del (los) arreglo(s) declarado(s) en main sin que exista algún tipo de conflicto.

   Tanto la función leeArregloInt como imprimeArregloInt pueden ser integradas dentro de una biblioteca personalizada de funciones tal y como se describió en la entrada correspondiente, con la finalidad de poder reutilizarlas cuando se necesite leer de la entrada estándar un arreglo de números enteros, o imprimir un arreglo de enteros. Una posible salida del Ejemplo 6.3 se muestra en la siguiente figura:

Una posible salida del Ejemplo 6.3.

10 de agosto de 2016

Recursividad (Definición y conceptos).

   Existen al menos dos películas que, además de ser de mi agrado, ilustran excepcionalmente bien el concepto de recursividad:
  1. Inception: aquí la recursividad se presenta debido a que dentro de un sueño, se puede estar soñando, incluso, quizá haya sabido de personas que sueñan que sueñan, eso es recursividad.
  2. Being John Malcovich: uno de los personajes encuentra una especie de túnel que permite estar por unos minutos dentro de la mente de John Malcovich, y así poder ver lo que él ve, sentir lo que él siente, etc. John Malcovich se da cuenta de ello, y se introduce en el túnel que lleva a su propia mente. La recursividad radica en que la mente de John Malcovich es invadida por él mismo.
   Recursividad es la forma de especificar un proceso en función de su propia definición, y dicho proceso puede ser finito o infinito.

   Note que la definición de recursividad es recursiva.

   Para tener la especificación de un proceso finito, la especificación debe hacerse en términos de una simplificación del problema original, de tal forma que eventualmente se alcance un caso base o criterio de paro.

   Un proceso recursivo infinito se ilustra en la siguiente figura:

Recursividad infinita (aquí sólo hasta el nivel 3).
 
    Observe que del trazo inicial (Nivel 1) se puede generar el Nivel 2, repitiendo la misma figura del Nivel 1 en cada uno de los segmentos de recta o aristas del Nivel 1. Si este proceso se vuelve a aplicar (proceso recursivo) a las nuevas aristas generadas en el Nivel 2 se genera la figura del Nivel 3; y al menos en principio, potencialmente es posible repetir dicho proceso de manera infinita.

   Para el caso del ejemplo mostrado en la figura anterior sólo se ha llegado hasta el Nivel 3, generando de manera recursiva, una figura geométrica bastante interesante. Se deja como ejercicio al lector generar la figura geométrica correspondiente de Nivel 5.

Aspectos inherentes: overhead.
   Muchas funciones matemáticas como el factorial y la serie de Fibonacci por ejemplo, resultan útiles para ilustrar el concepto de recursividad, pero esto no quiere decir que la solución recursiva sea la más apropiada, de hecho es en general más ineficiente debido a que cada llamado de función lleva implícito un costo de gestión (overhead).

   ¿Se ha puesto a reflexionar lo que implica por ejemplo, cambiar el flujo de ejecución de un programa para procesar el código de una función?, ¿qué pasa cuando se llama a una función?, ¿cómo sabe la computadora a dónde tiene que regresar después de procesar el código de una función?, ¿cómo es que no se revuelven los datos de la función que llama con la función llamada si todos los datos se procesan en el mismo microprocesador? En este sentido, considere la siguiente figura:

Mecanismo de funcionamiento del llamado y retorno de una función.
 
    Cuando ocurre el llamado de una función, la secuencia de ejecución (flujo de control) cambia, y se da un salto hacia el código que define a dicha función (lo cual se ilustra con las flechas que van de izquierda a derecha en la figura anterior); sin embargo, dicho cambio no es tan simple como parece:
  • El ámbito de una función es un nuevo contexto en todos los sentidos:
    • Hay nuevas variables.
    • Hay nuevas instrucciones y sentencias a procesar y todas ellas se ejecutan en el mismo microprocesador y utilizan el mismo conjunto de registros, por lo que antes de ejecutar la primera línea de código de una función, se debe respaldar el estado de todos los registros y la dirección de retorno (la dirección de la siguiente instrucción en la secuencia de ejecución que se estaba procesando antes del llamado a la función), y otros elementos que se utilizarán para procesar el nuevo contexto de ejecución (la función).
  • Cuando la función termina, antes de regresar el control a donde estaba antes del llamado (ámbito del invocador), se deben restaurar en los registros todos los datos respaldados para que no exista ninguna afectación en la lógica de ejecución y de procesamiento de datos del ámbito del invocador. La ejecución debe de continuar como si nunca se hubiera cambiado de flujo de ejecución (lo cual se ilustra con las flechas que van de derecha a izquierda en la figura anterior).
   Aunque la descripción anterior es una versión sumamente reducida y resumida de lo que pasa con cada llamado de función, lo importante es tener en mente que atrás del llamado de una función suceden muchas muchas cosas, y a eso es a lo que se le llama, desde el punto de vista computacional costo de gestión de funciones (overhead).

3 de agosto de 2016

Ámbitos de variables.

   El Ejemplo 4.4 ilustra el concepto de ámbito de variables, el cual se refiere al nivel de visibilidad o alcance para las variables. Para visualizar mejor el concepto, imagine el ámbito de variables como un conjunto de capas que se sobreponen, todas las capas existen, pero en un momento dado, sólo es accesible una de ellas.

   Las líneas 7-9 declaran los prototipos de tres funciones: función1, función2 y función3, ninguna de las tres regresa ningún valor ni recibe nada como parámetro.

   La línea 11 define una variable global x de tipo entero (int) que es inicializada con el valor de uno; de hecho, observe que todas las variables se llaman igual y que son del mismo tipo.

   Una variable global tiene un alcance de archivo, lo cual quiere decir que dicha variable se reconoce desde el punto de su definición, hasta el final del archivo que la contiene.

   Las variables globales no deberían ser utilizadas debido a que introducen desorden en la estructura de los programas y son susceptibles de ser modificadas de manera accidental o intencional en cualquier parte del archivo, generando con ello resultados o efectos colaterales que invariablemente sumergirán al programador en “entretenidas” sesiones de depuración.

   Además de lo anterior, las variables globales se consideran en general una mala práctica de programación. La intención del Ejemplo 4.4 es ilustrar los ámbitos incluso para las variables globales, pero no pretende de ninguna manera promover su uso.

   Retomando la explicación del ejemplo, la línea 14 declara la variable local a main con valor inicial de diez. El ámbito de ésta variable es el bloque asociado con la función main, por lo que, ¿qué valor imprimirá la función printf de la línea 16?.

   La pregunta es bastante razonable, debido a que hasta la línea 16 se tienen definidas dos variables con el mismo nombre y del mismo tipo; por lo tanto, ¿cual se imprimirá? Si bien es cierto que el alcance de una variable global es de todo el archivo, también lo es que los ámbitos se superponen entre sí, pero no se sobre escriben, es decir, que para la línea 16 el ámbito activo es el de la función main, por lo que el valor que se imprimirá será diez.

   Por otro lado, las líneas 17-20 definen un nuevo ámbito interno delimitado por su propio bloque ( { ... } ) dentro de la función main. Observe que dicho bloque no pertenece a ninguna estructura de control, ya que las llaves no son propias de ninguna estructura de control (de hecho son opcionales), sino que delimitan bloques de código y cada bloque de código tiene su propio ámbito. Éste ámbito interno declara una nueva x con valor de 50, por lo que el valor a imprimir en la línea 19 es 50. Ahora bien, ¿qué valor se imprime en la línea 21?

   La línea 23 hace un sucesivo llamado a las funciones: funcion1, funcion2 y funcion3 respectivamente, mientras que la línea 24 repite dicha sucesión. Para entender la salida del programa, se debe analizar la definición de las funciones.

   Las líneas 31-36 definen a la función funcion1, la cual declara una variable local x en la línea 32 con un valor inicial de 100. Ésta variable, como todas las que se declaran de esta forma, se denominan variables automáticas debido a que se crean y se destruyen cada vez que la secuencia de ejecución entra y sale, respectivamente, de su respectivo ámbito de función. La función imprimirá en las líneas 34 y 35, los valores de 100 y 101 respectivamente, sin importar si la función se llama una vez, tres veces, 456 o 987 veces.

   Por otro lado, las líneas 38-43 definen a la función funcion2, la cual declara una variable local estática (static) en la línea 39 con un valor inicial de 1000. El modificador static instruye al compilador para que no destruya a la variable que es afectada por el modificador cuando la secuencia de ejecución abandone el ámbito en que se definió conservando así su valor. Sin embargo, dicha variable sólo es accesible dentro de su ámbito, esto es, sólo la funcion2 la puede “ver”. Cuando una variable es estática, su valor de inicialización sólo se establece la primera vez que se hace uso de la variable, por lo que sus valores subsecuentes dependerán del último valor asignado o modificado; en éste sentido, los valores que se imprimirán en las líneas 41 y 42 en el primer llamado de la funcion2 serán 1000 y 1001 respectivamente, pero no lo serán en los llamados subsecuentes.

   Finalmente, las líneas 45-48 definen a la función funcion3, la cual no declara ninguna variable pero sí hace uso de una variable en las líneas 46 y 47. Quizá se pregunte ¿cómo es esto posible?, pues es posible debido al uso de la variable global (línea 11), por lo que los valores a imprimir son uno y dos respectivamente.

   Siguiendo la descripción que se acaba de hacer de las funciones, determine los valores que se imprimirán en la salida estándar para las líneas 24 y 26.

   La salida del Ejemplo 4.4 se muestra en la siguiente figura:

Salida del Ejemplo 4.4.

1 de agosto de 2016

Conceptos y estructura de las funciones (módulos de programa).

   Las funciones son un mecanismo de abstracción, y en el contexto de la programación estructurada, constituyen módulos o segmentos de código, que le permiten al programador encapsular determinada funcionalidad o característica en un bloque de código, con la finalidad de que éste pueda ser reutilizado y administrado de una mejor manera.

   Por otro lado, a la cualidad de escribir programas gobernados por módulos, es a lo que se le conoce como programación modular.

   La programación modular es una técnica de programación que consiste en identificar partes funcionales y reutilizables de un programa (abstracción), y encapsularlas en entidades que siguen una estructura propia. A dichas entidades se les denomina en el lenguaje de programación C como funciones.

   En C todos los módulos son funciones, a diferencia de otros lenguajes de programación, en los que los módulos pueden ser funciones, procedimientos, clases, etcétera.

   Un programa modular en C opera de la siguiente manera: la función main es la función principal (jefe), y es la encargada de delegar responsabilidades y/o solicitar servicios a otras funciones (sus subordinados), las cuales a su vez se pueden valer de otras funciones (colegas) para llevar a cabo su tarea. Lo anterior se ilustra en la siguiente figura:

Programación modular.

   Por último, pero no por ello menos importante, se debe recordar que cuando se trabaja con funciones, se requieren de tres aspectos muy importantes a considerar:
  1. Prototipo de función: es la firma de la función, y le indica al compilador el tipo de valor de retorno de la función, el nombre de la función, y el número y tipo de cada uno de los parámetros con los que trabajará.
  2. Llamado (invocación) de función: es el uso de la función, el llamado de la función se realiza a través de su nombre y de la lista de argumentos que requiere la función para trabajar.
  3. Definición de función: es en dónde se define el código que describe el funcionamiento de la función.
   Los tres aspectos anteriores mencionados son igualmente importantes. El primero de ellos se ha usado de manera implícita al incluir las bibliotecas de funciones del lenguaje, el segundo al utilizar (llamar) a las funciones de dichas bibliotecas.

   Por otro lado, la definición de funciones hasta ahora no se ha utilizado debido a que involucra la definición de las funciones escritas por el programador; resta entonces el establecer cómo definir nuestras propias funciones, lo cual se comentará en la entrada correspondiente a la Estructura para las funciones en C.