Repaso de C: ficheros de cabecera y preprocesador

Ficheros de cabecera:

Los ficheros de cabecera o “header files” ya se han introducido en entradas anteriores, concretamente al revisar las librerías de cc65 para hacer entrada / salida, manejar un joystick, un ratón, gráficos bitmap, sprites o sonido.

Estos ficheros sirven para recoger definiciones de constantes simbólicas, prototipos de funciones, tipos de datos, macros, etc. Son útiles especialmente en el contexto de programas grandes que necesitan ser troceados en varios ficheros, o que utilizan librerías propias o ajenas.

De este modo, es posible concentrar esas definiciones comunes en uno o varios ficheros *.h, y luego referenciar esos ficheros y sus definiciones desde los ficheros *.c que las utilizan, evitando así los errores que podrían derivarse de tener que replicarlas en varios sitios.

Los ficheros de cabecera guardan mucha relación con el preprocesador de C. En primer lugar, porque muchas de las definiciones que incluyen, por ejemplo, una constante simbólica o una macro, se definen mediante directivas del preprocesador (ej. #define SPRITES_NUM 8). Y, en segundo lugar, porque para referenciar o incluir un fichero de cabecera en un fichero de implementación también se utilizan las directivas del preprocesador #include <fichero.h> o #include “fichero.h”.

La directiva #include, como su nombre indica, lo que hace es incluir las definiciones del fichero de cabecera en el fichero de implementación, justo antes de que éste se compile.

Preprocesador:

El preprocesador también lo hemos introducido en entradas anteriores. Como hemos adelantado, “es un paso previo a la compilación”.

Las dos directivas más habituales del preprocesador son #define, para definir constantes simbólicas y macros, e #include, para incluir el contenido de un fichero en otro. Ambas han sido presentadas también.

#define no sólo vale para definir constantes simbólicas, es decir, literales que se sustituyen por un valor (ej. #define SPRITES_NUM 8). También vale para definir macros.

Las macros son básicamente lo mismo que una constante, pero con algunos parámetros. Por ejemplo, si definimos #define MULTI(A, B) ((A)*(B)), cada vez que el preprocesador se encuentre MULTI(A, B) en el código fuente, lo sustituirá por ((A)*(B)), donde A y B lógicamente son parámetros. Es decir, MULTI(2, 4) se sustituiría por ((2)*(4)).

No es obligatorio abusar tanto de los paréntesis, pero es habitual hacerlo para garantizar la correcta asociación en caso de que los valores de A o B no sean simples (ej. si A = 1+1).

Una macro se parece a una función, porque tiene parámetros, pero no es lo mismo. Cada aparición de la macro se sustituye por su definición en tiempo de preprocesamiento, justo antes de compilar. Por tanto, el concepto es más similar al de una macro de un macroensamblador (defm – endm en CBM prg Studio).

Por otra lado, #include <fichero.h> sirve para buscar fichero.h en la ruta donde el compilador guarda por defecto las librerías, e incluirlo en el fichero que se está procesando. En el caso de cc65 esa ruta por defecto es cc65\include.

La variante #include “fichero.h” hace lo mismo, pero busca el fichero de cabecera primero en la ruta actual o de trabajo, es decir, donde se encuentra el fichero que se está procesando o compilando en ese momento. Si no lo encuentra ahí, busca en la ruta por defecto.

El preprocesador vale para más cosas, por ejemplo, para hacer inclusión condicional. Con una estructura como esta en un fichero de cabecera:

es posible evaluar una condición, concretamente si está definida la constante CONST, y actuar en consecuencia. En este ejemplo, si CONST no está definida se define y, además, se definen las constantes, macros, prototipos y demás construcciones que fueran de interés.

Estos controles son bastante habituales en los ficheros de cabecera. Ha de tenerse en cuenta que algunos ficheros de cabecera incluyen a otros, y estos a otros, por ejemplo, cabeceras de librerías estándar, y al final el galimatías de referencias cruzadas acaba siendo muy complejo. Por ello, para evitar que el preprocesador incluya varias veces las mismas definiciones, se verifica si una bandera (CONST) ya está definida y, caso de ya estarlo, no se hace nada; si no está definida, se define y se acompañan el resto de definiciones.

#ifdef CONST e #ifndef CONST, sin paréntesis, son versiones abreviadas de #if defined(CONST) e #if !defined(CONST) respectivamente.


Código de ejemplo: macros