Preprocessor Magic en C

Hola,

He estado jugando con C y dando el coñazo a la gente de mi entorno con esto así que voy a hablar un poco “del C sostenido”1, es decir, la magia del preprocesador, todo propiciado por un fantástico hack que he visto en la librería cURL.

No voy a entrar a explicar el hack del que hablo más arriba porque tampoco lo entiendo aunque creo que sé por dónde puede ir. Investigadlo vosotros y me comentáis si queréis. En lugar de eso voy a hablar del preprocesador de C y voy a jugar un poco con alguna macro, sin meternos demasiado en harina. Esta entrada está más orientada a la gente que sabe algo de C y puede pegarse con el lenguaje pero no tiene más que un conocimiento superficial del mismo y no se lleva bien con esas órdenes que empiezan por una almohadilla (o hashtag para los modernos).

Primero, ¿qué son las Directivas del Preprocesador?

Básicamente son las frasecitas del hashtag, como los #include, #define, #ifdef, #ifndef, #pragma or whatever. Sirven para dar información al preprocesador como su propio nombre indica. El preprocesador analiza nuestro código fuente antes de que se realice la compilación así que, en realidad, no formarán parte del binario de nuestro programa de forma explícita, pero sí afectarán a cómo se hace la compilación. Las más usadas solamente facilitan la programación, nada más, pero ahora explico un poco de eso.

Define

Define sirve para definir mierdas (¿En serio? No lo hubiese dicho nunca). Sirve tanto con valores como con macros pero el concepto es el mismo. Sustituye el texto del primer argumento por el texto del segundo dentro de tu programa. Y cuando digo texto, digo texto, literalmente.

Está un poco más trabajado que eso y permite macros con parámetros tal que así:

#define error(a) fprintf(stderr, "ERROR: %s", a)

Este define permitiría que escribiésemos error() con un string dentro en lugar de tener que escribir un fprintf(stderr, "ERROR: %s", ) con ese string. Es decir, sustituirá error por el fprintf pero usando a como parámetro.

Habiendo dicho esto, en el siguiente bloque de código fuente defino dos veces el número pi. ¿Podríais decirme cuál es la diferencia entre la variable a y la b? ¿Y la diferencia entre pi y PI?

La respuesta está debajo del código, no la miréis, no seáis tramposos.

// Define constante PI
#define PI 3.1416
// const es para que el valor sea constante
const float pi = 3.1416;

int main(int argc, char * argv[]){
    float a = PI; // a = 3.1416
    float b = pi; // b = 3.1416
    return 0;
}

Vamos a por las respuestas. Entre a y b no hay ninguna diferencia, ambas valen 3.1416. Sin embargo, el caso de pi y PI es diferente. Tanto PI como pi son invariantes, no puede editarse su valor, pero pi es una variable global y PI “es un define” que, si os fijáis, no podríamos decir siquiera si es un float u otra cosa, porque no lo pone. Tal y como he dicho antes, en todos los lugares del código donde ponga PI pondrá 3.1416 después del preprocesado, pero no pasará lo mismo con pi que es una variable con su pedacito de memoria, que se cargará con ese valor y, después, b será igualada con él. Por eso es importante evitar escribir constantes como pi si lo que realmente queremos algo literal.

Como digo, el código mostrado es completamente equivalente a no definir PI y poner 3.1416 en su lugar.

También es interesante que, si definiésemos el tamaño de un array con un const int size;, como hemos hecho con pi el compilador podría reirse de nosotros y no dejarnos porque por muy const que sean, los arrays no pueden tener longitud de variable en algunos estándares de C. Un define sí que nos sirve, porque no es una variable sino una sustitución literal de texto. Sacaría un error de esta pinta2 si estuviésemos compilando en modo estricto:

warning: ISO C90 forbids variable length array

Por otro lado, si lo que queréis es borrar la definición de algo que hayáis definido antes lo que estáis buscando es #undef.

Include

Include, tan simple como define, sirve para incluir mierdas. Su movida es coger el archivo que le indicamos y a lo brutus meterlo ahí donde aparece el include. Más o menos.

Hay dos formas de usar el include:

#include<archivo>
#include"archivo"
  • La primera de las que muestro utiliza <> para cerrar el nombre del archivo a incluir. Esto sirve para incluir archivos desde los directorios habituales, en linux /usr/include/ entre otros. Esto significa que el sistema automáticamente buscará en esos directorios (llamados include path) el archivo que queremos incluir. Con el compilador GCC, el include path puede ser editado a la hora de compilar añadiendo -I seguido de la ruta al directorio que queremos incluir. Haciendo esto, el directorio añadido sería incluido en el path y sus archivos podrían ser objeto de un #include que use <>.

  • La segunda manera usa "" o comillas dobles. Este include busca el archivo a incluir partiendo del directorio en el que se encuentra el archivo que tiene el include.

Normalmente el primer modo se utiliza para incluir archivos independientes a la solución, archivos aportados por el sistema. Cabeceras de librerías externas, por ejemplo. El segundo modo suele servir para incluir archivos de cabecera del propio proyecto.

Pragma

Pues #pragma ya no es tan evidente, pero no por ello es menos sencillo. Sirve para darle indicaciones al compilador, marcándole unos parámetros. Yo no la he visto muy a menudo, sobre todo la he visto a la hora de programar microcontroladores en C. Os pongo un ejemplo de un Microchip PIC18F45503:

// Microchip PIC PIC18F4550
#pragma config WDT = OFF //Disable watchdog timer

Las directivas pragma no son una parte del lenguaje C en realidad, son unos datos para el compilador y cada compilador tiene las suyas, por eso al programar microcontroladores tenemos algunas especiales.

Si el compilador que usamos no reconoce la directiva pragma la ignora y no avisa, cuidado con eso.

Error

Error sirve para marcar un error y abortar la compilación. Si en algún momento el preprocesador llega hasta un #error la compilación se abortará con el mensaje que hayamos puesto. Ejemplo cortito y al pie:

#error Pues parece que algo ha ido mal

Como he dicho antes, estas cosas ocurren al preprocesar, así que no puedo hacer esto:

// WTF, this is wrong
if (codigo_error != 0){
#error Sal con error
}

No mezclemos preprocesado con código C por favor.

Ifdef, ifndef, y cambios de flujo

Claro, si habéis visto que se puede abortar la compilación, os habréis quedado esperando que haya una manera de que el preprocesador esquive esos #error para que no aborte. Pues la hay. Las directivas que os muestro sirven para incluir o quitar código de forma condicional. Si la condicional no se cumple, el efecto del bloque no va a verse ni compilarse ni nada, ese texto, ese código, no va a existir para el compilador.

Os pongo la lista de convocados para este bloque:

#ifdef, #ifndef, #if, #endif, #else y #elif

Y un ejemplo para que veáis como irían con los #error:

#ifndef _cplusplus
#error C++ es obligatorio
#endif

En este ejemplo se abortaría la compilación si no se hubiese definido la constante _cplusplus que viene definida en todos los compiladores de C++. Es decir, sólo permitiría compilar este código fuente si el compilador es de C++.

Y este sería un ejemplo bestia quitando código fuente del medio, basándose en lo mismo:

#ifdef __cplusplus
extern "C" {
#endif

... CÓDIGO C AQUÍ...

#ifdef __cplusplus
}
#endif

Haciendo esto tendíamos un programa en C, que podría ser compilado en C++ porque se añadiría extern "C"{} alrededor de él si el compilador fuera de C++. Si fuese compilado en un compilador de C, éste ni se enteraría del extern.

Line

De esto no os voy a hablar mucho porque la he encontrado mientras preparaba la entrada4 pero nunca lo había visto. Sirve para cambiar el mensaje de error en caso de que algo esté mal en el código. Cambiaría nuestro número de línea y/o el nombre de archivo. Cuando encuentra la directiva #line cuenta el número de línea desde el número asignado en adelante. Se usa así:

#line NUMERO_LINEA "Nombre de archivo"

 

Y punto. Por mi parte he terminado, esto es todo lo que sé del tema. Si tenéis preguntas, para eso está la sección de comentarios más abajo. Os dejo, como siempre, un pequeño listado de links interesantes:

Creo que con esto tenéis investigación para rato, ¿no?

Pues nada, os dejo con ello. Espero que hayáis aprendido algo nuevo, porque yo he aprendido bastante preparando esta entrada y he afianzado conocimientos.

¡Quitadle el miedo a la parte sostenida de C1!

Un abrazo.


  1. En música, el símbolo del “sostenido” se marca con una almohadilla, como las directivas del preprocesador de las que hablo. 
  2. C90 es el estándar de C de 1990. Se definen con los dos últimos dígitos del año, así que C16 sería el de este año 2016 y C89 sería el de 1989. No por ser más grande tiene que ser mejor, que hace overflow. 
  3. Ahí os dejo un superdocumento de las configuraciones de los Microcontroladores PIC para que veáis que directivas pragma permiten:
    http://ww1.microchip.com/downloads/en/devicedoc/C18_Config_Settings_51537f.pdf&#160;
  4. Sí amigos, a veces me preparo las entradas. No siempre uso el teorema del Mono infinito
Anuncios

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s