Escribir programas modulares en C usando funciones
Los programas que hemos escrito en artículos anteriores tienen un pequeño problema, y es que no pueden crecer en su funcionalidad y tamaño de forma sostenible. Todas las sentencias de los programas estaban en la función principal, main
, en el caso de C, o símplemente sueltas, en el cuerpo del programa, en el caso de Python.
Si quieres crear programas de una complejidad y funcionalidad mayores que las de los ejemplos anteriores, hay que hacer algo. ¿Te imaginas un sistema operativo completo que empieza y termina dentro de main()
?
En este artículo vamos a dar un primer paso en la creación de programas modulares, usando funciones.
Funciones en C
Las funciones se definen, en C, de la siguiente forma:
t_devuelto n_funcion (t_arg1 arg1, t_arg2 arg2, ...)
{
/* Sentencias en C*/
return valor_t_devuelto;
}
Elemento a elemento:
t_devuelto
es el tipo de datos devuelto. Más adelante entraremos en detalles de cuáles son los tipos de datos en C, pero de momento hemos visto algunos, comoint
ychar
.n_funcion
es el nombre de la función. Hay reglas para las funciones en C: no pueden empezar por números, y no aceptan caracteres distintos a letras, números y_
. Esas mismas reglas aplican a los nombres de las variables.t_arg1
es el tipo de datos para el argumentoarg1
, y así sucesivamente.return valor_t_devuelto;
es una sentencia que devuelve el resultado de la función. El resultado puede ser un valor literal, una variable o el resultado de una expresión, pero el valor o el resultado de la expresión tiene que ser de tipot_devuelto
.
Ejemplo: potencias con exponentes enteros
Veamos un ejemplo real, que consiste en programar una función que calcule el resultado de elevar un número, con parte decimal, a un exponente entero.
Éste es el código de mi solución particular:
float power(float num, int pow) {
float result = 1;
if (pow < 0) {
num = 1 / num;
pow = -1 * pow;
}
int i = 1;
while (i <= pow) {
result = result * num;
i++;
}
return result;
}
La función power
toma como argumentos un número con parte decimal, num
, de tipo float
, y lo eleva a una potencia entera, pow
. El resultado es de tipo float
.
“Float” es una abreviatura de “floating point”, que significa coma flotante en español. “Coma flotante” es una forma técnica de almacenar y operar números con parte decimal con una precisión variable de forma digital, en la memoria de un ordenador. Es una forma muy técnica y precisa de referirse a una representación específica de números con parte decimal. Todo esto se hace y se dice así porque es imposible representar números naturales, con precisión infinita, en un ordenador.
En el cuerpo de la función vemos que se declara una variable de tipo float
para devolver el resultado, con un valor inicial igual a 1:
float power(float num, int pow) {
float result = 1;
//...
return result;
}
El cuerpo de la función es relativamente fácil de entender si tienes fresca la memoria de cómo funcionan las potencias:
- Todo número elevado a 0 da como resultado 1.
- Todo número elevado a 1 da como resultado sí mismo.
- Un número num, elevado a un número positivo distinto a 0, pow, numˆpow, se calcula como multiplicar num por sí mismo pow veces.
- Un número num, elevando a un número negativo distinto a 0, -pow, numˆ-pow, se calcula multiplicando el inverso de num por sí mismo, pow veces. También se puede calcular como el inverso de num, elevado a pow: (1/num)ˆpow.
Así pues, el código siguiente debería ser fácil de interpretar con lo que ya sabemos:
if (pow < 0) {
num = 1 / num;
pow = -1 * pow;
}
Con este código hemos preparado el caso especial de que el exponente sea negativo. Para el caso especial de que el exponente sea 0, todavía no hemos tomado ninguna decisión.
Con estas condiciones ya podemos ejecutar la potencia, usando una variable adicional para movernos a través de nuevo bucle: el bucle while.
int i = 1;
while (i <= pow) {
result = result * num;
i++;
}
La variable int i = 1
se va a utilizar para contar el número de veces que multiplicamos num
sobre sí mismo.
El bucle funciona de la siguiente forma:
mientras i sea menor o igual que pow,
result = result * num;
incrementa i en una unidad;
.
- Como vemos,
i++
es una forma compacta de incrementari
.++i
es también una forma compacta de incrementari
, pero sin embargo, el comportamiento dei++
e++i
no es intercambiable, como veremos en otro artículo. Y tampoco son equivalentes ai = i + 1
. Si quisiéramos abreviari = i + 1
estrictamente, deberíamos escribiri += 1
, sintaxis que en C es perfectamente legítima. - Como
result
está inicializado a 1, la primera iteración del bucle va a producirresult = 1 * num
, que cumple con el funcionamiento de las potencias cuando el exponente es 1. - Las iteraciones siguientes irán multiplicando
result
pornum
, hasta quei
sea estrictamente mayor quepow
, momento en el que no ejecutaremos nada más. - Como hemos empezado el bucle habiendo inicializado
i = 1
, sipow
es 0, el bucle no se ejecuta nunca, y se devuelve el valor inicial deresult
, que es 1.
Con esto, ya podremos ejecutar nuestra función power
para calcular potencias con exponentes enteros. La función se invoca como ya sabes gracias a artículos anteriores: la expresión pow (2, 2)
en tu código devolvería un valor igual a 4.
Conclusiones y siguientes pasos en la modularidad de tus programas
Al nivel en el que estamos ahora mismo en Micromáquina, las funciones nos ayudan a organizar programas de tal forma que aquellas funcionalidades que queremos poder reutilizar, son accesibles de forma fácil.
Sin embargo, las funciones en C tienen bastantes variantes interesantes que no hemos visto aquí. Éstas son algunas de ellas:
- Una función que no devuelve resultado, sino que realiza un proceso “silenciosamente”, se declara utilizando
void
(vacío) como tipo de datos devuelto, y no tiene sentenciareturn
. Estas funciones se suelen denominar procedimientos, y tienes un ejemplo en el listado de código del apartado de ejercicios que va a continuación: localiza la líneavoid print_result(...)
. - Echa un vistazo al siguiente programa y su ejecución:
/**
* paso_por_valor.c
*/
#include <stdio.h>
void test(int par) {
par = 5;
printf("En test, par=%d\n", par);
}
int main () {
int par = 0;
printf("Antes de test, par=%d\n", par );
test(par);
printf("Después de test, par=%d\n", par );
return 0;
}
$ gcc paso_por_valor.c -o paso_por_valor
$ chmod +x paso_por_valor
$ ./paso_por_valor
Antes de test, par=0
En test, par=5
Después de test, par=0
- Este comportamiento que acabamos de ver en el ejemplo se llama “paso de parámetros por valor”:
- Cuando se llama a una función en C usando una variable como parámetro, lo que ocurre es que se pasar el valor de la variable a la función, no la variable en sí
- El cuerpo de la función tiene una variable local que recibe el valor de la variable externa, pero son áreas de memoria diferentes, independientemente de que el programador les haya dado a ambas el mismo nombre.
- Por lo tanto, cualquier modificación que se haga dentro de la función, se pierde.
- Esto suele llamar a confusión a veces, porque intuitivamente podríamos inclinarnos a creer que, si la variable comparte nombre con un parámetro, la función debería modificar la variable externa.
- Como ya hemos dicho, eso no funciona así.
- En C existe un mecanismo llamado “paso de parámetros por referencia”, que proporciona al código de dentro de una función un mecanismo para modificar variables externas a dicha función. Como este mecanismo requiere entender direcciones y punteros, lo vamos a dejar para el futuro.
El siguiente paso en el camino de dar modularidad a tus programas será construir nuestra propia librería, llamada en algunos ámbitos módulo, para poder usarla con #include
.
Programa de pruebas y ejercicios
Para terminar el artículo, aquí tienes el código fuente completo, con una función adicional y el programa principal, para poner a prueba esta función y poder experimentar con ella:
/**
* prog_power.c
*/
#include <stdio.h>
float power(float num, int pow) {
float result = 1;
if (pow < 0) {
num = 1 / num;
pow = -1 * pow;
}
int i = 1;
while (i <= pow) {
result = result * num;
i++;
}
return result;
}
void print_result(float num, int pow, float result) {
printf("%.2f elevado a %d = %.2f\n", num, pow, result);
}
int main() {
print_result(3, 0, power(3,0));
print_result(2, 5, power(2,5));
print_result(3, -2, power (3, -2));
print_result(1.2, 2, power (1.2, 2));
return 0;
}
Probablemente no entiendas la sentencia que imprime los mensajes,
printf("%.2f elevado a %d = %.2f\n", num, pow, result);
, pero no te preocupes por el momento. Este tipo de funciones disponibles en stdio.h
serán objeto de otro artículo, más adelante.
De momento, prueba el programa con los siguientes comandos:
$ gcc prog_power.c -o prog_power
$ chmod +x prog_power
$ ./prog_power
El resultado, para el listado superior, es el siguiente:
3.00 elevado a 0 = 1.00
2.00 elevado a 5 = 32.00
3.00 elevado a -2 = 0.11
1.20 elevado a 2 = 1.44
Sobre este programa:
- Cambia el programa principal para ejecutar más casos de pruebas, como por ejemplo, usando los exponentes 1, -1 y -0.
- Cambia el comportamiento del bucle para que funcione empezando en 0, con
int i = 0
. Obviamente, va a funcionar igual, técnicamente hablando, pero, ¿cuáles son las desventajas del código resultante? - El bucle do... while y el bucle while tienen comportamientos diferentes. ¿Cuáles son esas diferencias? Reescribe la función
power
usando do... while. - ¿Qué pasa si mueves la función
power
para que esté debajo de la funciónmain
? ¿Puedes ofrecer una explicación a ese comportamiento?
Envía tu respuestas, si quieres, escribiendo en Mastodon a @gabriel@fedi.gvisoc.com y @gabriel@micromaquina.com, con un enlace a este artículo y tus respuestas. Te contestaré en privado.
👉 Sigue a micromáquina en @gabriel@micromaquina.com 👉 Sigue a Gabriel Viso en @gabriel@fedi.gvisoc.com