CAPITULO 5: FUNCIONES



1.INTRODUCCION

La forma más razonable de encarar el desarrollo de un programa complicado es aplicar lo que se ha dado en llamar "Programación Top - Down" .
Esto implica que, luego de conocer cual es la meta a alcanzar, se subdivide esta en otras varias tareas concurrentes, por ejemplo :
Leer un teclado, procesar datos, mostrar los resultados .
Luego a estas se las vuelve a dividir en otras menores :

 


Y así se continúa hasta llegar a tener un gran conjunto de pequeñas y simples tareas, del tipo de "leer una tecla" ó "imprimir un caracter".
Luego sólo resta abocarse a resolver cada una de ellas por separado.
De esta forma el programador, sólo se las tendrá que ver con diminutas piezas de programa, de pocas lineas, cuya escritura y corrección posterior es una tarea simple.
Tal es el criterio con que está estructurado el lenguaje C, donde una de sus herramientas fundamentales són las funciones. Todo compilador comercial trae una gran cantidad de Librerias de toda índole, matematicas, de entrada - salida, de manejo de textos, de manejo de gráficos, etc, que solucionan la mayor parte de los problemas básicos de programación .
Sin embargo será inevitable que en algún momento tenga que crear mis propias funciones, las reglas para ello son las que desarrollaremos en este capítulo .
Comencemos con algunos conceptos básicos: para hacer que las instrucciones contenidas en una función, se ejecuten en determinado momento, no es necesario más que escribir su nombre como una linea de sentencia en mi programa. Convencionalmente en C los nombres de las funciones se escriben en minúscula y siguen las reglas dadas anteriormente para los de las variables, pero deben ser seguidos, para diferenciarlas de aquellas por un par de paréntesis .
Dentro de estos paréntesis estarán ubicados los datos que se les pasan a las funciones. Está permitido pasarles uno, ninguno ó una lista de ellos separados por comas, por ejemplo: pow10( a ), getch(), strcmp( s1, s2 ) .
Un concepto sumamente importante es que los argumentos que se les envían a las funciones son los VALORES de las variables y NO las variables mismas. En otras palabras, cuando se invoca una función de la forma pow10( a ) en realidad se está copiando en el "stack" de la memoria el valor que tiene en ese momento la variable a, la función podrá usar este valor para sus cálculos, pero está garantizado que los mismos no afectan en absoluto a la variable en sí misma.
Como veremos más adelante, es posible que una función modifique a una variable, pero para ello, será necesario comunicarle la DIRECCION EN MEMORIA de dicha variable .
Las funciones pueden ó no devolver valores al programa invocante. Hay funciones que tan sólo realizan acciones, como por ejemplo clrscr(), que borra la pantalla de video, y por lo tanto no retornan ningun dato de interés; en cambio otras efectuan cálculos, devolviendo los resultados de los mismos.
La invocación a estos dos tipos de funciones difiere algo, por ejemplo escribiremos :

clrscr() ;
c = getch() ;

donde en el segundo caso el valor retornado por la función se asigna a la variable c. Obviamente ésta deberá tener el tipo correcto para alojarla .

2. DECLARACION DE FUNCIONES
Antes de escribir una función es necesario informarle al Compilador los tamaños de los valores que se le enviarán en el stack y el tamaño de los valores que ella retornará al programa invocante .
Estas informaciones están contenidas en la DECLARACION del PROTOTIPO DE LA FUNCION.
Formalmente dicha declaración queda dada por :

  tipo del valor de retorno   nombre_de_la_función(lista de tipos de parámetros)

Pongamos algunos ejemplos :

float mi_funcion(int i, double j ) ;
double otra_funcion(void) ;
       otra_mas(long p) ;
void   la_ultima(long double z, char y, int x, unsigned long w) ;


El primer término del prototipo da, como hemos visto el tipo del dato retornado por la función; en caso de obviarse el mismo se toma, por omisión, el tipo int. Sin embargo, aunque la función devuelva este tipo de dato, para evitar malas interpretaciones es conveniente explicitarlo .
Ya que el "default" del tipo de retorno es el int, debemos indicar cuando la función NO retorna nada, esto se realiza por medio de la palabra VOID ( sin valor).
De la misma manera se actúa, cuando no se le enviarán argumentos.
Más adelante se profundizará sobre el tema de los argumentos y sus características.
La declaración debe anteceder en el programa a la definición de la función. Es normal, por razones de legibilidad de la documentación, encontrar todas las declaraciones de las funciones usadas en el programa, en el HEADER del mismo, junto con los include de los archivos *.h que tienen los prototipos de las funciones de Librería.
Si una ó más de nuestras funciones es usada habitualmente, podemos disponer su prototipo en un archivo de texto, e incluirlo las veces que necesitemos, según se vio en capítulos previos.

3. DEFINICION DE LAS FUNCIONES
La definición de una función puede ubicarse en cualquier lugar del programa, con sólo dos restricciones: debe hallarse luego de dar su prototipo, y no puede estar dentro de la definición de otra función ( incluida main() ). Es decir que a diferencia de Pascal, en C las definiciones no pueden anidarse.
NOTA: no confundir definición con llamada; una función puede llamar a tantas otras como desee .
La definición debe comenzar con un encabezamiento, que debe coincidir totalmente con el prototipo declarado para la misma, y a continuación del mismo, encerradas por llaves se escribirán las sentencias que la componen; por ejemplo:

#include <stdio.h>
float mi_funcion(int i, double j ); /* DECLARACION observe que termina en  ";"  */
main()
{
float k ;
int p ;
double z ;
...........
k = mi_funcion( p, z );             /* LLAMADA a la función */
...........
}                                     /* fin de la función main() */
float mi_funcion(int i, double j )   /* DEFINICION  observe que NO lleva  ";"  */
{
float n
...................
printf("%d", i );                 /* LLAMADA  a otra función */
...................
return ( 2 * n );                  /* RETORNO devolviendo un valor float */
}

Pasemos ahora a describir más puntualmente las distintas modalidades que adoptan las funciones .

4. FUNCIONES QUE NO RETORNAN VALOR NI RECIBEN PARAMETROS
Veamos como ejemplo la implementacion de una funcion "pausa"


#include <stdio.h>
void pausa(void) ;
main()
{
  int contador = 1;
  printf("VALOR DEL CONTADOR DENTRO DEL while \n");
  while (contador <= 10) {
    if(contador == 5 ) pausa();
    printf("%d\n", contador++);
  }
  pausa() ;
  printf("VALOR DEL CONTADOR LUEGO DE SALIR DEL while: %d", contador) ;
  return 0;
}
void pausa(void)
{
char c ;
printf("\nAPRIETE ENTER PARA CONTINUAR ") ;
while( (c = getchar()) != '\n') ;
}

Analicemos lo hecho, en la segunda linea hemos declarado la función pausa, sin valor de retorno ni parámetros.
Luego esta es llamada dos veces por el programa principal, una cuando contador adquiere el valor de 5 (antes de imprimirlo) y otra luego de finalizar el loop.
Posteriormente la función es definida. El bloque de sentencias de la misma está compuesto, en este caso particular, por la definición de una variable c, la impresión de un mensaje de aviso y finalmente un while que no hace nada, solo espera recibir un caracter igual a <ENTER>.
En cada llamada, el programa principal transfiere el comando a la función, ejecutandose, hasta que ésta finalice, su propia secuencia de instrucciones. Al finalizar la función esta retorna el comando al programa principal, continuandose la ejecución por la instrucción que sucede al llamado .
Si bien las funciones aceptan cualquier nombre, es una buena técnica de programación nombrarlas con términos que representen, aunque sea vagamente, su operatoria .
Se puede salir prematuramente de una función void mediante el uso de RETURN, sin que este sea seguido de ningun parámetro ó valor .

5. FUNCIONES QUE RETORNAN VALOR
Analicemos por medio de un ejemplo dichas funciones :


#include <stdio.h>
#include <conio.h>
#define FALSO 0
#define CIERTO 1
int finalizar(void);
int lea_char(void) ;
main()
{
  int i = 0;
  int fin = FALSO;
  printf("Ejemplo de Funciones que retornan valor\n");
  while (fin == FALSO) {
    i++;
    printf("i == %d\n", i);
    fin = finalizar();
  }
  printf("\n\nFIN DEL PROGRAMA........");
  return 0;
}
int finalizar(void)
{
  int c;
  printf("Otro número ? (s/n) ");
  do {
    c = lea_char() ;
  } while ((c != 'n') && (c != 's'));
  return (c == 'n');
}
int lea_char(void)
{
int j ;
if( (j = getch()) >>= 'A' && j <<= 'Z' )
 return( j + ( 'a' - 'A') ) ;
else
 return j ;
}

Analicemos paso a paso el programa anterior; las dos primeras lineas incluiran, en el programa los prototipos de las funciones de librería usadas, ( en este caso printf() y getch() ). En las dos siguientes damos nombres simbólicos a dos constantes que usaremos en las condiciones lógicas y posteriormente damos los prototipos de dos funciones que hemos creado.
Podrían haberse obviado, en este caso particular, estas dos últimas declaraciones, ya que ambas retornan un int (default), sin embargo el hecho de incluirlas hará que el programa sea más facilmente comprensible en el futuro.
Comienza luego la función main(), inicializando dos variables, i y fin, donde la primera nos servirá de contador y la segunda de indicador lógico. Luego de imprimir el rótulo del programa, entramos en un loop en el que permaneceremos todo el tiempo en que fin sea FALSO.
Dentro de este loop, incrementamos el contador, lo imprimimos, y asignamos a fin un valor que es el retorno de la función finalizar() .
Esta asignación realiza la llamada a la función, la que toma el control del flujo del programa, ejecutando sus propias instrucciones.
Saltemos entonces a analizar a finalizar(). Esta define su variable propia, c, (de cuyas propiedades nos ocuparemos más adelante) y luego entra en un do-while, que efectúa una llamada a otra función, lea_char(), y asigna su retorno a c iterando esta operativa si c no es 'n' ó 's', note que: c != 'n' && c != 's' es equivalente a: !( c == 'n' || c == 's' ) .
La función lea_char() tiene como misión leer un caracter enviado por el teclado, ( lo realiza dentro de la expresión relacional del IF ) y salvar la ambigüedad del uso de mayúsculas ó minúsculas en las respuestas, convirtiendo las primeras en las segundas. Es facil de ver que, si un caracter esta comprendido entre A y Z, se le suma la diferencia entre los ASCII de las minúsculas y las mayúsculas ( 97 - 65 = 32 ) para convertirlo, y luego retornarlo al invocante.
Esta conversión fué incluida a modo de ejemplo solamente, ya que existe una de Librería, tolower() declarada en ctype.h, que realiza la misma tarea.
Cuando lea_char() devuelva un caracter n ó s, se saldrá del do-while en la función finalizar() y se retornará al programa principal, el valor de la comparación lógica entre el contenido de c y el ASCII del caracter n. Si ambos son iguales, el valor retornado será 1 (CIERTO) y en caso contrario 0 ( FALSO ) .
Mientras el valor retornado al programa principal sea FALSO, este permanecerá dentro de su while imprimiendo valores sucesivos del contador, y llamadas a las funciones, hasta que finalmente un retorno de CIERTO ( el operador presionó la tecla n) hace terminar el loop e imprimir el mensaje de despedida.
Nota: preste atención a que en la función finalizar() se ha usado un do-while .¿Cómo modificaría el programa para usar un while ?. En la función lea_char se han usado dos returns, de tal forma que ella sale por uno u otro. De esta manera si luego de finalizado el else se hubiera agregado otra sentencia, esta jamás sería ejecutada.
En el siguiente ejemplo veremos funciones que retornan datos de tipo distinto al int.
Debemos presentar antes, otra función muy común de entrada de datos: scanf(), que nos permitirá leer datos completos (no solo caracteres) enviados desde el teclado, su expresión formal es algo similar a la del printf() ,

scanf("secuencia de control", dirección de la variable ) ;

Donde en la secuencia de control se indicará que tipo de variable se espera leer, por ejemplo :

%d  si se desea leer un entero decimal      (int)
%o  "  "    "    "   "    "    octal          "
%x  "  "    "    "   "    "    hexadecimal    "
%c  "  "    "    "   "  caracter
%f   leerá un flot
%ld  leerá un long int
%lf  leerá un double
%Lf  leerá un long double


Por "dirección de la variable" deberá entenderse que se debe indicar, en vez del nombre de la variable en la que se cargará el valor leido, la dirección de su ubicación en la memoria de la máquina. Esto suena sumamente apabullante, pero por ahora solo diremos, (más adelante abundaremos en detalles ) que para ello es necesario simplemente anteponer el signo & al nombre de la misma .
Así, si deseo leer un entero y guardarlo en la variable "valor_leido" escribiré: scanf("%d",&valor_leido); en cambio si deseara leer un entero y un valor de punto flotante será: scanf("%d %f", &valor_entero, &valor_punto_flotante) ;
El tipo de las variables deberá coincidir EXACTAMENTE con los expresados en la secuencia de control, ya que de no ser así, los resultados son impredecibles.
Por supuesto esta función tiene muchísimas más opciones, ( consulte el Manual de Librerias de su Compilador, si tiene curiosidad ) sin embargo, por simplicidad, por ahora nos conformaremos con las antedichas.
El prototipo de scanf() esta declarado en stdio.h .
Usaremos también otra función, ya citada, clrscr(). Recordemos que esta es solo válida para máquinas tipo PC compatibles y no corre bajo Windows.
Encaremos ahora un programa que nos presente primero, un menú para seleccionar la conversión de ºC a Fahrenheit ó de centímetros a pulgadas, hecha nuestra elección, nos pregunte el valor a convertir y posteriormente nos de el resultado .
Si suponemos que las funciones que usaremos en el programa serán frecuentemente usadas, podemos poner las declaraciones de las mismas, así como todas las contantes que usen, en un archivo texto, por ejemplo convers.h. Este podrá ser guardado en el subdirectorio donde están todos los demás (INCLUDE) ó dejado en el directorio activo, en el cual se compila el programa fuente de nuestro problema. Para variar, supongamos que esto último es nuestro caso .

CONVERS.H

#include <conio.h>
#define FALSO  0 
#define CIERTO 1 
#define CENT_POR_INCH  25.4 
void pausa(void)                 ;
void mostrar_menu(void)          ;
int seleccion(void)              ;
void cm_a_pulgadas(void)         ;
void grados_a_fahrenheit(void)   ;
double leer_valor(void)          ;

Vemos que un Header puede incluir llamadas a otros (en este caso conio.h). Hemos puesto tambien la definición de todas las constantes que usaran las funciones abajo declaradas. De dichas declaraciones vemos que usaremos funciones que no retornan nada, otra que retorna un entero y otra que devuelve un double .
Veamos ahora el desarrollo del programa en sí. Observe que la invocación a conversión.h se hace con comillas, por haber decidido dejarlo en el directorio activo .


#include <stdio.h>
#include "convers.h"
main()
{
  int fin = FALSO;
  while (!fin) {
    mostrar_menu();
    switch(seleccion()) {
      case 1:
        cm_a_pulgadas();
        break;
      case 2:
        grados_a_fahrenheit();
        break;
      case 3:
        fin = CIERTO;
        break;
      default:
        printf("\n¡Error en la Seleccion!\a\a\n");
        pausa() ;
    }
  }
  return 0;
}
/* Funciones */
void pausa(void)
{
char c = 0;
printf("\n\n\nAPRIETE ENTER PARA CONTINUAR ") ;
while( (c = getch()) != '\r') ;
}
void mostrar_menu(void)
{
  clrscr();
  printf("\n         Menu\n");
  printf("---------------------------\n");
  printf("1:  Centimetros a pulgadas\n");
  printf("2:  Celsius a Fahrenheit\n");
  printf("3:  Terminar\n");
}
int seleccion(void)
{
  printf("\nEscriba el número de su Selección: ");
  return (getche() - '0');
}
void cm_a_pulgadas(void)
{
  double centimetros;  /* Guardará el valor pasado por leer_valor() */
  double pulgadas    ; /* Guardará el valor calculado */
  printf("\nEscriba los Centimetros a convertir: ");
  centimetros = leer_valor();
  pulgadas = centimetros  * CENT_POR_INCH;
  printf("%.3f Centimetros = %.3f Pulgadas\n", centimetros, pulgadas);
  pausa()   ;
}
void grados_a_fahrenheit(void)
{
  double grados;  /* Guardará el valor pasado por leer_valor() */
  double fahrenheit  ; /* Guardará el valor calculado */
  printf("\nEscriba los Grados a convertir: ");
  grados = leer_valor();
  fahrenheit = (((grados * 9.0)/5.0) + 32.0) ;
  printf("%.3f Grados = %.3f Fahrenheit", grados, fahrenheit);
  pausa();
}
double leer_valor(void)
{
  double valor;  /* Variable para guardar lo leido del teclado */
  scanf("%lf", &valor);
  return valor;
}

Veamos que hemos hecho: primero incluimos todas las definiciones presentes en el archivo convers.h que habiamos previamente creado. Luego main() entra en un loop, que finalizará cuando la variable fin tome un valor CIERTO, y dentro del cual lo primero que se hace es llamar a mostrar_menú(), que pone los rótulos de opciones .
Luego se entra en un SWITCH que tiene como variable ,el retorno de la función selección() (recuerde que tiene que ser un entero), según sea éste se saldrá por alguno de los tres CASE. Observe que selección() lee el teclado mediante un getche(), (similar a getch() antes descripta, pero con la diferencia que aquella hace eco del caracter en la pantalla) y finalmente devuelve la diferencia entre el ASCII del número escrito menos el ASCII del número cero, es decir, un entero igual numericamente al valor que el operador quizo introducir .
Si este fue 1, el SWITCH invoca a la función cm_a_pulgadas() y en caso de ser 2 a grados_a_fahrenheit() .
Estas dos últimas proceden de igual manera: indican que se escriba el dato y pasan el control a leer_valor(), la que mediante scanf() lo hace, retornando en la variable valor, un double, que luego es procesado por aquellas convenientemente. Como hasta ahora la variable fin del programa principal no ha sido tocada, y por lo tanto continua con FALSO ,la iteración del while sigue realizandose, luego que se ejecuta el BREAK de finalización del CASE en cuestión. En cambio, si la selección() hubiera dado un resultado de tres, el tercer case, la convierte en CIERTO, con lo que se finaliza el WHILE y el programa termina.
Vemos en este ejemplo, la posibilidad de múltiples llamados a funciones, una llama a otra, que a su vez llama a otra, la cual llama a otra, etc ,etc, dando un esquema de flujo de programa de la forma :

 

6. AMBITO DE LAS VARIABLES (SCOPE)

VARIABLES GLOBALES
Hasta ahora hemos diferenciado a las variable segun su "tipo" (int, char double, etc), el cual se refería, en última instancia, a la cantidad de bytes que la conformaban. Veremos ahora que hay otra diferenciación de las mismas, de acuerdo a la clase de memoria en la que residen .
Si definimos una variable AFUERA de cualquier función (incluyendo esto a main() ), estaremos frente a lo denominado VARIABLE GLOBAL. Este tipo de variable será ubicada en el segmento de datos de la memoria utilizada por el programa, y existirá todo el tiempo que esté ejecutandose este .
Este tipo de variables son automaticamente inicializadas a CERO cuando el programa comienza a ejecutarse .
Son accesibles a todas las funciones que esten declaradas en el mismo, por lo que cualquiera de ellas podrá actuar sobre el valor de las mismas. Por ejemplo :



#include <stdio.h>
double una_funcion(void);
double variable_global ;
main()
{
double i ;
printf("%f", variable_global );       /* se imprimirá 0 */
i = una_funcion() ;
printf("%f", i );                     /* se imprimirá 1 */
printf("%f", variable_global );       /* se imprimirá 1 */
variable_global += 1 ;
printf("%f", variable_global );       /* se imprimirá 2 */
return 0 ;
}
double una_funcion(void)
{
return( variable_global  += 1) ;
}


Observemos que la variable_global está definida afuera de las funciones del programa, incluyendo al main(), por lo que le pertenece a TODAS ellas. En el primer printf() del programa principal se la imprime, demostrandose que está automaticamente inicializada a cero .
Luego es incrementada por una_funcion() que devuelve ademas una copia de su valor, el cual es asignado a i ,la que, si es impresa mostrará un valor de uno, pero tambien la variable_global ha quedado modificada, como lo demuestra la ejecución de la sentencia siguiente. Luego main() tambien modifica su valor , lo cual es demostrado por el printf() siguiente.
Esto nos permite deducir que dicha variable es de uso público, sin que haga falta que ninguna función la declare, para actuar sobre ella.
Las globales son a los demás tipos de variables, lo que el GOTO es a los otros tipos de sentencias .
Puede resultar muy difícil evaluar su estado en programas algo complejos, con múltiples llamados condicionales a funciones que las afectan, dando comunmente orígen a errores muy engorrosos de corregir .

VARIABLES LOCALES
A diferencia de las anteriores, las variables definidas DENTRO de una función, son denominadas VARIABLES LOCALES a la misma, a veces se las denomina también como AUTOMATICAS, ya que son creadas y destruídas automaticamente por la llamada y el retorno de una función, respectivamente .
Estas variables se ubican en la pila dinámica (stack) de memoria ,destinandosele un espacio en la misma cuando se las define dentro de una función, y borrándose cuando la misma devuelve el control del programa, a quien la haya invocado.
Este método permite que, aunque se haya definido un gran número de variables en un programa, estas no ocupen memoria simultaneamente en el tiempo, y solo vayan incrementando el stack cuando se las necesita, para luego, una vez usadas desaparecer, dejando al stack en su estado original .
El identificador ó nombre que se la haya dado a una variable es sólo relevante entonces, para la función que la haya definido, pudiendo existir entonces variables que tengan el mismo nombre, pero definidas en funciones distintas, sin que haya peligro alguno de confusión .
La ubicación de estas variables locales, se crea en el momento de correr el programa, por lo que no poseen una dirección prefijada, esto impide que el compilador las pueda inicializar previamente. Recuerdese entonces que, si no se las inicializa expresamente en el momento de su definición, su valor será indeterminado (basura) .

VARIABLES LOCALES ESTATICAS
Las variables locales vistas hasta ahora, nacen y mueren con cada llamada y finalización de una función, sin embargo muchas veces sería util que mantuvieran su valor, entre una y otra llamada a la función sin por ello perder su ámbito de existencia, es decir seguir siendo locales sólo a la función que las defina. En el siguiente ejemplo veremos que esto se consigue definiendo a la variable con el prefacio static.

VARIABLES DE REGISTRO
Otra posibilidad de almacenamiento de las variables locales es, que en vez de ser mantenidas en posiciones de la memoria de la computadora, se las guarde en registros internos del Microprocesador que conforma la CPU de la misma .
De esta manera el acceso a ellas es mucho más directo y rápido, aumentando la velocidad de ejecución del programa. Se suelen usar registros para almacenar a los contadores de los FOR, WHILE, etc.
Lamentablemente, en este caso no se puede imponer al compilador, este tipo de variable, ya que no tenemos control sobre los registros libres en un momento dado del programa, por lo tanto se SUGIERE, que de ser posible, ubique la variable del modo descripto. El prefacio en éste caso será :

register int var_reg ;

Hay que recalcar que esto es sólo válido para variables LOCALES, siendo imposible definir en un registro a una global. Por otra parte las variables de registro no son accesibles por dirección, como se verá más adelante .

VARIABLES EXTERNAS
Al DEFINIR una variable, como lo hemos estado haciendo hasta ahora, indicamos al compilador que reserve para la misma una determinada cantidad de memoria, (sea en el segmento de memoria de datos, si es global ó en el stack, si es local), pero debido a que en C es normal la compilación por separado de pequeños módulos, que componen el programa completo, puede darse el caso que una función escrita en un archivo dado, deba usar una variable global definida en otro archivo. Bastará para poder hacerlo, que se la DECLARE especificando que es EXTERNA a dicho módulo, lo que implica que está definida en otro lado .
Supongamos que nuestro programa está compuesto por sólo dos módulos: mod_prin.c y mod_sec.c los cuales se compilarán y enlazarán juntos, por medio del compilador y el linker, por ejemplo corriendo: bcc mod_prin.c mod_sec.c si usaramos el compilador de Borland .
Si en el primer módulo (mod_prin.c) aparece una variable global, definida como

double var1 = 5 ;

El segundo módulo, ubicado en un archivo distinto de aquel, podrá referenciarla mediante la declaración de la misma :

extern double var1 ;

Notesé que la inialización de la variable sólo puede realizarse en su DEFINICION y no en la declaración. Esta última, no reserva memoria para la variable sino que sólo hace mención que la misma ha sido definida en otro lado .

Será finalmente el Linker el que resuelva los problemas de direccionamiento de la variable al encadenar los dos módulos compilados .

7. ARGUMENTOS Y PARAMETROS DE LAS FUNCIONES
Supongamos que en un determinado programa debemos calcular repetidamente el valor medio de dos variables, una solución razonable sería crear una función que realice dicho cálculo, y llamarla cada vez que se necesite. Para ello será necesario, en cada llamada, pasarle los valores de las variables para que calcule su valor medio. Esto se define en la declaración de la funcion especificando, no solo su valor de retorno sino también el tipo de argumentos que recibe :

double valor_medio(double x, double y) ;

de esta declaración vemos que la función valor_medio recibe dos argumentos ( x e y ) del tipo double y devuelve un resultado de ese mismo tipo .
Cuando definamos a la función en sí, deberemos incluir parámetros para que alberguen los valores recibidos, así escribiremos:

double valor_medio(double x, double y )
{
return ( (x + y) / 2.0 )
}

NOTA: No es necesario que los NOMBRES de los párametros coincidan con los declarados previamente, es decir que hubiera sido equivalente escribir: double valor_medio(double a, double b) etc, sin embargo es una buena costumbre mantenerlos igual. En realidad en la declaración de la función, no es necesario incluir el nombre de los parámetros, bastaría con poner solo el tipo, sin embargo es práctica generalizada, explicitarlos a fin de hacer más legible al programa .
Aquí estamos utilizando la síntaxis moderna del lenguaje C, pudiendose encontrar en versiones arcaicas, definiciones equivalentes como :

double valor_medio()      ó     double valor_medio(double, double)
double x;                      double x ;
double y;                      double y ;
{                               {
............                    ..............

Sin embargo es preferible utilizar la nomenclatura moderna, ya que esta facilita la rápida comprensión del programa .
Veamos un ejemplo, para determinar el comportamiento de los parámetros, Supongamos desear un programa que calcule el valor medio de dos variables incrementadas en un valor fijo, es decir:

( ( x + incremento ) + ( y + incremento ) ) / 2.0

Lo podríamos resolver de la siguiente forma :


#include <stdio.h>
/* Declaración de la función y el tipo de sus parámetros */
double valor_medio(double p_valor, double s_valor, double inc) ;
main()
{
double x, y, z, resultado ;
printf("Ingrese el primer valor: ") ;
scanf("%lf", &x ) ;
printf("\nIngrese el segundo valor: ");
scanf("%lf", &y ) ;
printf("\nIngrese el incremento    : ");
scanf("%lf", &z) ;
resultado = valor_medio( x, y, z );     /* llamada a la función y
                                              pasaje de argumentos   */
printf("\n\nResultado de la operación: %lf", resultado) ;
printf("\n\nValor con que quedaron las variables: ") ;
printf("\n Primer valor : %lf ", x ) ;
printf("\n Segundo valor: %lf ", y ) ;
printf("\n Incremento   : %lf ", z ) ;
}
/* Definición de la función y sus parámetros */
double valor_medio( double p_valor, double s_valor, double inc )
{
p_valor  += inc ;
s_valor += inc ;
return ( (p_valor + s_valor ) / 2.0 ) ;

}


Veamos primero cual seria la salida de pantalla de este programa :


SALIDA DEL EJEMPLO
Ingrese el primer valor:         [SUPONGAMOS ESCRIBIR: 10.0]
Ingrese el segundo valor:       [             "                     "       :  8.0]
Ingrese el incremento    :       [             "                     "       :  2.0]
Resultado de la operación: 11.000000
Valor con que quedaron las variables: 
 Primer valor : 10.000000 
 Segundo valor: 8.000000 
 Incremento   : 2.000000 

 

Vemos que luego de obtenidos, mediante scanf(), los tres datos x, y, z, los mismos son pasados a la función de calculo en la sentencia de asignación de la variable resultado. La función inicializa sus parámetros ( p_valor, s_valor e inc ) con los valores de los argumentos enviados ( x, y, z ) y luego los procesa. La unica diferencia entre un argumento y una variable local, es que ésta no es inicializada automaticamente, mientras que aquellos lo són, a los valores de los argumentos colocados en la expresión de llamada.
Acá debemos remarcar un importante concepto: éste pasaje de datos a las funciones, se realiza COPIANDO el valor de las variables en el stack y No pasandoles las variables en sí. Esto se denomina: PASAJE POR VALOR y garantiza que dichas variables no sean afectadas de ninguna manera por la función invocada. Una clara prueba de ello es que, en la función valor_medio() se incrementa p_valor y s_valor, sumandoseles el contenido del parámetro inc. Sin embargo cuando, luego de retornar al programa principal, imprimimos las variables cuyos valores fueron enviados como parametros, vemos que conservan sus valores iniciales. Veremos más adelante que otras estructuras de datos pueden ser pasadas a las funciones por direcciones en vez de por valor, pudiendo aquellas modificarlas a gusto .
Debe aclararse que, el pasaje de argumentos, es también una OPERACION, por lo que las variables pasadas quedan afectadas por las reglas de Conversión Automática de Tipo, vistas en el Capítulo 2. Como ejemplo, si x hubiera sido definida en la función main() como int, al ser pasada como argumento a valor_medio() sería promovida a double. Especial cuidado debe tenerse entonces con los errores que pueden producirse por redondeo ó truncamiento, siendo una buena técnica de programación hacer coincidir los tipos de los argumentos con los de los parámetros.

CAPITULO ANTERIOR DEL CURSO
PROXIMO CAPITULO DEL CURSO



Other links: Henri Cartier-Bresson prints and posters