Skip to content

Instantly share code, notes, and snippets.

@rlunaro
Last active April 18, 2024 18:19
Show Gist options
  • Save rlunaro/6cf1d79924a6eef57e4d06d44449e7e9 to your computer and use it in GitHub Desktop.
Save rlunaro/6cf1d79924a6eef57e4d06d44449e7e9 to your computer and use it in GitHub Desktop.
Básico de programación modular en C

Básico de programación modular en C

Intro

Al repartir el código en varios ficheros fuente, conseguimos estructurar nuestra aplicación en "temas": socios por un lado, cursos por otro, por ejemplo. Eso permite que la colaboración entre varias personas sea más fácil. Pero no sólo eso, facilita la mantenibilidad de la aplicación.

Y además así es como trabajan los profesionales, por todos estos motivos.

Comencemos

Éste es el clásico programa sin modulos:

image

Presta tención a ése #include que se ve ahí: eso es una directiva del preprocesador de C: lo que hace es leer el fichero stdio.h, que existe en tu ordenador y copia todo el código fuente de ese fichero dentro de main.c. Una vez que ha hecho eso, entonces toma el control el compilador y compila el codigo resultante.

Es decir, para compilar ése programa está haciendo dos pasadas: en la primera pasada entra el preprocesador y resuelve los #include's, los #define's y todas las directivas del precompilador (lo que empieza por #, vaya). Luego, se ejecuta el compilador propiamente dicho. Y luego se hace una tercera pasada, que es el enlazador (linker). Pero eso lo vemos luego.

Esto es lo que se ve cuando ejecutas el compilador para que sólo haga la pasada del preprocesador:

image

Vale, pues eso pasa con <stdio.h>... Pero..... *¿podemos hacerlo con nuestros propios ficheros???? ¿podríamos crear nosotros nuestros propios ficheros .h???

La respuesta es que sí.

Vamos a crear un fichero .h (se llaman ficheros de cabecera) muy sencillito (te animo a que en este punto lo hagas tú en un proyecto de prueba):

image

Y ahora lo incorporamos a nuestro main.c:

image

¿¿¿Funcionará??? Y...

image

Pues claro que funciona. Pero este enfoque de poner el código en el fichero de cabecera tiene un problema.... ¿adivinas cual???

El problema empieza a aparecer cuando metes más ficheros de cabecera que se incluyen entre ellos.

Para ver el problema, vamos a meter un fichero de cabecera nuevo que se llamara simple_double.h. En el definiremos la funcion simple_double que hará el doble de la suma:

image

¿¿¿Funcionará???

image

No funciona. ¿qué está pasando??? ¿qué es eso de "redefinition"?

Antes de entrar en pánico, veamos qué pasa al desenredar los includes:

image

El agudo lector observará que hay dos funciones add. ¿podría decir de dónde cojones han salido???

La primera viene directamente de nuestro main.c -> ahí tenemos un #include "simple_add.h".

La segunda viene a través de nuestro simple_double.h, que a su vez contiene simple_add.h: al ir desdenredando e incluyendo un fichero tras otro ha metido dos veces la función add.

Esto se puede arreglar???

Pues si, y HAY DOS FORMAS DE ARREGLARLO. Veamos la primera, que no es la más ortodoxa, pero verás que funciona.

Primera aproximación a la modularidad en C

Porque vamos a ver, ¿Para qué sirven los #define's??? Los podemos usar para quitar código, no???

image

Fíjate en esta composición tan curiosa: si no está definido aún SIMPLE_ADD, vas y lo defines: y entonces incluyes toooodo el código hasta el #endif. Peeeero si ya lo has definido, entonces no hagas nada.

Esta composición de #ifndef, #define actúa de salvaguarda, impidiendo que el código de la función add se suministre al compilador más de una vez: la segunda vez que el preprocesador de C procesa este fichero, el #define SIMPLE_ADD ya se encuentra definido, por lo que se saltará todo el #ifndef: es una forma de garantizar que un fragmento de código sólo se incluirá una vez en nuestro programa aunque pongamos muchas veces el mismo #include.

Tras este cambio, nuestro programa sólo carga la función add una vez, y compila adecuadamente:

image

La regla es que esa salvaguarda se pone siempre en un fichero de cabecera para garantizar que sólo se cargará una vez en nuestro proyecto.

Por cierto, nota al margen: verás que en lugar de los #ifndef BLABLA, #define BLABLA verás en muchos sitios #pragma once: esto es una directiva del compilador que es más reciente y mucho más eficaz: es equivalente; garantiza que ése fichero de cabecera se cargará sólo una vez por ejecutable. Aunque muchos programadores de la vieja escuela y muchos IDE's mantienen los #ifndef por compatibilidad con versiones antiguas del compilador.

Segunda aproximación a la modularidad en C, más elaborada y eficaz

Pero aún podemos darle una vuelta más, Veamos la segunda forma de hacerlo.

Resulta que tenemos declaraciones de funciones y definiciones:

image

Podemos separar la definición en un fichero *.c y la declaración y los tipos en un fichero *.h:

image

Y ésto sigue compilando exactamente igual: el fichero simple_add.c se compila exactamente igual y se incorpora a tu proyecto como si tal cosa....

Mira, éste es el comando que compila sólo el fichero simple_add.c:

image

Ése fichero no se compila en un ejecutable... se compila en una cosa que se llama fichero objeto. Ese "fichero objeto" es un fichero intermedio entre nuestro código fuente y un fichero ejecutable real.... contiene las funciones compiladas y listas para "copiarse y pegarse" en el ejecutable final.... Ya verás luego que ésto es interesante porque 1) ahorra tiempo de compilación -aunque en proyectos pequeños ni se nota- y 2) mejora la colaboración y el mantenimiento de la aplicación, ya que ahora no tocamos todos el mismo fichero.

Y todos los objetos se unen en un ejecutable en esa famosa "tercera pasada del compilador" que te dije al principio:

image

Ves??? ahí junta main.o (el fichero objeto correspondiente a main.c) y simple_add.o (el fichero objeto correspondiente a simple_add.c).

De esta forma se consigue trabajar con varios ficheros *.c y hacer el mantenimiento de las aplicaciones más sencillo.

PUEDES VER Y PROBAR ESTE CODIGO AQUÍ:

https://www.online-cpp.com/7tPB2dyCGu

En conclusión

La ventaja de separar el código en ficheros *.c es que se consigue por una parte que cada modulo sea más pequeño, así que el compilador en cada pasada, trabaja menos. Por otra parte hay ficheros *.c que no se tocan entre compilación y compilación la mayor parte de las veces: eso son ficheros *.obj que no se tocan tampoco.

La compilación, si el programa se estructura en módulos, es más rápida. Además nuestro código queda más estructurado e incluso es reutilizable: podemos usar un módulo en otros proyectos sin necesidad de tener que reescribirlo o incorporarlo al proyecto.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment