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.
Éste es el clásico programa sin modulos:
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:
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):
Y ahora lo incorporamos a nuestro main.c
:
¿¿¿Funcionará??? Y...
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:
¿¿¿Funcionará???
No funciona. ¿qué está pasando??? ¿qué es eso de "redefinition"?
Antes de entrar en pánico, veamos qué pasa al desenredar los includes:
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.
Porque vamos a ver, ¿Para qué sirven los #define
's??? Los podemos
usar para quitar código, no???
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:
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.
Pero aún podemos darle una vuelta más, Veamos la segunda forma de hacerlo.
Resulta que tenemos declaraciones de funciones y definiciones:
Podemos separar la definición en un fichero *.c y la declaración y los tipos en un fichero *.h:
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
:
É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:
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.
https://www.online-cpp.com/7tPB2dyCGu
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.