Capitulo 03
Capitulo 03
Unidad III
Funciones
Cátedra:
Computación II (ELO)
Informática II (BIO)
COMPUTACIÓN II (ELO) - INFORMÁTICA II (BIO) Introducción al Lenguaje de Programación C
INDICE
3 Funciones .............................................................................................................................................. 1
3.1 Introducción.................................................................................................................................... 1
3.2 Definición de una función .............................................................................................................. 2
3.3 Paso de argumentos a una función ................................................................................................. 5
3.4 Paso de arreglos a funciones .......................................................................................................... 5
3.5 Recursividad................................................................................................................................. 10
COMPUTACIÓN II (ELO) - INFORMÁTICA II (BIO)
Capítulo 3
3 Funciones
3.1 Introducción
El uso de funciones definidas por el programador permite dividir un programa grande en un
cierto número de componentes más pequeños, cada uno de las cuales posee un propósito
único e identificable. Por lo tanto, un programa en C se puede modularizar mediante el uso
inteligente de las funciones, y esto resulta útil para la depuración del programa, para evitar
redundancia de código, para generación de bibliotecas propias para posteriormente usarlas en
otros programas, etc.
Igualmente importante es la claridad lógica resultante de la descomposición de un programa en
varias funciones concisas, representando cada función alguna parte bien definida del problema
global. Estos programas son más fáciles de escribir y depurar, y su estructura lógica es más
fácil de entender que la de aquellos que adolecen de este tipo de estructuras. La mayoría de
los programas en C se modularizan de esta manera, aun cuando no impliquen la ejecución
repetida de las mismas tareas. De hecho, la descomposición de un programa en módulos
individuales y perfectamente definidos es la parte básica de las técnicas de análisis y diseño de
programas, tal como se vió al estudiar la metodología Top/Down.
El análisis Top/Down de un problema permite obtener sus bloques constitutivos de forma
simple y eficiente, a la vez que proporciona sólidas guías para encarar la modularización de un
programa por medio de funciones. Como ya se vió en la asignatura Computación I (ítem 6.10
Subprogramas), el algoritmo solución de cada uno de los bloques encontrados es factible de
ser encerrado en una “cápsula” que lo aisla del resto de los módulos, que le provee los
recursos necesarios para llevar a cabo su tarea y que define las vías de comunicación que la
cápsula tendrá con el exterior. Y cada uno de estos “algoritmos encapsulados” no es más que
lo que se denomina función.
Así, una función es un segmento de programa que realiza determinadas tareas bien definidas,
y todo programa en C consta de una o más funciones. Una de estas funciones se debe llamar
main. La ejecución del programa siempre comenzará por las instrucciones contenidas en main.
El uso de funciones dentro de un programa en C consta de tres partes perfectamente definidas:
1. Declaración de la función: Con esta estructura le indicamos al compilador que existe una
función definida por el usuario. Normalmente a la declaración de una función se la
denomina prototipo de función.
2. Definición de la función: Esta parte contiene el código ejecutable de la misma. Aquí es
donde se define la tarea (qué y cómo) que realizará la función.
3. Invocación o llamada de una función: Es cuando se la función se pone a trabajar.
Si un programa contiene varias funciones, sus definiciones pueden aparecer en cualquier
orden, pero deben ser independientes unas de otras. Esto es, una definición de una función no
puede estar incluida en otra. Cuando se accede a una función desde alguna determinada parte
del programa (o sea, cuando se invoca la función), se ejecutan todas las instrucciones de que
consta. Se puede acceder a una función desde distintos lugares de un programa.
1
COMPUTACIÓN II (ELO) - INFORMÁTICA II (BIO)
Una vez que se ha completado la ejecución de una función, se devuelve el control al punto
desde el que se accedió a ella. Generalmente, una función procesará la información que le es
pasada desde el punto del programa en donde se accede a ella y devolverá un solo valor.
La información se le pasa a la función mediante identificadores especiales denominados
argumentos (también llamados parámetros) y es devuelta por la oración return. Sin embargo,
algunas funciones aceptan información pero no devuelven nada (void), mientras que otras
devuelven un valor a través de la instrucción return.
return prod;
}
Si el tipo de datos especificado en la primera línea de la definición de la función es
inconsistente con la expresión de retorno, el compilador intentará convertir la cantidad
representada por la expresión al tipo de datos especificado en la primera línea. El resultado de
esto puede ser un error de compilación o una pérdida precisión (por ejemplo, debido al
truncamiento). En cualquier caso se deben evitar incongruencias de este tipo.
La siguiente definición de función es idéntica a la del ejemplo anterior, excepto en la primera
línea, en la que no aparece el especificador de tipo.
factorial(unsigned int n) {
int i;
long int prod=1;
if(n>1) {
for(i=2,i<=n,++i)
prod *=i;
}
return prod;
}
La función devolverá una cantidad entera, ya que no hay una declaración explícita del tipo de
datos en la primera línea de la definición. Sin embargo, la cantidad que se va a devolver (prod)
se declara como un entero largo. El resultado de esto puede ser un error. Se puede evitar el
problema añadiendo una declaración de tipo long int en la primera línea de la definición de la
función como en el ejemplo anterior.
La mayoría de los compiladores C permiten que aparezca la palabra clave void como
especificador de tipo de datos cuando se define una función que no devuelve nada. Es una
buena práctica de programación utilizar void cuando se dispone de ella.
El siguiente ejemplo calcula el máximo de dos cantidades enteras:
void maximo(int x , int y) {
/* Definición de una función que no retorna valor */
int z;
z = ( x >= y) ? x : y;
printf("\n\nValor maximo = %d",z);
}
El lenguaje C no permite definir una función dentro de otra. Si se invoca una función desde
una parte del programa anterior a la definición de tal función, debe entonces aparecer en el
módulo invocante la declaración de la función invocada (declaración por adelanto o declaración
forward), esto informará al compilador que la función será llamada antes de que sea definida.
Como ejemplo consideremos la función que convierte caracteres de minúscula a mayúscula.
# include <stdio.h>
/* declaración de la función */
char minusc_a_mayusc(char minusc);
int main( ) {
char minusc, mayusc;
printf("Introducir una letra minúscula: ");
scanf("%c", &minusc);
/* llamada a la función, con parámetros actuales */
mayusc = minusc_a_mayusc(minusc);
4
COMPUTACIÓN II (ELO) - INFORMÁTICA II (BIO)
El siguiente ejemplo pasa un arreglo de tres elementos enteros a una función donde estos
elementos son alterados. Los valores de los elementos del arreglo se escriben en tres partes del
programa para ilustrar los efectos de las alteraciones.
#include <stdio.h>
void modificar(int datos[]); /* Declaración de la función */
int main( ) {
int cont, a[3]; /* Definición del arreglo */
printf("Desde main, antes de llamar a la función:\n");
for(cont = 0; cont <= 2; ++cont) {
a[cont] = cont + 1;
printf("a[%d] = %d\n", cont, a[cont]);
}
modificar(a);
printf("Desde main, después de llamar a la función: \n");
for(cont = 0; cont <= 2; ++cont)
printf("a[%d] = %d\n", cont, a[cont]);
}
/* definición de función */
void modificar(int a[]) {
int cont;
printf("Desde la función, después de modificar los valores:\n");
for(cont = 0; cont <= 2; ++cont) {
a[cont] = -9;
printf("a[%d] = %d\n", cont, a[cont]);
}
}
Los elementos del arreglo tienen asignados los valores a[0] = 1, a[1] = 2, a[2] = 3 en
el primer bucle de main. Estos valores se muestran después de ser asignados. Entonces el
arreglo se pasa a la función modificar, donde se les asigna el valor -9 a cada elemento del
arreglo. Estos valores se muestran dentro de la función. Finalmente, los valores del arreglo se
muestran de nuevo desde main, una vez que el control ha sido transferido desde modificar
hasta main.
El hecho de que un arreglo pueda ser modificado globalmente dentro de una función
proporciona un mecanismo adecuado para mover múltiples datos de o desde una función a la
porción llamante del programa. Simplemente se pasa el arreglo a la función y se alteran sus
elementos dentro de ella. O, si el arreglo debe ser preservado, se copia el arreglo (elemento
por elemento) dentro de la porción llamante del programa, se pasa la copia a la función y se
realizan las alteraciones. El programador debe tener cierto cuidado al alterar un arreglo dentro
de una función pues es muy fácil alterarlo de modo no intencionado fuera de la función.
Consideremos el problema de ordenar una lista de n enteros en una secuencia de valores
algebraicos crecientes. Escribamos un programa que realice la reordenación de modo que no
se use un almacenamiento innecesario. Por tanto, el programa contendrá solo un arreglo
unidimensional de enteros llamado x, en el que se reordenará un elemento cada vez. La
reordenación comenzará recorriendo todo el arreglo para encontrar el menor de los números
(algebraicamente). Este número será intercambiado con el primer elemento del arreglo,
colocando así el menor elemento al principio de la lista. El resto de los n-1 elementos del
arreglo se recorrerán para encontrar el menor, que se intercambiara con el segundo y así
sucesivamente, hasta que el arreglo completo sea reordenado. La reordenación completa
7
COMPUTACIÓN II (ELO) - INFORMÁTICA II (BIO)
requerirá de n-1 pasos a lo largo del arreglo, pero cada vez la parte del arreglo a considerar es
menor.
Para encontrar el menor de los números en cada paso, comparamos secuencialmente cada
número del arreglo, x[i], con el número de comienzo, x[elem], donde elem es una variable
entera que se usa para identificar un elemento particular del arreglo. Si x[i] es menor que
x[elem], entonces se intercambian los dos números; en otro caso se dejan los dos números
en sus posiciones originales.
El programa en C que ejecuta esta tarea se escribe de la siguiente manera:
#include <stdio.h>
#define TAM 100
void reordenar(int cantidad, int datos[]); /* Declaración de la función */
int main( ) {
int i, n, x[TAM];
/* Leer un valor para n */
printf("Cuántos números serán introducidos? ");
scanf("%d",&n);
fflush(stdin); /* Esta función limpia el buffer de teclado */
printf("\n");
/* Leer la lista de números */
for(i = 0; i < n; ++i) {
printf("i = %d x = ", i + 1);
scanf("%d", &x[i]);
}
/* reordenar todos los elementos del arreglo */
reordenar(n,x);
/* mostrar el arreglo reordenado */
printf("Lista de números reordenada:\n");
for(i = 0; i < n; ++i)
printf("i = %d x = %d\n", i + 1, x[i]);
return 0;
}
void reordenar(int n, int x[]) {
int i, elem, temp;
for (elem = 0; elem < n-1; ++elem) {
/* encontrar el menor del resto de los elementos */
for (i = elem + 1; i < n; ++i) {
if ( x[i] < x[elem]) {
/* intercambiar los dos elementos */
temp = x[elem];
x[elem] = x[i];
x[i] = temp;
}
}
}
}
Sin embargo, se requiere algún cuidado cuando se pasan arreglos multidimensionales a una
función. En particular, las declaraciones de argumentos formales dentro de la definición de función
debe incluir especificaciones explícitas de tamaño en todos los índices excepto en el primero.
8
COMPUTACIÓN II (ELO) - INFORMÁTICA II (BIO)
Para lograr esto supondremos que las matrices tienen igual número de filas y columnas y que
no exceden de 20 filas y 20 columnas.
El programa se modulariza escribiendo funciones separadas para leer los arreglos, calcular la
suma de los elementos del arreglo y escribir el arreglo. Llamaremos a estas funciones
leematriz, sumamatriz, escribirmatriz.
#include <stdio.h>
#define MAXF 20
#define MAXC 20
/* prototipos de función */
void leematriz (int a[][MAXC], int nfilas, int ncols);
void sumamatriz(int a[][MAXC], int b[][MAXC], int c[][MAXC],int nfilas, int ncols);
void escribirmatriz (int c[][MAXCOL], int nfilas, int ncols);
int main( ) {
int nfilas, ncols; /* número de filas y número de columnas */
/* definiciones de arreglos */
int a[MAXF][MAXC], b[MAXF][MAXC], c[MAXF][MAXC];
printf("Cuantas filas?");
scanf("%d", &nfilas);
printf("Cuántas columnas?");
scanf("%d", &ncols);
printf("Primera Matriz:\n");
leematriz(a, nfilas, ncols);
printf("Segunda Matriz:\n");
leematriz(b, nfilas, ncols);
sumamatriz(a, b, c, nfilas, ncols);
printf("Suma de los Elementos:\n");
escribirmatriz(c, nfilas, ncols);
return 0;
}
/* función para leer una matriz */
void leermatriz(int a[][MAXC], int m, int n) {
int fila, col; /* contador de número de filas y columnas */
for(fila = 0; fila < m; ++fila) {
printf("Introd. datos para la fila nro. %2d\n", fila +1);
for(col = 0; col < n; ++col) {
scanf("%d", &a[fila][col]);
}
}
}
9
COMPUTACIÓN II (ELO) - INFORMÁTICA II (BIO)
3.5 Recursividad
Se llama recursividad a un proceso mediante el cual una función se invoca repetidamente a sí
misma hasta que se satisface alguna condición de finalización predefinida. Este proceso se
utiliza para “simular” una repetición sin emplear ninguna de las estructuras de control de flujo
iterativas conocidas.
Se deben satisfacer dos condiciones para poder resolver un problema recursivamente:
Cond. 1). El algoritmo solución se debe poder escribir en forma recursiva.
Cond. 2). El enunciado del problema debe contener una condición de fin asegurada.
Para entender como trabaja el proceso de recursividad se debe conocer previamente la
operación del mecanismo de invocación de funciones en el lenguaje C, aunque es
prácticamente lo mismo para cualquier lenguaje de programación que emplee subprogramas.
Cuando un programa A invoca a un subprograma B, el programa A detiene su ejecución y
comienza la ejecución del subprograma B. Cuando este último finaliza, el programa A retoma la
ejecución a partir del punto donde la detuvo, tal como lo muestra el siguiente esquema:
Programa A
A1
A2… LLAMADOR
A espera que B
termine.
Subprograma B
…A2
10
COMPUTACIÓN II (ELO) - INFORMÁTICA II (BIO)
Resulta claro que, dado que el programa A y el subprograma B residen en memoria, todo este
proceso debe contar con algún medio de almacenar la dirección de memoria donde el
programa A dejó pendiente su ejecución para así poder retomarla cuando finalice el
subprograma B.
Este almacenamiento está dado por una zona especial en la memoria de la computadora que
se denomina stack (o también pila o LIFO) debido a la forma en la que almacena y devuelve
los valores guardados en ella. Quizás el nombre LIFO, por Last In – First Out (“El ultimo en
entrar es el primero en salir”) es el más representativo de la forma de operación de esta zona
de memoria. Para entenderlo mas claramente se debe pensar en la forma de trabajo de una
pila de platos, en la que el ultimo plato ingresado a la pila (el que se encuentra en lo mas alto
de la pila) es el primero en ser retirado cuando es necesario usarlo.
El lenguaje C almacena en esta “pila” o stack tanto las direcciones de retorno (direcciones de
memoria desde donde deben continuar las ejecuciones de los programas o subprogramas
luego de la invocación de otro subprograma) como las variables locales del subprograma,
siendo estas las verdaderas variables locales y/o los parámetros formales del subprograma o
función.
Mediante un ejemplo se mostrará como se genera la repetición recursiva empleando el stack
como medio de almacenamiento temporario de la información. El problema a resolver es:
“Desarrollar un programa que lea una línea de texto caracter por caracter y luego escriba los
caracteres en orden inverso al ingresado”.
#include <stdio.h>
#define EOLN '\r'
void inverso(void);
void main(void) {
printf("Introduzca una línea de texto debajo\n");
inverso(); /* Primera invocación */
}
void inverso(void) {
char c;
if((c=getchar())!= EOLN)
inverso(); /* Invocacion recursiva */
putchar( c );
}
Los valores a ingresar a este programa será la siguiente secuencia de letras y símbolos:
A B C <Enter>
1- Ante la primer invocación de la función inverso() en el
Variable
main, se almacena la posición de retorno en el stack, se crea la c=A
variable local c y esta recibe el valor ‘A’ al ejecutarse la función
getchar. Dirección de retorno
1º invocación
Dado que el valor leído no es EOLN, la función inverso() se
STACK
auto-invoca.
11
COMPUTACIÓN II (ELO) - INFORMÁTICA II (BIO)
Variable
c=B
2- Ante la segunda invocación de la función inverso(), se
almacena la posición de retorno en el stack, se crea una nueva Dirección de retorno
2º invocación
variable local c y esta recibe el valor ‘B’ al ejecutarse la función
getchar. Variable
c=A
Dado que el valor leído no es EOLN, la función inverso() se
auto-invoca nuevamente. Dirección de retorno
1º invocación
STACK
Variable
c=C
Dirección de retorno
3- Ante la tercera invocación de la función inverso(), se 3º invocación
almacena la posición de retorno en el stack, se crea una nueva Variable
variable local c y esta recibe el valor ‘C’ al ejecutarse la función c=B
getchar.
Dirección de retorno
Dado que el valor leído no es EOLN, la función inverso() se 2º invocación
Dirección de retorno
1º invocación
STACK
Variable
c = \n
Dirección de retorno
4º invocación
4- Ante la cuarta invocación de la función inverso(), se almacena
Variable
la posición de retorno en el stack, se crea una nueva variable local c c=C
que recibe el valor ‘\n’ al ejecutarse la función getchar.
Dirección de retorno
Dado que ahora el valor leído es EOLN, la función inverso() NO 3º invocación
se auto-invoca y procede a ejecutar la función putchar( c )
Variable
mostrando por pantalla una nueva línea en blanco. c=B
Dirección de retorno
1º invocación
STACK
12
COMPUTACIÓN II (ELO) - INFORMÁTICA II (BIO)
Variable
c=C
Dirección de retorno
5- Luego del retorno de la 4º invocación se produce la ejecución de 3º invocación
Dirección de retorno
1º invocación
STACK
Variable
c=B
6- Luego del retorno de la 3º invocación se produce la ejecución de
la función putchar( c ) de la segunda invocación mostrando por Dirección de retorno
pantalla la letra ‘B’. 2º invocación
STACK
7- Luego del retorno de la 2º invocación se produce la ejecución de
la función putchar( c ) de la primer invocación mostrando por Variable
pantalla la letra ‘A’. c=A
vacío el stack.
Analizando el crecimiento del stack a lo largo del algoritmo, es fácil determinar que el consumo
de memoria requerido para esta solución recursiva excede largamente al de una solución
equivalente que empleara un arreglo de char y una simple iteración para lograr la impresión de
valores en secuencia inversa.
De hecho, es importante resaltar que una alternativa recursiva puede no ser la solución
óptima a un problema, aún cuando éste sea recursivo por naturaleza. Una solución con
iteraciones convencionales puede ser más eficiente en cuanto a utilización de recursos
(típicamente la memoria) y rapidez de ejecución, y por ello, el uso de la recursividad puede
involucrar un compromiso entre simplicidad y prestaciones, por lo que cada problema debe
juzgarse individualmente teniendo en cuenta sus características específicas.
Otro ejemplo de función recursiva es el cálculo del factorial de un número, definido
recursivamente en forma matemática (condición 1) como:
13
COMPUTACIÓN II (ELO) - INFORMÁTICA II (BIO)
14