LABORATORIO : FUNCIONES(PROGRAMACION MODULAR)


10.1 Introducción
10.2 Diseño Descendente
10.3 Módulos
10.4 Prototipos
10.5 Paso de Parámetros a funciones
10.6 Ambito de las variables)
10.7 Organización de programas con módulos
10.8 Ejemplos de programas con módulos
10.9 Actividades

10.1 Introducción

Uno de los métodos más conocidos para resolver un problema es dividirlo en problemas más pequeños, llamados subproblemas. De esta manera, en lugar de resolver una tarea compleja y tediosa, resolvemos otras más sencillas y a partir de ellas llegamos a la solución. Esta técnica se usa mucho en programación ya que programar no es más que resolver problemas, y se le suele llamar diseño descendente, metodología del divide y vencerás o programación top-down.

Es natural pensar, que si esta metodología nos lleva a tratar con subproblemas de un problema, entonces también se pueda crear y trabajar con subprogramas para resolverlos. A estos subprogramas se les suele llamar módulos, de ahí viene el nombre de programación modular. En generaal se dispone de dos tipos de módulos: los procedimientos y las funciones. En lenguaje C/C++ se trabaja únicamente con funciones.

Cuando se desarrolla un programa el programador se enfrenta a varios problemas entre los cuales se encuentran los errores de sintaxis y los errores de lógica. Los errores mas difíciles de encontrar son los errores lógicos y dichos errores solo se pueden enfrentar desarrollando una Prueba de Escritorio, bien específicada. Sin embargo no es fácil concebir una prueba de escritorio cuando se ha desarrollado un algoritmo que tiene mas de 1000 líneas, dado que resulta realmente complejo seguir la ejecución de dicha prueba o sencillamente se puede hacer mal.

10.2 Diseño Descendente

El diseño descendente (top-down) es el proceso mediante el cual un problema se descompone en una serie de niveles o pasos sucesivos de refinamiento (stepwise). La metodología de diseño descendente consiste en efectuar una relación entre las sucesivas etapas de estructuración, de modo que se relacionen unas con otras mediante entradas y salidas de información.

Consideremos el siguiente problema: " Elaborar un programa que lea parejas de enteros, hasta que el usuario seleccione la opción de terminar el programa y que con cada pareja se puedan realizar las operaciones básicas de suma, resta, multiplicación y división, según determine el usuario. Adicionalmente el programa debe permitir al usuario visualizar los datos que digitó y el resultado de la operación realizada.".

Figura 1. Diseño Descendente para el problema de operaciones de enteros.

La subdivision mostrada en la figura 1 para el problema planteado, facilita el diseño del programa y permite plantear una solución mucho más fácil de comprobar.

10.3 Programación Modular

De forma general, cuando se habla de programación modular se hace referencia a funciones y procedimientos. Sin embargo, en el caso del lenguaje C/C++, solo existe el concepto de función como tal. No obstante, C/C++ soportan un tipo de función void ("sin tipo"), donde la función de dicho tipo se asemeja a un procedimiento ya que no tiene un valor de retorno. Este aspecto debe quedar más claro al considerar las definiciones de función y procedimiento.

10.3.1 Procedimientos

Un procedimiento es un subprograma que realiza una tarea específica. Para invocarlo, es decir, para hacer que se ejecute, basta con escribir su nombre en el cuerpo de otro módulo o en el programa principal. Pero, hay que tener muy en cuenta que su declaración debe hacerse antes de que sea llamado por otro módulo.

Crear un procedimiento no es complicado, pues tiene prácticamente la misma estructura que un programa. Veamos las secciones que comparten y no comparten un procedimiento y un programa principal:

En C la declaración de un procedimiento tiene la siguiente estructura:

void nombre_procedimiento (argumentos);

Y su definición:

void nombre_procedimiento (argumentos)
{
     sentencias;
}

Ejemplo 1. Declaración y definición de subrutina en lenguaje C:

void visualizar_entero(int a);

void visualizar_entero(int a)
{
    
   printf("%d",a);/*Cuerpo de la subrutina*/

}

La subrutina visualizar_entero toma un parametro entero a y lo imprime en la pantalla. Esta subrutina es de tipo void, es decir no tiene valor de retorno.

En general, la impresión de resultados o datos se realiza dentro de procedimientos o funciones void, en el caso específico del lenguaje C.

10.3.2 Funciones

Las funciones de usuario son, como su nombre indica, las que el propio usuario declara, de igual manera que declara procedimientos. Las funciones nacen con el propósito de ser subrutinas que siempre tienen que devolver algún valor.

Las dos principales diferencias entre procedimientos y funciones son:

En C la declaración de una función tiene la siguiente estructura:

tipo_devuelto nombre_funcion (argumentos);

Y su definición:

tipo_devuelto nombre_funcion (argumentos)
{
     sentencias;
     return valor;
}

Nota: A la hora de definir una función que no acepte argumentos escribiremos void en lugar de los argumentos o parámetros.

Ejemplo 2. Programa que usa una función para sumar dos enteros en lenguaje C:

#include <stdio.h>
int sumar( int a, int b);/*Declaración del prototipo de la función sumar*/
int main()
{
   printf("%d",sumar(5,6));/*Llamada a la función sumar desde la función printf */
   return EXIT_SUCCESS;
}

int sumar( int a, int b)
{
   return(a+b);
}

10.4 Prototipos

En lenguaje C/C++ un prototipo es la forma de informar al compilador sobre el valor de retorno de una función. Los prototipos también permiten que el compilador identifique cualquier conversión ilegal de tipo entre la definición y los parametros de la función. El prototipo también permite identificar cuando los argumentos no corresponden con el número de argumentos esperados.

La forma general de definición de un prototipo es:

tipo_devuelto nombre_funcion (tipo nombre_parametro1, tipo nombre_parametro2, .. , tipo nombre_parametroN);

En el ejemplo anterior, se usa el siguiente prototipo para la función sumar:

int sumar( int a, int b);

Sobra decir que el prototipo de la función debe coincidir con la implementación de la misma.

El protipo de una subrutina o función se define antes de la función main, de tal forma que al momento de usar dicha subrutina el compilador la identifique.

10.5 Paso de Parámetros a funciones

10.5.1 Parámetros por valor

Es importante destacar que en C todos los argumentos de una función se pasan por valor. Esto es, las funciones trabajan sobre copias privadas y temporales de las variables que se le han pasado como argumentos, y no directamente sobre ellas. Lo que significa que no se pueden modificar directamente desde una función las variables de la función que la ha llamado.

Ejemplo 3. Parámetros por valor. Programa que calcual un factorial

#include <stdio.h>
int factorial(int n);/*Prototipo de la función factorial*/
int main()
{
   int a=5;
   int b;
   b=factorial(a);
   printf("a=%d\n",a);
   printf("b=%d\n",b);
   return EXIT_SUCCESS;
}

int factorial(int n)
{
   int res=1;
   for (;n>1;n--)
   {
      res=res*n;
   }
   return res;
}

Como se puede ver, en este caso no se utiliza una variable temporal en la función factorial para ir calculando la solución, sino que se va disminuyendo el argumento n de entrada. Esto no influye en la variable a (que es la que se paso como argumento a la función factorial) ya que cuando se pasan los parámetros por valor se crea una copia de la variable a y no se manipula la variable que maneja la función factorial como argumento (n), en este caso los cambios ocurren únicamente sobre el valor local de n, que en principio es el valor que tiene a.

10.5.2 Parámetros por Referencia.

A la hora de pasar una variable como argumento a una función, el paso por referencia consiste en entregar como argumento un puntero a la variable, y no el contenido de la variable.

Si tratamos de modificar los valores de los argumentos de una función, estos cambios no serán vistos desde fuera de la misma. Esto es debido a que los argumentos de la función son copias de las variables reales y son almacenadas como variables locales a dicha función, desapareciendo por tanto al regresar a la rutina que hace la llamada. Se incluye un ejemplo a continuación:

Ejemplo 4. Parámetros por Referencia

Si compilamos y ejecutamos el código anterior, se obtiene el siguiente resultado:

Variable var1 = 1
Variable var1 = 1

Como era de esperar, no se ha modificado el valor de la variable var1 fuera de la función. Por esto, si queremos modificar el valor de un argumento en una función, debemos pasar este parámetro por referencia, como se muestra en el siguiente ejemplo:

Como se puede comprobar, la salida de este programa es correcta:

Variable var1 = 1
Variable var1 = 2

Si en una función quisiéramos modificar un parámetro que es un puntero, sería necesario pasar como argumento la dirección del mismo, es decir, doble indirección. Y así sucesivamente si el argumento fuera doble puntero, triple puntero, etc.

También es muy aconsejable el paso por referencia en el caso en que una función reciba como argumento una gran estructura de datos (arrays, matrices, ...), puesto que el paso por valor implicaría una copia completa de la estructura en el espacio de memoria de la función.

10.6 Ambito de las variables

10.6.1 Variables locales

Una variable local es una variable declarada dentro de un subprograma o función y, por tanto, sólo disponible dentro del subprograma o función en el cual se realiza la declaración.

10.6.2 Variables Globales

Una variable Global es una variable que puede ser utilizadas por el programa principal y por todos sus subprogramas, a pesar de que no se hayan declarado dentro de estos. Todas las variables que se declaran fuera de cualquier función, incluida el main, son globales.

10.7 Organización de programas con módulos.

Existen tres formas diferentes de organizar programas cuando se utilizan módulos:

Siempre que se pueda, debe evitarse la primera forma pues presenta problemas si no se tiene cuidado con el orden de los módulos. Al igual que el main se escribe al final, pues será el que llame al resto de módulos, aquellos módulos que llamen a otros deberán escribirse después de estos.

Entre la segunda y la tercera forma es recomendable esta última ya que permite reutilizar código.

Si en el mismo fichero que contiene el main escribimos los prototipos y los módulos, cuando en otro programa necesitemos utilizar uno de esos módulos tendremos que volver a escribirlo en el correspondiente fichero .c. Para evitar repetir trabajo inútil, lo recomendable es escribir los prototipos de los módulos que se utilicen en un fichero .h, el main en un fichero .c y los módulos en un fichero .c distinto al que contiene el main. Esto nos permitirá reutilizar estos módulos en programas futuros sin más que incluir el fichero .h.

En lenguaje C, cada vez que se utiliza un módulo (ya sea de una biblioteca o definida por el programador) es necesario conocer previamente su cabecera o prototipo. Si los prototipos están definidos en el mismo fichero que el main (forma segunda), éstos son conocidos directamente. Por el contrario, si están en otro fichero (forma tercera), será necesario incluir el fichero .h al igual que se hace con la biblioteca math.h cuando se quieren utilizar las funciones matemáticas.

Para incluir un fichero de prototipos definido por el usuario :

#include “nombre.h”

Para incluir librerías:

#include <libreria.h>

10.8 Ejemplos de Programa con Módulos

10.8.1 Ejemplo 1

Retomando el problema de operaciones con enteros, a continuación vamos a desarrollar la solución al problema usando programación modular.

Siguiendo el esquema planteado en la Figura 1, a través de la metodología de diseño descendente, tenemos la descomposición del problema inicial en varios problemas más pequeños: Elegir Opción, Leer Datos, Sumar, Restar, Multiplicar, Dividir y Visualizar Datos. Al combinar estos subprocesos en un programa tendremos la solución del problema que se nos planteó.

Podemos plantear la solución algorítmica general del problema de la siguiente manera:

Algoritmo Operaciones con enteros.

Inicio
   Var
     entero: opcion,x,y,resultado
   repetir
      opcion = leer_opcion()
      segun_sea opcion hacer
         1: x=leer_datos()
            y=leer_datos()
         2: resultado= sumar(x,y)
         3: resultado= restar(x,y)
         4: resultado= multiplicar(x,y)
         5: resultado= dividir(x,y)
         6: visualizar(resultado,x,y)
      fin_segun
   hasta_que (opcion=0)
fin

El algoritmo anterior realiza lo siguiente: dentro de una estructura cíclica repetir hasta, que se repite hasta que la opcion que se lea sea igual a cero (opción de terminar), donde permite que el usuario seleccione cual de las opciones desea realizar: 1: leer los valores de la pareja de enteros x y y; 2: realizar la suma de la pareja de enteros x y y, almacenando el resultado en la variable resultado; 3: realizar la resta de la pareja de enteros x y y, almacenando el resultado en la variable resultado; 4: realizar la multiplicación de la pareja de enteros x y y, almacenando el resultado en la variable resultado; 5: realizar la división de la pareja de enteros x y y, almacenando el resultado en la variable resultado; 6: visualiza los resultados y/o la pareja de enteros x,y.

El parrafo anterior es la descripción del problema planteado, como se puede ver es muy similar al pseudocódigo. Tal y como esta planteado, el algoritmo parece ser solución del problema. Esta es una primera ventaja de la programación modular: los programas se hacen más entendibles a partir del código. Sin embargo, aún falta por solucionar realmente el problema, es decir hace falta definir cada una de las funciones que se plantean en el algoritmo, el cual será nuestro siguiente paso.  

Función leer_opcion: Esta función lee un valor entero, lo valida y lo retorna. Los valores válidos son 1, 2, 3, 4, 5, 6 y 0.

entero funcion leer_opcion ()
inicio
   var entero: opcion /* Esta es una variable local para esta función, por lo tanto puede tener el mismo nombre que una variable del programa principal.*/
   repetir
     imprimir ('1: Leer Datos')
     imprimir ('2: Sumar')
     imprimir ('3: Restar')
     imprimir ('4: Multiplicar')
     imprimir ('5: Dividir')
     imprimir ('6: Visualizar')
     imprimir ('0: Terminar')
     imprimir ('Digite opción:')
     leer (opcion)
   hasta_que (opcion>=0 && opcion<=6)
   devolver (opcion)
fin_funcion
 

Función leer_datos: Esta función lee un valor entero, lo valida y lo retorna. Los valores válidos son cualquier entero.

entero funcion leer_datos ()
inicio
   var entero: dato /* Esta es una variable local para esta función.*/
   repetir
     imprimir ('Digite un valor entero:')
     leer (dato)
   hasta_que (ent(dato)=dato)/*Recuerde que la función ent devuelve el valor entero del argumento.*/
   devolver (dato)
fin_funcion

Función sumar: Esta función requiere dos valores enteros, como argumento, y retorna el valor de su suma.

entero funcion sumar (E entero: x,y)/* x,y: parámetros de entrada.*/
inicio
   devolver (x+y)
fin_funcion
 

Función restar: Esta función requiere dos valores enteros, como argumento, y retorna el valor de restar el segundo del primero.

entero funcion restar (E entero: x,y)
inicio
   devolver (x-y)
fin_funcion

Función multiplicar: Esta función requiere dos valores enteros, como argumento, y retorna el valor de su multiplicación.

entero funcion multiplicar (E entero: x,y)
inicio
   devolver (x*y)
fin_funcion  

Función dividir: Esta función requiere dos valores enteros, como argumento, y retorna el valor de su división.

entero funcion dividir (E entero: x,y)
inicio
   si (y<>0)
      devolver (x/y)
   fin_si
   imprimir ('Error división por cero')/* Esto solo se ejecuta si y=0, debido a que si se cumple la condición se devuelve el valor de la operación y por lo tanto ya no se ejecutan las siguientes instrucciones.*/
   devolver(0)
fin_funcion  

Procedimiento visualizar: Esta función requiere tres valores enteros, como argumento, y muestra por pantalla los datos o los resultados, según la selección del usuario. Este módulo no devuelve ningun valor al programa llamante.

procedimiento visualizar (E entero: resultado,x,y)
inicio
   var entero: opcion
   repetir
     imprimir ('1: Visualizar Datos')
     imprimir ('2: Visualizar Resultados')
     leer (opcion)
   hasta_que (opcion>=1&& opcion<=2)
   si (opcion=1)
      imprimir('Los datos actuales son:')
      imprimir('x=',x)
      imprimir('y=',y)
   si_no
      imprimir('El resultado actual es:',resultado)
   fin_si
fin_funcion

Entonces, con la definición del programa principal y los módulos planteados, se da solución al problema propuesto. A partir de la solución algoritmica vamos codificar el programa en lenguaje C:

 

10.8.2 Ejemplo 2:

Implementar en lenguaje C un algoritmo que imprima el siguiente patrón, para un n dado:

Para n=3:
*  ******  *
** **  ** **
****    ****

Para n=5:
*    **********    *
**   ****  ****   **
***  ***    ***  ***
**** **      ** ****
******        ******

El patron se consigue con secuencias de asterisco y espacios y saltos de líneas.

10.9 Actividades

Actividad 1. Del ejemplo de la sección 10.8.1, verifique si la solución allí planteada es una solución del problema enunciado.

Actividad 2. Proponga e implemente mejoras que se puedan realizar sobre el ejemplo de la sección 10.8.1.

Actividad 3. Considera usted que la solución dada al problema planteado, obtuvo una mejor solución haciendo uso de funciones?. Critique la solución y comparela con una solución sin programación modular.

Actividad 4. En que tipo de problemas considera usted que deba usarse programación modular?

Actividad 5. Mencione ventajas y desventajas de la programación modular.

Actividad 6. Mencione ventajas y desventajas de la programación estructurada.

Actividad 7. Qué aspectos de la programación modular, considera usted que deben profundizarse en una próxima sesión?


INGENIERO NESTOR DIAZ
FIET - UNICAUCA
2005