FUNCIONES - INTRODUCCIÓN
Conceptos relacionados con los subprogramas.
Conceptos relacionados con los subprogramas.
Un programa es una colección de instrucciones que puede ejecutar el procesador. Estas instrucciones están almacenadas en la memoria de la computadora, y ocupan distintos números de bytes, dependiendo de los sus operandos. Por tanto, las instrucciones de programa ocupan bloques de memoria de longitudes irregulares. Estos bloques se almacenan de forma contigua, para no desperdiciar inútilmente espacio, pero esto no quiere decir que el procesador siempre ejecute las instrucciones sucesivamente; en realidad, el procesador va "saltando" de una instrucción a otra, y existen, precisamente, instrucciones de "salto", que hacen que el procesador "salte" a una instrucción que no es la siguiente a la última que ha ejecutado, sino una instrucción que puede estar muy lejos (en memoria) de la anterior.
Este concepto de salto puede hacernos pensar en un método interesante para optimizar programas. Si se estudian detalladamente las instrucciones de un programa, es fácil encontrar que una cierta sucesión de instrucciones se repite en numerosas ocasiones a lo largo del programa. Estos bloques de código aparecen una y otra vez en el programa, siempre iguales. Esto se debe a que hay ciertas tareas de programa, como escribir un carácter en la pantalla, que se ejecutan cientos o miles de veces. ¿Sería posible abreviar el programa, escribiendo una sola vez los bloques que se repiten? Ciertamente, si: podemos descomponer el código de programa en dos grandes colecciones de secuencias de instrucciones: las que se repiten y las que no. Para ejecutar un segmento de código que se repite, basta con "saltar" a su comienzo y, finalizado el segmento, "saltar" a la instrucción siguiente. En la imagen, el lado izquierdo muestra (en rojo) el código que se repite en varias ocasiones. A la derecha se muestra el resultado de almacenar en RAM
una sola vez
el código que antes se repetía. Para obtener un programa funcionalmente equivalente al primero, pero más corto, basta efectuar los saltos que se muestran. El primero de ellos, marcado con (1), representa la ejecución del bloque repetido por primera vez. Cuando se han ejecutado todas las instrucciones de ese bloque, se efectúa un "salto" a la primera instrucción del resto del programa. El último salto, marcado con (4), representa la ejecución del bloque repetido previa al último bloque del programa.
En primera aproximación, el concepto de función o subprograma es, precisamente, el de un segmento de código que se utiliza muchas veces a lo largo de un programa, y que se separa del resto del programa en su propio bloque, para ejecutarlo cuando convenga sin necesidad de repetirlo.Esta primera aproximación no es una descripción completa del concepto de función: el compilador no solo separa físicamente el bloque de código repetido; además, dota a este bloque de código de dos características adicionales que son las que realmente facilitan la tarea del programador:
-
Variables locales.
-
Parámetros formales.
Estos conceptos se detallan más adelante. Veamos un programa que hace uso de una función, para apreciar el "feeling" de las funciones, y después mostraremos de inmediato la sintaxis de las funciones en el lenguaje C.
Ejemplo
.- Construir el programa
Hola, Mundo
empleando una función.
Una posible versión de este programa sería la siguiente:
#include<stdio.h>
/* prototipo de la función decir_hola() */
void decir_hola(int numero_de_veces);
int main(int argc, char * argv) {
decir_hola(7);
return 0;
}
/* Declaración de la función decir_hola() */
void decir_hola(int numero_de_veces)
{
int i;
printf("******************************\n");
printf("******************************\n");
printf("******************************\n");
for(i=0;i<numero_de_veces;i++)
printf("Hola por vez número %d\n", i);
printf("******************************\n");
printf("******************************\n");
printf("******************************\n");
return;
}
/*
******************************
******************************
******************************
Hola por vez número 0
Hola por vez número 1
Hola por vez número 2
Hola por vez número 3
Hola por vez número 4
Hola por vez número 5
Hola por vez número 6
******************************
******************************
******************************
Y ahora otra vez
******************************
******************************
******************************
Hola por vez número 0
Hola por vez número 1
Hola por vez número 2
Hola por vez número 3
Hola por vez número 4
Hola por vez número 5
Hola por vez número 6
Hola por vez número 7
Hola por vez número 8
Hola por vez número 9
Hola por vez número 10
Hola por vez número 11
Hola por vez número 12
Hola por vez número 13
******************************
******************************
******************************
*/
Comentarios
.- El segmento de código que se repite en este programa es el contenido en el bloque de la función
decir_hola()
. Este bloque escribe varias líneas de asteriscos, y después va escribiendo la frase "
Hola por vez número N
", haciendo variar el valor de
N
. En la función
main()
aparecen dos llamadas (dos saltos, decíamos antes) a
decir_hola()
, una con el parámetro real 7 y otra con el parámetro real 14, lo cual da lugar a dos ejecuciones del bloque de
decir_hola()
: una con
numero_de_veces
igual a 7, y otra con
numero_de_veces
igual a 14. Estas llamadas a
decir_hola()
son sólo dos, pero podríamos poner cualquier número de llamadas dentro del bloque de
main()
. El ahorro de espacio es evidente, aunque, como se verá, esa no es la ventaja más importante que aportan las funciones.
Sintaxis de una función en C
En las versiones actuales de C, para poder utilizar una función o subprograma es preciso declararlo previamente. Esta declaración (y definición, de hecho) consta de dos partes:
-
Declaración del prototipo.
-
Definición de la función.
La declaración del prototipo tiene la forma siguiente:
Tipo_proporcionado nombre_de_función(lista_de_parámetros);
Y un posible ejemplo sería:
int numero_de_vocales(char * cadena);
en donde:
-
Tipo_proporcionado
(
int
en el ejemplo) es cualquier tipo válido en C, y denota el tipo de variable en que debe almacenarse el resultado que se obtiene al ejecutar la función. Esto es, la ejecución de una función puede producir un resultado, que podríamos recoger mediante una expresión como la siguiente:
resultado = numero_de_vocales(lista);
en donde el tipo de
resultado
y el tipo proporcionado por la función
suma
deben ser iguales o al menos compatibles. Si la función no va a proporcionar resultado alguno, entonces el
Tipo_proporcionado
debe ser
void
. El tipo
void
significa nada o nulo y tiene otras aplicaciones, especialmente en el mundo de los punteros.
-
nombre_subprograma
(
numero_de_vocales
en el ejemplo) es el nombre que deseamos dar a al función. Por convención (no porque lo imponga el lenguaje), los nombres de funciones empiezan por una letra minúscula. Para llamar a una función, esto es, para que en el código máquina haya un salto de ida al código de la función y de vuelta a la función desde la cual se hace la llamada, se escribe el nombre de la función y después unos paréntesis. Si la función admite parámetros, se ponen dentro de esos paréntesis. En el ejemplo anterior,
resultado = suma(lista);
,
lista
es un
parámetro
. Los parámetros son una vía de comunicación entre la función que hace la llamada y la función la recibe, esto es, la que se ejecuta. En un ejemplo anterior, la función que hacía la llamada era
main()
y la función que recibía la llamada era
decir_hola()
.
-
lista_de_parámetros
es es una colección de declaraciones de variables, de la forma
Tipo variable
Si la función posee varios parámetros, esto es, si en la llamada a la función hay que aportar varios valores, será preciso separar las declaraciones mediante
comas
. Por ejemplo, la función cuyo prototipo se muestra a continuación admite varios parámetros:
float calcular(int x, int y, float z, struct Registro * p);
Como se observará, cada parámetro (
x
,
y
,
z
y
p
)
debe
ir precedido por su tipo. Las distintas declaraciones van separadas mediante
comas
. Los parámetros que se declaran en el prototipo y en el encabezado de la función reciben el nombre de parámetros
formales
. El número, tipo y nombre de los parámetros especificados en el prototipo y en el encabezado de la función tiene que ser exactamente el mismo. Si no hay ni un sólo parámetro, debe indicarse esta circunstancia empleando la palabra
void
como lista de parámetros, como se indica en la declaración siguiente:
int calcular(void)
que especifica que la función
calcular
no recibe parámetro alguno. Si se especifica una lista de parámetros
void
en el prototipo, tiene que especificarse también una lista de parámetros
void
en el encabezado de la función.
Por otra parte, la declaración del cuerpo o bloque de la función tiene la forma siguiente:
Tipo_proporcionado nombre_subprograma(lista_de_parametros) /* Encabezado */
{
/* Cuerpo */
return expresión;
}
En donde
-
El encabezado es una copia exacta del protototipo, con excepción del punto y coma. Si se desea, se admite omitir en el prototipo los nombres de los parámetros formales; sin embargo, la cabecera de la función exige indicar los nombres de los parámetros formales, como cabía esperar... ¿con qué nombre se podría acceder a estas variables locales? Por ejemplo, la siguiente pareja de declaraciones (prototipo/cuerpo) es válida:
void funcion(int, float, long);
...
void funcion(int x, float y, long z)
{
/* Sentencias */
return;
};
Obsérvese que coinciden exactamente los tipos de los parámetros formales especificados en el prototipo y en el encabezado de la función; lo contrario sería un error. Recuérdese también que en el caso de que no haya parámetros formales es preciso emplear la palabra
void
como lista de parámetros tanto en el prototipo como en el encabezado de la función. Por ejemplo, las declaraciones siguientes corresponden a una función que carece de parámetros y no proporciona resultado alguno:
void funcion(void);
...
void funcion(void)
{
/* Sentencias */
return;
};El
Tipo_proporcionado
que se indica en el prototipo y el que se indica en el encabezado tiene que coincidir exactamente.
-
El cuerpo puede reducirse a un par de llaves, en cuyo caso la función no haría absolutamente nada (esto exige que el tipo proporcionado sea
void
; si el
Tipo_proporcionado
no es
void
y la función carece de sentencia
return
(o ésta no se ejecuta), el resultado proporcionado por la función no está definido). La sentencia
return
puede adoptar dos formas, dependiendo del
Tipo_proporcionado
por la función. Si éste es
void
, la sentencia
return
va acompañada por ninguna expresión, como puede verse en el fragmento de código anterior. Si la función tiene un
Tipo_proporcionado
que no es
void
, entonces la sentencia return debe ir acompañada por una expresión; el tipo del valor obtenido al evaluar esa expresión debe ser el mismo que el
Tipo_proporcionado
, o al menos compatible con él.
La extensión del cuerpo de una función no debería superar un folio de extensión
.
Notas
-
Los programas comienzan a ejecutarse en la función
main()
; todo programa está formado, como mínimo, por la función
main()
. Un archivo de código en C sin función
main()
no es ejecutable, pero puede ser una biblioteca de funciones; de hecho, es frecuente construir y compilar bibliotecas de funciones, que después se utilizarán sin tener que volver a compilarlas y, sobre todo, sin necesidad de conocer el cuerpo de esas funciones. El uso de una biblioteca de funciones exige conocer los prototipos de esas funciones: esa va a ser la misión de los archivos de encabezado, como
stdio.h
. .
-
main()
posee parámetros que le proporciona el sistema operativo. Concretamente, es frecuente encontrar programas en los que la función main() posee el aspecto siguiente:
int main(int argc, char * argv[]) {...}Los parámetros formales argc y argv sirven para que el sistema operativo pueda pasar a
main()
argumentos recibidos a través de la línea de órdenes. La mecánica de este proceso se describe en la
sección correspondiente
. Este es el método que se emplea en las órdenes de
shell
de los sistemas operativos basados en texto; de hecho, la cola de palabras de los sistemas basados en texto será sustituida por una cola de eventos en sistemas dotados de una interfaz gráfica de usuario (GUI) pero el concepto es bastante paralelo.
-
Todo programa en C es una colección de funciones y llamadas a funciones. La idea de un programa "monolítico", esto es, de un programa formada por una única y monstruosa función
main()
carece de sentido, y viola todos los principios de programación estructurada.
-
Hay un orden para la declaración de variables, prototipos y funciones. Concretamente, el compilador de C espera una organización como la siguiente, teniendo en cuenta que en un programa todo es opcional salvo la función
main()
:
-
Zona de inclusión de encabezados; por ejemplo,
#include<stdio.h>;
#include<stdlib.h>;
#include "mi_encabezado.h";
-
Zona de declaración de constantes, tipos y variables globales (si las hubiera); por ejemplo,
#define MAX 10
#define MIN 5
struct Registro {
char nombre[20];
int n;
float f
};
long int p;
struct Registro * q;
-
Zona de declaración de prototipos; por ejemplo,
int calcular (float, int, char);
void imprimir(void);
char * cadena(const char *, const char *);
-
Zona de declaración de funciones; por ejemplo,
int calcular (float f, int n, char c)
{
return 3.0*f - n;
}
void imprimir(void)
{
printf("Datos finales\n");
}
char * cadena(const char * c1, const char * c2)
{
return strcat(c1, c2);
}Estas zonas definen el contenido de un archivo genérico en C,
programa.c
Todas las constantes y variables que se declaran en un archivo, pero fuera de las funciones construidas en el mismo, reciben el nombre de constantes y variables
globales
. Todas las constantes y variables que se declaran dentro de una función reciben el nombre de constantes y variables
locales
.
-
No se admite la declaración anidada de funciones. Esto es, no está permitido declarar un prototipo y una función dentro de otra función.
Ejercicios iniciales - repetir empleando funciones los ejercicios sobre matrices.
-
Construir un programa que muestre las opciones necesarias para leer una lista de 10 elementos de tipo
int
, calcular valores de interés, mostrar valores de interés y salir de programa. Los valores de interés de la lista son el máximo, el mínimo y el valor medio.
-
Construir un programa que muestre las opciones necesarias para leer dos matrices de dimensiones 2x2 (formadas por elementos de tipo
float
), calcular la suma de las matrices, calcular la resta de las matrices, mostrar la suma, mostrar la resta y salir de programa.
-
Construir un programa capaz de leer un "libro" basado en la siguiente estructura de datos:
#define PAGINAS 3
#define LINEAS 4
#define COLUMNAS 10
char libro[PAGINAS][LINEAS][COLUMNAS];
El programa ofrecerá opciones para leer el libro (de teclado), mostrar una cierta página y salir del programa. Mejora.- Hacer que el programa lea de disco el contenido del libro, preguntando al usuario el nombre del fichero que lo contiene.
-
Construir un programa que muestre en pantalla un tablero de damas. El programa debe ser capaz de admitir "movimientos"; esto es, partirá de un tablero con blancas y negras en sus posiciones, y permitirá al usuario "mover" fichas, mostrando en pantalla el resultado del movimiento.
Mejoras
-
Verificar la corrección de todos los movimientos.
-
Permitir mover a blancas y a negras.
-
Hacer que el tablero se muestre siempre en pantalla, en el último estado (después del último movimiento).
-
(Más complicado)Sugerir movimientos al jugador de blancas. Los movimientos deben ser válidos, y a ser posible comportar alguna ventaja.
-
Construir un programa que muestre en pantalla un tablero de ajedrez, con características similares a las del ejercicio anterior. El tablero debe ser capaz de admitir movimientos para todas las fichas. Comprobar la validez de los movimientos, y advertir al usuario si no son correctos.
-
Construir un programa que admita dos vectores de ℜ³ y muestre el producto escalar, el productor vectorial, el módulo de los vectores, el coseno del ángulo que forman y el seno del ángulo que forman.
-
Construir un programa que admita una matriz 3x3 y un vector de ℜ³ y calcula el producto del vector por la matriz y de la matriz por el vector. Sugerencia: si se llama f a la aplicación definida por la matriz, utilizar un vector de Ker(f) para comprobar que en efecto el resultado de aplicar la matriz al vector es el vector 0.
Soluciones
Ejercicios propuestos
-
Ejercicio 0801r01.-
Se dispone de los coeficientes de una ecuación de segundo grado, de la forma
ax^2 + bx + c = 0
Se se pide construir tres funciones, adecuadas para calcular el valor de las raíces de esta ecuación, en función del valor del discriminante
d = b^2 - 4.0*a*c
. Probar las funciones mediante un programa principal.
-
Ejercicio 0801r02.-
Una función recibe como argumento tres cadenas, que corresponden al nombre, apellidos y teléfono de una persona. Se pide construir la función de tal modo que proporcione como resultado la suma de las longitudes de las tres cadenas.
-
Ejercicio 0801r03.-
Una función recibe como argumento un nombre de usuario y una contraseña. Se pide construir la función de tal modo que proporcione el valor 1 si el nombre y la contraseña son correctos, o bien el valor 0 en caso contrario.
-
Ejercicio 0801r04.-
Una función recibe una cadena que podría ser un número entero. Se pide construir la función de tal modo que proporcione como resultado un 1 si la cadena corresponde a un número entero, o un 0 en caso contrario.
-
Ejercicio 0801r05.-
Una función recibe como argumento un nombre de archivo. Se pide comprobar mediante la función si el nombre posee una extensión correcta.
-
Ejercicio 0801r06.-
Una función recibe como argumento el nombre de un archivo. Se pide construir la función de tal modo que proporcione como resultado la longitud del archivo si éste existe, o bien -1 si el archivo no existe.
-
Ejercicio 0801r07.-
Una función recibe un número entero N como argumento. La función debe proporciona como resultado el puntero de una lista adecuada para almacenar N números enteros, o bien NULL si no se dispone de memoria suficiente.
-
Ejercicio 0801r08.-
Una función recibe dos cadenas como argumento; la primera se llama
palabra
y la segunda se llama
diccionario
. Se pide construir la función de tal modo que proporcione el valor 1 si
palabra
aparece dentro de
diccionario
, o 0 en caso contrario.