Subsecciones

21. Compilación de Programas con Archivos Múltiples

En este capítulo se revisa los aspectos teoricos y prácticos que necesitan ser considerados cuando son escritos programas grandes.

Cuando se escriben programas grandes se deberá programar en módulos. Estos serán archivos fuentes separados. La función main() deberá estar en un archivo, por ejemplo en main.c, y los otros archivos tendrán otras funciones.

Se puede crear una biblioteca propia de funciones escribiendo una suite de subrutinas en uno o más módulos. De hecho los módulos pueden ser compartidos entre muchos programas simplemente incluyendo los módulos al compilar como se verá a continuación.

Se tiene varias ventajas si los programas son escritos de esta forma:

21.1 Archivos Cabezera

Si se adopta el modelo modular entonces se querrá tener para cada módulo las definiciones de las variables, los prototipos de las funciones, etc. Sin embargo, ¿qué sucede si varios módulos necesitan compartir tales definiciones? En tal caso, lo mejor es centralizar las definiciones en un archivo, y compartir el archivo entre los módulos. Tal archivo es usualmente llamado un archivo cabecera.

Por convención estos archivos tienen el sufijo .h

Se han revisado ya algunos archivos cabecera de la biblioteca estándar, por ejemplo:

#include <stdio.h>

Se pueden definir los propios archivos cabecera y se pueden incluir en el programa como se muestra enseguida:

#include "mi_cabecera.h"

Los archivos cabecera por lo general sólo contienen definiciones de tipos de datos, prototipos de funciones y comandos del preprocesador de C.

Considerar el siguiente programa de ejemplo:

main.c

/*
 *      main.c
 */
#include "cabecera.h"
#include <stdio.h>

char    *Otra_cadena = "Hola a Todos";

main()
{
	printf("Ejecutando...\n");

	/*
	*      Llamar a EscribirMiCadena() - definida en otro archivo
	*/
	EscribirMiCadena(MI_CADENA);

	printf("Terminado.\n");
}

EscribirMiCadena.c

/*
*      EscribirMiCadena.c
*/
extern char     *Otra_cadena;

void EscribirMiCadena(EstaCadena)
char    *EstaCadena;
{
	printf("%s\n", EstaCadena);
	printf("Variable Global = %s\n", Otra_cadena);
}

cabecera.h

/*
 *      cabecera.h
 */
#define MI_CADENA "Hola Mundo"

void EscribirMiCadena();

Cada módulo será compilado separadamente como se verá más adelante.

Algunos módulos tienen la directiva de preprocesamiento #include "cabecera.h" ya que comparten definiciones comunes. Algunos como main.c también incluyen archivos cabecera estándar. La función main.c llama a la función EscribirMiCadena() la cual esta en el módulo (archivo) EscribirMiCadena.c

El prototipo void de la función EscribirMiCadena esta definida en cabecera.h.

Observar que en general se debe decidir entre tener un módulo .c que tenga acceso solamente a la información que necesita para su trabajo, con la consecuencia de mantener muchos archivos cabecera y tener programas de tamaño moderado con uno o dos archivos cabecera (probablemente lo mejor) que compartan más definiciones de módulos.

Un problema que se tiene al emplear módulos son el compartir variables. Si se tienen variables globales declaradas y son instanciadas en un módulo, ¿cómo pueden ser pasadas a otros módulos para que sean conocidas?

Se podrían pasar los valores como parámetros a las funciones, pero:

21.2 Variables y Funciones Externas

Las variables y argumentos definidos dentro de las funciones son ``internas'', es decir, locales.

Las variables ``externas'' están definidas fuera de las funciones -- se encuentran potencialmente disponibles a todo el programa (globales) pero NO necesariamente. Las variables externas son siempre permanentes.

En el lenguaje C, todas las definiciones de funciones son externas, NO se pueden tener declaraciones de funciones anidadas como en PASCAL.

21.2.1 Alcance de las variables externas

Una variable externa (o función) no es siempre totalmente global. En el lenguaje C se aplica la siguiente regla:

El alcance de una variable externa (o función) inicia en el punto de declaración hasta el fin del archivo (módulo) donde fue declarada.

Considerar el siguiente código:

main()
   { ... }

int que_alcance;
float fin_de_alcance[10];

void que_global()
   { ... }

char solitaria;

float fn()
   { ... }

La función main() no puede ver a las variables que_alcance o fin_de_alcance, pero las funciones que_global() y fn() si pueden. Solamente la función fn() puede ver a solitaria.

Esta es también una de las razones por las que se deben poner los prototipos de las funciones antes del cuerpo del código.

Por lo que en el ejemplo la función main no conocerá nada acerca de las funciones que_global() y fn(). La función que_global() no sabe nada acerca de la función fn(), pero fn() si sabe acerca de la función que_global(), ya que esta aparece declarada previamente.

La otra razón por la cual se usan los prototipos de las funciones es para revisar los parámetros que serán pasados a las funciones.

Si se requiere hacer referencia a una variable externa antes de que sea declarada o que esta definida en otro módulo, la variable debe ser declarada como una variable externa, por ejemplo:

extern int que_global;

Regresando al ejemplo de programación modular, se tiene una arreglo de caracteres tipo global Otra_cadena declarado en main.c y que esta compartido con EscribirMiCadena donde esta declarada como externa.

Se debe tener cuidado con el especificador de almacenamiento de clase ya que el prefijo es una declaración, NO una definición, esto es, no se da almacenamiento en la memoria para una variable externa -- solamente le dice al compilador la propiedad de la variable. La variable actual sólo deberá estar definida una vez en todo el programa -- se pueden tener tantas declaraciones externas como se requieran.

Los tamaños de los arreglos deberán ser dados dentro de la declaración, pero no son necesarios en las declaraciones externas, por ejemplo:

main.c      int arr[100];

arch.c      extern int arr[];

21.3 Ventajas de Usar Varios Archivos

Las ventajas principales de dispersar un programa en varios archivos son:

21.4 Como dividir un programa en varios archivos

Cuando un programa es separado en varios archivos, cada archivo contendrá una o más funciones. Un archivo incluirá la función main() mientras los otros contendrán funciones que serán llamados por otros. Estos otros archivos pueden ser tratados como funciones de una biblioteca.

Los programadores usualmente inician diseñando un programa dividiendo el problema en secciones más fácilmente manejables. Cada una de estas secciones podrán ser implementaddas como una o más funciones. Todas las funciones de cada sección por lo general estarán en un sólo archivo.

Cuando se hace una implementación tipo objeto de las estructuras de datos, es usual tener todas las funciones que accesan ése objeto en el mismo archivo. Las ventajas de lo anterior son:

El archivo contiene la definición de un objeto, o funciones que regresasan valores, hay una restricción en la llamada de estas funciones desde otro archivo, al menos que la definición de las funciones estén en el archivo, no será posible compilar correctamente.

La mejor solución a este problema es escribir un archivo cabecera para cada archivo de C, estos tendrán el mismo nombre que el archivo de C, pero terminarán en .h. El archivo cabecera contiene las definiciones de todas las funciones usadas en el archivo de C.

Cuando una función en otro archivo llame una función de nuestro archivo de C, se puede definir la función usando la directiva #include con el archivo apropiado .h

21.5 Organización de los Datos en cada Archivo

Cualquier archivo deberá tener sus datos organizados en un cierto orden, tipícamente podrá ser la siguiente:

El orden anterior es importante ya que cada objeto deberá estar definido antes de que pueda ser usado. Las funciones que regresan valores deberán estar definidos antes de que sean llamados. Esta definición podría ser una de las siguientes:

Una función definida como:

float enc_max(float a, float b, float c)
{  ...  }

podrá tener el siguiente prototipo:

float enc_max(float a, float b, float c);

El prototipo puede aparecer entre las variables globales en el inicio del archivo fuente. Alternativamente puede ser declarado en el archivo cabecera el cual es leído usando la directiva #include.

Es importante recordar que todos los objetos en C deberán estar declarados antes de ser usados.

21.6 La utilería Make

La utilería Make es un manejador inteligente de programas que mantiene la integridad de una colección de módulos de un programa, una colección de programas o un sistema completo -- no tienen que ser programas, en la práctica puede ser cualquier sistema de archivos (por ejemplo, capítulos de texto de un libro que esta siendo tipografiado). Su uso principal ha sido en la asistencia del desarrollo de sistemas de software.

Esta utilería fue inicialmente desarrollada para UNIX, pero actualmente esta disponible en muchos sistemas.

Observar que make es una herramienta del programador, y no es parte del lenguaje C o de alguno otro.

Supongamos el siguiente problema de mantenimiento de una colección grande de archivos fuente:

main.c f1.c ...... fn.c

Normalmente se compilarán los archivos de la siguiente manera:

gcc -o main main.c f1.c ....... fn.c

Sin embargo si se sabe que algunos archivos han sido compilados previamente y sus archivos fuente no han sido cambiados desde entonces, entonces se puede ahorrar tiempo de compilación ligando los códigos objetos de estos archivos, es decir:

gcc -o main main.c f1.c ... fi.o ... fj.o ... fn.c

Se puede usar la opción -c del compilador de C para crear un código objeto (.o) para un módulo dado. Por ejemplo:

gcc -c main.c

que creará un archivo main.o. No se requiere proporcionar ninguna liga de alguna biblioteca ya que será resuelta en la etapa de ligamiento.

Se tiene el problema en la compilación del programa de ser muy larga, sin embargo:

Si se usa la utilería make todo este control es hecho con cuidado. En general sólo los módulos que sean más viejos que los archivos fuente serán recompilados.

21.6.1 Programando Make

La programación de make es directa, basicamente se escribe una secuencia de comandos que describe como nuestro programa (o sistema de programas) será construído a partir de los archivos fuentes.

La secuencia de construción es descrita en los archivos makefile, los cuales contienen reglas de dependencia y reglas de construcción.

Una regla de dependencia tiene dos partes -- un lado izquierdo y un lado derecho separados por :

lado izquierdo : lado derecho

El lado izquierdo da el nombre del destino (los nombres del programa o archivos del sistema) que será construído (target), mientras el lado derecho da los nombres de los archivos de los cuales depende el destino (por ejemplo, archivos fuente, archivos cabecera, archivos de datos).

Si el destino esta fuera de fecha con respecto a las partes que le constituyen, las reglas de construcción siguiendo las reglas de dependencia son usadas.

Por lo tanto para un programa típico de C cuando un archivo make es ejecutado las siguientes tareas son hechas:

  1. El archivo make es leído. El Makefile indica cuales objetos y archivos de biblioteca se requieren para ser ligados y cuales archivos cabecera y fuente necesitan ser compilados para crear cada archivo objeto.
  2. La hora y la fecha de cada archivo objeto son revisados contra el código fuente y los archivos cabecera de los cuales dependen. Si cualquier fuente o archivo cabecera son más recientes que el archivo objeto, entonces los archivos han sido modificados desde la última modificación y por lo tanto los archivos objeto necesitan ser recompilados.
  3. Una vez que todos los archivos objetos han sido revisados, el tiempo y la fecha de todos los archivos objeto son revisados contra los archivos ejecutables. Si existe archivos ejecutables viejos serán recompilados.

Observar que los archivos make pueden obedecer cualquier comando que sea tecleado en la línea de comandos, por consiguiente se pueden usar los archivos make para no solamente compilar archivos, sino también para hacer respaldos, ejecutar programas si los archivos de datos han sido cambiados o limpieza de directorios.

21.7 Creación de un Archivo Make (Makefile)

La creación del archivo es bastante simple, se crea un archivo de texto usando algún editor de textos. El archivo Makefile contiene solamente una lista de dependencias de archivos y comandos que son necesarios para satisfacerlos.

Se muestra a continuación un ejemplo de un archivo make:

prog: prog.o f1.o f2.o
   gcc -o prog prog.o f1.o f2.o -lm ...

prog.o: cabecera.h prog.c
   gcc -c prog.c

f1.o: cabecera.h f1.c
   gcc -c f1.c

f2.o: ....
   ...

La utilería make lo interpretará de la siguiente forma:

  1. prog depende de tres archivos: prog.o, f1.o y f2.o. Si cualquiera de los archivos objeto ha cambiado desde la última compilación los archivos deben ser religados.
  2. prog.o depende de 2 archivos, si estos han cambiado prog.o deberá ser recompilado. Lo mismo sucede con f1.o y f2.o.

Los últimos 3 comandos en makefile son llamados reglas explícitas -- ya que los archivos en los comandos son listados por nombre.

Se pueden usar reglas implícitas en makefile para generalizar reglas y hacer más compacta la escritura.

Si se tiene:

f1.o: f1.c
   gcc -c f1.c

f2.o: f2.c
   gcc -c f2.c

se puede generalizar a:

.c.o: gcc -c $<

Lo cual se lee como .ext_fuente.ext_destino: comando donde $< es una forma breve para indicar los archivos que tienen la extensión .c

Se pueden insertar comentarios en un Makefile usando el símbolo #, en donde todos los caracteres que siguen a # son ignorados.

21.8 Uso de macros con Make

Se pueden definir macros para que sean usadas por make, las cuales son tipícamente usadas para guardar nombres de archivos fuente, nombres de archivos objeto, opciones del compilador o ligas de bibliotecas.

Se definen en una forma simple, por ejemplo:

FUENTES          = main.c f1.c f2.c
CFLAGS           = -ggdb -C
LIBS             = -lm
PROGRAMA         = main
OBJETOS          = (FUENTES: .c = .o)
en donde (FUENTES: .c = .o) cambia la extensión .c de los fuentes por la extensión .o

Para referirse o usar una macro con make se debe hacer $(nomb_macro), por ejemplo:

$(PROGRAMA) : $(OBJETOS)
$(LINK.C) -o $@ $(OBJETOS) $(LIBS)

En el ejemplo mostrado se observa que:

Existen varias macros internas a continuación se muestran algunas de ellas:

$*
Parte del nombre del archivo de la dependencia actual sin el sufijo.
$@
Nombre completo del destino actual.
$
Archivo .c del destino

Un ejemplo de un makefile para el programa modular discutido previamente se muestra a continuación:

#
# Makefile
#
FUENTES.c=main.c EscribirMiCadena.c
INCLUDES=
CFLAGS=
SLIBS=
PROGRAMA=main

OBJETOS=$(FUENTES.c:.c=.o)

# Destino (target) especial (inicia con .)
.KEEP_STATE:

debug := CFLAGS=-ggdb

all debug: $(PROGRAMA)

$(PROGRAMA): $(INCLUDES) $(OBJETOS)
	$(LINK.c) -o $@ $(OBJETOS) $(SLIBS)

clean:
	rm -f $(PROGRAMA) $(OBJETOS)

Para ver más información de esta utilería dentro de Linux usar info make

21.9 Ejecución de Make

Para usar make solamente se deberá teclear en la línea de comandos. El sistema operativo automáticamente busca un archivo con el nombre Makefile (observar que la primera letra es mayúscula y el resto minúsculas), por lo tanto si se tiene un archivo con el nombre Makefile y se teclea make en la línea de comandos, el archivo Makefile del directorio actual será ejecutado.

Se puede anular esta búsqueda de este archivo tecleando make -f makefile.

Existen algunas otras opciones para make las cuales pueden ser consultadas usando man.