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:

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:

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;
.

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:

/**
 * 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

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:

  1. Cambia el programa principal para ejecutar más casos de pruebas, como por ejemplo, usando los exponentes 1, -1 y -0.
  2. 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?
  3. El bucle do... while y el bucle while tienen comportamientos diferentes. ¿Cuáles son esas diferencias? Reescribe la función power usando do... while.
  4. ¿Qué pasa si mueves la función power para que esté debajo de la función main? ¿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.

#C

👉 Sigue a micromáquina en @gabriel@micromaquina.com 👉 Sigue a Gabriel Viso en @gabriel@fedi.gvisoc.com