23 El preprocesador

El preprocesador analiza el fichero fuente antes de la fase de compilación real, y realiza las sustituciones de macros y procesa las directivas del preprocesador. El preprocesador también elimina los comentarios.

Una directiva de preprocesador es una línea cuyo primer carácter es un #.

A continuación se describen las directivas del preprocesador, aunque algunas ya las hemos visto antes.

Directiva #define

La directiva #define, sirve para definir macros. Las macros suministran un sistema para la sustitución de palabras, con y sin parámetros.

Sintaxis:

#define identificador_de_macro <secuencia>

El preprocesador sustituirá cada ocurrencia del identificador_de_macro en el fichero fuente, por la secuencia. Aunque como veremos, hay algunas excepciones. Cada sustitución se conoce como una expansión de la macro. La secuencia es llamada a menudo cuerpo de la macro.

Si no se especifica una secuencia, el identificador_de_macro sencillamente, será eliminado cada vez que aparezca en el fichero fuente.

Después de cada expansión individual, se vuelve a examinar el texto expandido a la búsqueda de nuevas macros, que serán expandidas a su vez. Esto permite la posibilidad de hacer macros anidadas. Si la nueva expansión tiene la forma de una directiva de preprocesador, no será reconocida como tal.

Existen otras restricciones a la expansión de macros:

Las ocurrencias de macros dentro de literales, cadenas, constantes alfanuméricas o comentarios no serán expandidas.

Una macro no será expandida durante su propia expansión, así #define A A, no será expandida indefinidamente.

No es necesario añadir un punto y coma para terminar una directiva de preprocesador. Cualquier carácter que se encuentre en una secuencia de macro, incluido el punto y coma, aparecerá en la expansión de la macro. La secuencia termina en el primer retorno de línea encontrado. Las secuencias de espacios o comentarios en la secuencia, se expandirán como un único espacio.

Directiva #undef

Sirve para eliminar definiciones de macros previamente definidas. La definición de la macro se olvida y el identificador queda indefinido.

Sintaxis:

#undef identificador_de_macro

La definición es una propiedad importante de un identificador. Las directivas condicionales #ifdef e #ifndef se basan precisamente en esta propiedad de los identificadores. Esto ofrece un mecanismo muy potente para controlar muchos aspectos de la compilación.

Después de que una macro quede indefinida puede ser definida de nuevo con #define, usando la misma u otra definición.

Si se intenta definir un identificador de macro que ya esté definido, se producirá un aviso, un warning, si la definición no es exactamente la misma. Es preferible usar un mecanismo como este para detectar macros existentes:

#ifndef NULL
  #define NULL 0L
#endif

De este modo, la línea del #define se ignorará si el símbolo NULL ya está definido.

Directivas #if, #elif, #else y #endif

Permiten hacer una compilación condicional de un conjunto de líneas de código.

Sintaxis:

#if expresión-constante-1
<sección-1>
#elif <expresión-constante-2>
<sección-2>
.
.
.
#elif <expresión-constante-n>
<sección-n>
<#else>
<sección-final>
#endif

Todas las directivas condicionales deben completarse dentro del mismo fichero. Sólo se compilarán las líneas que estén dentro de las secciones que cumplan la condición de la expresión constante correspondiente.

Estas directivas funcionan de modo similar a los operadores condicionales C++. Si el resultado de evaluar la expresión-constante-1, que puede ser una macro, es distinto de cero (true), las líneas representadas por sección-1, ya sean líneas de comandos, macros o incluso nada, serán compiladas. En caso contrario, si el resultado de la evaluación de la expresión-constante-1, es cero (false), la sección-1 será ignorada, no se expandirán macros ni se compilará.

En el caso de ser distinto de cero, después de que la sección-1 sea preprocesada, el control pasa al #endif correspondiente, con lo que termina la secuencia condicional. En el caso de ser cero, el control pasa al siguiente línea #elif, si existe, donde se evaluará la expresión-constante-2. Si el resultado es distinto de cero, se procesará la sección-2, y después el control pasa al correspondiente #endif. Por el contrario, si el resultado de la expresión-constante-2 es cero, el control pasa al siguiente #elif, y así sucesivamente, hasta que se encuentre un #else o un #endif. El #else, que es opcional, se usa como una condición alternativa para el caso en que todas la condiciones anteriores resulten falsas. El #endif termina la secuencia condicional.

Cada sección procesada puede contener a su vez directivas condicionales, anidadas hasta cualquier nivel, cada #if debe corresponderse con el #endif más cercano.

El objetivo de una red de este tipo es que sólo una sección, aunque se trate de una sección vacía, sea compilada. Las secciones ignoradas sólo son relevantes para evaluar las condiciones anidadas, es decir asociar cada #if con su #endif.

Las expresiones constantes deben poder ser evaluadas como valores enteros.

Directivas #ifdef e #ifndef

Estas directivas permiten comprobar si un identificador está o no actualmente definido, es decir, si un #define ha sido previamente procesado para el identificador y si sigue definido.

Sintaxis:

#ifdef <identificador>
#ifndef <identificador>

La línea:

#ifdef identificador

tiene exactamente el mismo efecto que

#if 1

si el identificador está actualmente definido, y el mismo efecto que

#if 0

si el identificador no está definido.

#ifndef comprueba la no definición de un identificador, así la línea:

#ifndef identificador

tiene el mismo efecto que

#if 0

si el identificador está definido, y el mismo efecto que

#if 1

si el identificador no está definido.

Por lo demás, la sintaxis es la misma que para #if, #elif, #else, y #endif.

Un identificador definido como nulo, se considera definido.

Directiva #error

Esta directiva se suele incluir en sentencias condicionales de preprocesador para detectar condiciones no deseadas durante la compilación. En un funcionamiento normal estas condiciones serán falsas, pero cuando la condición es verdadera, es preferible que el compilador muestre un mensaje de error y detenga la fase de compilación. Para hacer esto se debe introducir esta directiva en una sentencia condicional que detecte el caso no deseado.

Sintaxis:

#error mensaje_de_error 

Esta directiva genera el mensaje:

Error: nombre_de_fichero nº_línea : Error directive: mensaje_de_error

Este ejemplo está extraído de uno de los ficheros de cabecera del compilador GCC:

#ifndef BFD_HOST_64_BIT
 #error No 64 bit integer type available
#endif /* ! defined (BFD_HOST_64_BIT) */

Directiva #include

La directiva #include, como ya hemos visto, sirve para insertar ficheros externos dentro de nuestro fichero de código fuente. Estos ficheros son conocidos como ficheros incluidos, ficheros de cabecera o "headers".

Sintaxis:

#include <nombre de fichero cabecera>
#include "nombre de fichero de cabecera"
#include identificador_de_macro

El preprocesador elimina la línea #include y, conceptualmente, la sustituye por el fichero especificado. El tercer caso haya el nombre del fichero como resultado de aplicar la macro.

El código fuente en si no cambia, pero el compilador "ve" el fichero incluido. El emplazamiento del #include puede influir sobre el ámbito y la duración de cualquiera de los identificadores en el interior del fichero incluido.

La diferencia entre escribir el nombre del fichero entre "<>" o """", está en el algoritmo usado para encontrar los ficheros a incluir. En el primer caso el preprocesador buscará en los directorios "include" definidos en el compilador. En el segundo, se buscará primero en el directorio actual, es decir, en el que se encuentre el fichero fuente, si no existe en ese directorio, se trabajará como el primer caso.

Si se proporciona el camino como parte del nombre de fichero, sólo se buscará es el directorio especificado.

Directiva #line

No se usa, se trata de una característica heredada de los primitivos compiladores C.

Sintaxis:

#line constante_entera <"nombre_de_fichero">

Esta directiva se usa para sustituir los números de línea en los programas de referencias cruzadas y en mensajes de error. Si el programa consiste en secciones de otros ficheros fuente unidas en un sólo fichero, se usa para sustituir las referencias a esas secciones con los números de línea del fichero original, como si no se hubiera integrado en un único fichero.

La directiva #line indica que la siguiente línea de código proviene de la línea "constante_entera" del fichero "nombre_de_fichero". Una vez que el nombre de fichero ha sido registrado, sucesivas apariciones de la directiva #line relativas al mismo fichero pueden omitir el argumento del nombre.

Las macros serán expandidas en los argumentos de #line del mismo modo que en la directiva #include.

La directiva #line se usó originalmente para utilidades que producían como salida código C, y no para código escrito por personas.

Directiva #pragma

Sintaxis:

#pragma nombre-de-directiva

Con esta directiva, cada compilador puede definir sus propias directivas, que no interferirán con las de otros compiladores. Si el compilador no reconoce el nombre-de-directiva, ignorará la línea completa sin producir ningún tipo de error o warning.

Teniendo lo anterior en cuenta, veamos una de las directivas pragma más extendidas en compiladores, pero teniendo en cuenta que no tienen por qué estar disponibles en el compilador que uses.

Tampoco es bueno usar estas directivas, ya que suelen hacer que el código no sea portable, es mejor buscar alternativas.

Directiva #pragma pack()

Esta directiva se usa para cambiar la alienación de bytes cuando se declaran objetos o estructuras.

Recordemos lo que nos pasaba al aplicar el {cc:011b#STR_sizeof:operador sizeof a estructuras} con el mismo número y tipo de campos, aunque en distinto orden.

Algunas veces puede ser conveniente alterar el comportamiento predefinido del compilador en lo que respecta al modo de empaquetar los datos en memoria. Por ejemplo, si tenemos que leer un objeto de una estructura y con un alineamiento determinados, deberemos asegurarnos de que nuestro programa almacena esos objetos con la misma estructura y alineamiento.

La directiva #pragma pack() nos permite alterar ese alineamiento a voluntad, indicando como parámetro el valor deseado. Por ejemplo:

#include <iostream>
using namespace std;

#pragma pack(1)

struct A {
   int x;
   char a;
   int y;
   char b;
};

#pragma pack()

struct B {
   int x;
   int y;
   char a;
   char b;
};

int main() {
   cout << "Tamaño de int: "
        << sizeof(int) << endl;
   cout << "Tamaño de char: "
        << sizeof(char) << endl;
   cout << "Tamaño de estructura A: "
        << sizeof(A) << endl;
   cout << "Tamaño de estructura B: "
        << sizeof(B) << endl;

   return 0;
}

La salida, en este caso es:

Tamaño de int: 4
Tamaño de char: 1
Tamaño de estructura A: 10
Tamaño de estructura B: 12

Vemos que ahora la estructura A ocupa exactamente lo mismo que la suma de los tamaños de sus componentes, es decir, 10 bytes.

Esta directiva funciona como un conmutador, y permanece activa hasta que se cambie el alineamiento con otra directiva, o se desactive usando la forma sin parámetros.

Pero existe una alternativa, aparentemente más engorrosa, pero más portable.

Atributos

Se pueden especificar ciertos atributos para todas las variables, ya sea en la declaración de tipos o en la declaración de los objetos.

Para ello se usa la palabra __attribute__, que admite varios parámetros:

  • __aligned__ que permite especificar el número del que tiene que se múltiplo la dirección de memoria.
  • __packed__ que permite especificar el formato empaquetado, es decir, el alineamiento sera de un byte.

Por ejemplo:

struct __attibute__((__packed__)) A {
   int x;
   char a;
   int y;
   char b;
};

Esta forma tiene el mismo efecto que el ejemplo anterior, pero tiene algunas ventajas.

Para empezar, se aplica sólo a la estructura, unión o clase especificada. Además, nos aseguramos de que o bien el compilador tiene en cuenta este atributo, o nos da un mensaje de error. Con la directiva #pragma, si el compilador no la reconoce, la ignora sin indicar un mensaje de error.

El otro atributo, __aligned__ requiere un número entero como parámetro, que es el número del que las direcciones de memoria han de ser múltiplos:

struct __attibute__((__aligned__(4))) A {
   int x;
   char a;
   int y;
   char b;
};

Directiva #warning

Sintaxis:

#warning mensaje_de_aviso

Sirve para enviar mensajes de aviso cuando se compile un fichero fuente C o C++. Esto nos permite detectar situaciones potencialmente peligrosas y avisar al programador de posibles errores.

Este ejemplo está extraído de uno de los ficheros de cabecera del compilador GCC:

#ifdef __DEPRECATED
#warning This file includes at least one deprecated or antiquated header. \
Please consider using one of the 32 headers found in section 17.4.1.2 of the \
C++ standard. Examples include substituting the <X> header for the <X.h> \
header for C++ includes, or <iostream> instead of the deprecated header \
<iostream.h>. To disable this warning use -Wno-deprecated.
#endif

Aprovecho para comentar que una línea en el editor se puede dividir en varias añadiendo el carácter '\' al final de cada una. En el ejemplo anterior, todo el texto entre la segunda y la sexta línea se considera una única línea por el compilador.