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:
make
nos ayudan a mantener sistemas grandes.
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:
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.
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[];
Las ventajas principales de dispersar un programa en varios archivos son:
make
de UNIX es muy útil para reconstruir programas con 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
Cualquier archivo deberá tener sus datos organizados en un cierto orden, tipícamente podrá ser la siguiente:
#define
), cabeceras de archivos (#include
) y los tipos de datos importantes (typedef
).
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.
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:
.c
-- si el módulo ha
sido compilado antes y no ha sido modificado el archivo fuente, por lo tanto
no hay necesidad de recompilarlo. Se puede solamente ligar los archivos
objeto. Sin embargo, no será fácil recordar cuales archivos han sido
actualizados, por lo que si ligamos un archivo objeto no actualizado, el
programa ejecutable final estará incorrecto.
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.
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:
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.
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.
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:
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.
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.
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:
$(PROGRAMA) : $(OBJETOS)
genera una lista de dependencias y el destino.
$@
.
Existen varias macros internas a continuación se muestran algunas de ellas:
.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
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
.