AnteriorPosterior

11.3. Programas a partir de varios fuentes

  Curso: Fundamentos de programación en C, por Nacho Cabanes

11.3. Programas a partir de varios fuentes

11.3.1. Creación desde la línea de comandos

Es bastante frecuente que un programa no esté formado por un único fuente, sino por varios. Puede ser por hacer el programa más modular debido a su complejidad, por reparto de trabajo entre varias personas, etc.

En cualquier caso, la gran mayoría de los compiladores de C serán capaces de juntar varios fuentes independientes y crear un único ejecutable a partir de todos ellos. Vamos a ver cómo conseguirlo.

Empezaremos por el caso más sencillo: supondremos que tenemos un programa principal, y otros dos módulos auxiliares en los que hay algunas funciones que se usarán desde el programa principal.

Por ejemplo, el primer módulo (uno.c) podría ser simplemente:

/*---------------------------*/
/*  Ejemplo en C nº 98a:     */
/*  uno.c                    */
/*                           */
/*  Programa a partir de     */
/*  varios fuentes (1)       */
/*                           */
/*  Curso de C,              */
/*    Nacho Cabanes          */
/*---------------------------*/
 
void uno()
{
     printf("Función uno\n");
}
 

y el segundo módulo (dos.c):

/*---------------------------*/
/*  Ejemplo en C nº 98b:     */
/*  dos.c                    */
/*                           */
/*  Programa a partir de     */
/*  varios fuentes (2)       */
/*                           */
/*  Curso de C,              */
/*    Nacho Cabanes          */
/*---------------------------*/
 
void dos(int numero)
{
   printf("Función dos, con el parámetro %d\n", numero);
}
 

Un programa principal simple, que los utilizase, sería (TRES.C):

/*---------------------------*/
/*  Ejemplo en C nº 98c:     */
/*  tres.c                   */
/*                           */
/*  Programa a partir de     */
/*  varios fuentes (3)       */
/*                           */
/*  Curso de C,              */
/*    Nacho Cabanes          */
/*---------------------------*/
 
#include <stdio.h>
 
int main()
{
   printf("Estamos en el cuerpo del programa.\n");
   uno();
   dos(3);
   return 0;
}
 

Para compilar los tres fuentes y crear un único ejecutable, desde la mayoría de los compiladores de Dos o Windows bastaría con acceder a la línea de comandos, y teclear el nombre del compilador seguido por los de los tres fuentes:

TCC UNO DOS TRES

(TCC es el nombre del compilador, en el caso de Turbo C y de Turbo C++; sería BCC para el caso de Borland C++, SCC para Symantec C++, etc.).

Entonces el compilador convierte los tres ficheros fuente (.C) a ficheros objeto (.OBJ), los enlaza y crea un único ejecutable, que se llamaría UNO.EXE (porque UNO es el nombre del primer fuente que hemos indicado al compilador), y cuya salida en pantalla sería:

Estamos en el cuerpo del programa.
Función uno
Función dos, con el parámetro 3

En el caso de GCC para Linux (o de alguna de sus versiones para Windows, como MinGW, o DevC++, que se basa en él), tendremos que indicarle el nombre de los ficheros de entrada (con extensión) y el nombre del fichero de salida, con la opción “-o”:

gcc uno.c dos.c tres.c -o resultado

Pero puede haber compiladores en los que la situación no sea tan sencilla. Puede ocurrir que al compilar el programa principal, que era:

int main()
{
printf("Estamos en el cuerpo del programa.\n");
uno();
dos(3);
return 0;
}

el compilador nos dé un mensaje de error, diciendo que no conoce las funciones "uno()" y "dos()". No debería ser nuestro caso, si al compilar le hemos indicado los fuentes en el orden correcto (TCC UNO DOS TRES), pero puede ocurrir si se los indicamos en otro orden, o bien si tenemos muchos fuentes, que dependan los unos de los otros.

La forma de evitarlo sería indicándole que esas funciones existen, y que ya le llegarán más tarde los detalles en concreto sobre cómo funcionan.

Para decirle que existen, lo que haríamos sería incluir en el programa principal los prototipos de las funciones (las cabeceras, sin el desarrollo) que se encuentran en otros módulos, así:

/*---------------------------*/
/*  Ejemplo en C nº 98d:     */
/*  tres.c                   */
/*                           */
/*  Programa a partir de     */
/*  varios fuentes (3b)      */
/*                           */
/*  Curso de C,              */
/*    Nacho Cabanes          */
/*---------------------------*/
 
#include <stdio.h>
 
void uno();              /* Prototipos de las funciones externas */
void dos(int numero);
 
int main()               /* Cuerpo del programa */
{
   printf("Estamos en el cuerpo del programa.\n");
   uno();
   dos(3);
   return 0;
}
 

Esta misma solución de poner los prototipos al principio del programa nos puede servir para casos en los que, teniendo un único fuente, queramos declarar el cuerpo del programa antes que las funciones auxiliares:

/*---------------------------*/
/*  Ejemplo en C nº 99:      */
/*  c099.c                   */
/*                           */
/*  Funciones después de     */
/*  "main&qquot;,usando prototipos */
/*                           */
/*  Curso de C,              */
/*    Nacho Cabanes          */
/*---------------------------*/
 
#include <stdio.h>
 
void uno();              /* Prototipos de las funciones */
void dos(int numero);
 
int main()               /* Cuerpo del programa */
{
   printf("Estamos en el cuerpo del programa.\n");
   uno();
   dos(3);
   return 0;
}
 
void uno()
{
   printf("Función uno\n");
}
 
void dos(int numero)
{
   printf("Función dos, con el parámetro %d\n", numero);
}
 

En ciertos compiladores puede que tengamos problemas con este programa si no incluimos los prototipos al principio, porque en "main()" se encuentra la llamada a "uno()", que no hemos declarado. Al poner los prototipos antes, el compilador ya sabe qué tipo de función es "uno()" (sin parámetros, no devuelve ningún valor, etc.), y que los datos concretos los encontrará más adelante.

De hecho, si quitamos esas dos líneas, este programa no compila en Turbo C++ 1.01 ni en Symantec C++ 6.0, porque cuando se encuentra en "main()" la llamada a "uno()", da por supuesto que va a ser una función de tipo "int". Como después le decimos que es "void", protesta. (En cambio, GCC, que suele ser más exigente, en este caso se limita a avisarnos, pero compila el programa sin problemas).

La solución habitual en estos casos en que hay que declarar prototipos de funciones (especialmente cuando se trata de funciones compartidas por varios fuentes) suele ser agrupar estos fuentes en "ficheros de cabecera". Por ejemplo, podríamos crear un fichero llamado EJEMPLO.H que contuviese:

/* EJEMPLO.H */

void uno(); /* Prototipos de las funciones */
void dos(int numero);

y en el fuente principal escribiríamos:

#include <stdio.h>
#include "ejemplo.h"

int main()
{
printf("Estamos en el cuerpo del programa.\n");
uno();
dos(3);
return 0;
}

Aquí es importante recordar la diferencia en la forma de indicar los dos ficheros de cabecera:

<stdio.h> Se indica entre corchetes angulares porque el fichero de cabecera es propio del compilador (el ordenador lo buscará en los directorios del compilador).

"ejemplo.h" Se indica entre comillas porque el fichero H es nuestro (el ordenador lo buscará en el mismo directorio que están nuestros fuentes).

Finalmente, conviene hacer una consideración: si varios fuentes distintos necesitaran acceder a EJEMPLO.H, deberíamos evitar que este fichero se incluyese varias veces. Esto se suele conseguir definiendo una variable simbólica la primera vez que se enlaza, de modo que podamos comprobar a partir de entonces si dicha variable está definida, con #ifdef, así:

/* EJEMPLO.H mejorado */

#ifndef _EJEMPLO_H
#define _EJEMPLO_H

void uno(); /* Prototipos de las funciones */
void dos(int numero);

#endif

11.3.2. Introducción al uso de la herramienta Make

Make es una herramienta que muchos compiladores incorporan y que nos puede ser de utilidad cuando se trata de proyectos de un cierto tamaño, o al menos creados a partir de bastantes fuentes.

Su uso normal consiste simplemente en teclear MAKE. Entonces esta herramienta buscará su fichero de configuración, un fichero de texto que deberá llamarse "makefile" (podemos darle otro nombre; ya veremos cómo). Este fichero de configuración le indica las dependencias entre ficheros, es decir, qué ficheros es necesario utilizar para crear un determinado "objetivo". Esto permite que no se recompile todos y cada uno de los fuentes si no es estrictamente necesario, sino sólo aquellos que se han modificado desde la última compilación.

En general, el contenido del fichero "makefile" será algo parecido a esto:

objetivo: dependencias
    órdenes

(En la primera línea se escribe el objetivo, seguido por dos puntos y por la lista de dependencias; en la segunda línea se escribe la orden que hay que dar en caso de que sea necesario recompilar, precedida por un tabulador). Si queremos añadir algún comentario, basta con precederlos con el símbolo #.

Vamos a crear un ejecutable llamado PRUEBA.EXE a partir de cuatro ficheros fuente llamados UNO.C, DOS.C, TRES.C, CUATRO.C, usando Turbo C++.

Así, nuestro primer “makefile” podría ser un fichero que contuviese el siguiente texto:

PRUEBA.EXE: UNO.C DOS.C TRES.C CUATRO.C
    TCC -ePRUEBA.EXE UNO.C DOS.C TRES.C CUATRO.C

Es decir: nuestro objetivo es conseguir un fichero llamado PRUEBA.EXE, que queremos crear a partir de varios ficheros llamados UNO.C, DOS.C, TRES.C y CUATRO.C. La orden que queremos dar es la que aparece en la segunda línea, y que permite, mediante el compilador TCC, crear un ejecutable llamado PRUEBA.EXE a partir de cuatro fuentes con los nombres anteriores. (La opción "-e" de Turbo C++ permite indicar el nombre que queremos que tenga el ejecutable; si no, se llamaría UNO.EXE, porque tomaría su nombre del primero de los fuentes).

¿Para qué nos sirve esto? De momento, nos permite ahorrar tiempo: cada vez que tecleamos MAKE, se lee el fichero MAKEFILE y se compara la fecha (y hora) del objetivo con la de las dependencias; si el fichero objetivo no existe o si es más antiguo que alguna de las dependencias, se realiza la orden que aparece en la segunda línea (de modo que evitamos escribirla completa cada vez).

En nuestro caso, cada vez que tecleemos MAKE, ocurrirá una de estas tres posibilidades

  • Si no existe el fichero PRUEBA.EXE, se crea uno nuevo utilizando la orden de la segunda línea.
  • Si ya existe y es más reciente que los cuatro fuentes, no se recompila ni se hace nada, todo queda como está.
  • Si ya existe, pero se ha modificado alguno de los fuentes, se recompilará de nuevo para obtener un fichero PRUEBA.EXE actualizado.

Eso sí, estamos dando por supuesto varias cosas “casi evidentes”:

  • Que tenemos la herramienta MAKE y está accesible (en el directorio actual o en el PATH).
  • Que hemos creado el fichero MAKEFILE.
  • Que existen los cuatro ficheros fuente UNO.C, DOS.C, TRES.C y CUATRO.C.
  • Que existe el compilador TCC y está accesible (en el directorio actual o en el PATH).

Vayamos mejorando este MAKEFILE rudimentario. La primera mejora es que si la lista de dependencias no cabe en una única linea, podemos partirla en dos, empleando la barra invertida \

PRUEBA.EXE: UNO.C DOS.C \ #Objetivo y dependencias
TRES.C CUATRO.C # Mas dependencias
TCC -ePRUEBA.EXE UNO.C DOS.C TRES.C CUATRO.C #Orden a dar

Al crear el MAKEFILE habíamos ganado en “velocidad de tecleo” y en que no se recompilase todo nuevamente si no se ha modificado nada. Pero en cuanto un fuente se modifica, nuestro MAKEFILE recompila todos otra vez, aunque los demás no hayan cambiado. Esto podemos mejorarlo añadiendo un paso intermedio (la creación cada fichero objeto OBJ) y más objetivos (cada fichero OBJ, a partir de cada fichero fuente), así:

# Creacion del fichero ejecutable

prueba.exe: uno.obj dos.obj tres.obj
tcc -eprueba.exe uno.obj dos.obj tres.obj

# Creacion de los ficheros objeto

uno.obj: uno.c
tcc -c uno.c

dos.obj: dos.c
tcc -c dos.c

tres.obj: tres.c
tcc -c tres.c

Estamos detallando los pasos que normalmente se dan al compilar, y que muchos compiladores realizan en una única etapa, sin que nosotros nos demos cuenta: primero se convierten los ficheros fuente (ficheros con extensión C) a código máquina (código objeto, ficheros con extensión OBJ) y finalmente los ficheros objeto se enlazan entre sí (y con las bibliotecas propias del lenguaje) para dar lugar al programa ejecutable (en MsDos y Windows normalmente serán ficheros con extensión EXE).

Así conseguimos que cuando modifiquemos un único fuente, se recompile sólo este (y no todos los demás, que pueden ser muchos) y después se pase directamente al proceso de enlazado, con lo que se puede ganar mucho en velocidad si los cambios que hemos hecho al fuente son pequeños.

(Nota: la opción "-c" de Turbo C++ es la que le indica que sólo compile los ficheros de C a OBJ, pero sin enlazarlos después).

Si tenemos varios MAKEFILE distintos (por ejemplo, cada uno para un compilador diferente, o para versiones ligeramente distintas de nuestro programa), nos interesará poder utilizar nombres distintos.

Esto se consigue con la opción "-f" de la orden MAKE, por ejemplo si tecleamos

MAKE -fPRUEBA

la herramienta MAKE buscaría un fichero de configuración llamado PRUEBA o bien PRUEBA.MAK.

Podemos mejorar más aún estos ficheros de configuración. Por ejemplo, si precedemos la orden por @, dicha orden no aparecerá escrita en pantalla

PRUEBA.EXE: UNO.C DOS.C TRES.C CUATRO.C
@TCC -ePRUEBA.EXE UNO.C DOS.C TRES.C CUATRO.C

Y si precedemos la orden por & , se repetirá para los ficheros indicados como "dependencias". Hay que usarlo en conjunción con la macro $**, que hace referencia a todos los ficheros dependientes, o $?, que se refiere a los ficheros que se hayan modificado después que el objetivo.

copiaSeguridad: uno.c dos.c tres.c
&copy $** a:\fuentes

Una última consideración: podemos crear nuestras propias macros, con la intención de que nuestro MAKEFILE resulte más fácil de leer y de mantener, de modo que una versión más legible de nuestro primer fichero sería:

FUENTES = uno.c dos.c tres.c
COMPIL = tcc

prueba.exe: $(FUENTES)
$(COMPIL) -eprueba.exe $(FUENTES)

Es decir, las macros se definen poniendo su nombre, el signo igual y su definición, y se emplean precediéndolas de $ y encerrándolas entre paréntesis.

Pero todavía hay más que no hemos visto. Las herramientas MAKE suelen permitir otras posibilidades, como la comprobación de condiciones (con las directivas "!if", "!else" y similares) o la realización de operaciones (con los operadores estándar de C: +, *, %, >>, etc). Quien quiera profundizar en estos y otros detalles, puede recurrir al manual de la herramienta MAKE que incorpore su compilador.

¿Alguna diferencia en Linux? Pocas. Sólo hay que recordar que en los sistemas Unix se distingue entra mayúsculas y minúsculas, por lo que la herramienta se llama “make”, y el fichero de datos “Makefile” o “makefile” (preferible la primera nomenclatura, con la primera letra en mayúsculas). De igual modo, el nombre del compilador y los de los fuentes se deben escribir dentro del “Makefile” exactamente como se hayan creado (habitualmente en minúsculas).


11.3.3. Introducción a los “proyectos”

En la gran mayoría de los compiladores que incorporan un “entorno de desarrollo”, existe la posibilidad de conseguir algo parecido a lo que hace la herramienta MAKE, pero desde el propio entorno. Es lo que se conoce como “crear un proyecto”.

Se suele poder hacer desde desde un menú llamado “Proyecto”, donde existirá una opción “Nuevo proyecto” (en inglés Project / New Project), o a veces desde el menú Archivo.

En muchas ocasiones, tendremos varios tipos de proyectos disponibles, gracias a que algún asistente deje el esqueleto del programa preparado para nosotros.

Desde esta primera ventana también le daremos ya un nombre al proyecto (será el nombre que tendrá el fichero ejecutable), y también desde aquí podemos añadir los fuentes que formarán parte del proyecto, si los tuviéramos creados desde antes (suele ser algo como “Añadir fichero”, o en inglés “Add Item”).

En una cierta ventana de la pantalla tendremos información sobre los fuentes que componen nuestro proyecto (en el ejemplo, tomado de Turbo C++ Second Edition´, esta ventana aparece en la parte inferior de la pantalla).

En otros entornos, como Anjuta o KDevelop, esta información aparece en la parte izquierda de la pantalla:

 

Las ventajas que obtenemos al usar “proyectos” son:

  • La posibilidad de manipular varios fuentes a la vez y de recompilar un programa que esté formado por todos ellos, sin necesidad de salir del entorno de desarrollo.
  • La ventaja añadida de que el gestor de proyectos sólo recompilará aquellos fuentes que realmente se han modificado, como comentábamos que hacía la herramienta MAKE.

Actualizado el: 16-01-2015 00:13

AnteriorPosterior