Algoritmos y Estructuras de Datos Herramientas Lenguaje de programación
!Prog C/C++ Rust
Linux Matemáticas
Mates Discretas
Programación Orientada a Objetos Sistemas Operativos

Preprocesador

[date: 27-06-2023 01:03] [last modification: 08-01-2024 21:48]
[words: 1635] [reading time: 8min] [size: 53552 bytes]

Cómo usar las directivas del preprocesador en C/C++, aquellas que comienzan por un #.

El preprocesador

El preprocesador es la primera fase de compilación del código C/C++, como ya se comentó en la introducción.

Este simplemente recibe un fichero de entrada y lo transforma aplicando las directivas que va encontrando. Estas directivas permiten incluir código de otros ficheros (habitualmente archivos de cabecera), substituir unos textos por otros (esto son las macros) y elegir si se incluye o no cierto código (esto es la compilación condicional).

Es decir
El preprocesador simplemente elimina, copia y pega código.

El preprocesador solo entiende las directivas de preprocesado, las que empiezan por #, y es completamente independiente del resto de la sintaxis de C o de C++. El estándar de C/C++ define las siguientes directivas:

#define

1#define NOMBRE VALOR
Nota
Es tradicional y recomendable definir las macros utilizando letras mayúsculas y separando palabras por guiones bajos.

Sirve para declarar una macro: el texto será substituido por otro por el preprocesador. Por ejemplo, el siguiente fichero:

1#define MIMACRO 17
2
3int main() {
4    return MIMACRO;
5}

Al ser traducido por el preprocesador se convertirá en:

1int main() {
2    return 17;
3}

Esta directiva también permite el uso de parámetros, por ejemplo:

 1#define SALUDO(nombre)               \
 2        printf("Hola %s\n", nombre); \
 3        printf("Bueno adios\n");     \
 4
 5#include<stdio.h>
 6
 7int main() {
 8    SALUDO("Magno");
 9    return 0;
10}

Si tiene varias líneas, recuerde añadir una barra invertida (\) antes de cada una de ellas.

Las macros tienen diferentes problemas, considera este código de ejemplo:

1if (alguna_condición)
2    SALUDO("Lucas");

El código anterior se traduce como:

1if (alguna_condición)
2    printf("Hola %s\n", "Lucas");
3printf("Bueno adios\n");

Con lo que a "Lucas" se le saluda solo si se cumple la condición pero se despide siempre. Esto no es lo que quería el autor, probablemente. Fíjese que aunque la última línea esté identada esta no pertenece al if, que solo sobre la primera línea, así que recuerde usar llaves en estos casos.

Aunque existe una solución por si sucede eso, crear un bloque:

1#define SALUDO(nombre) {                 \
2            printf("Hola %s\n", nombre); \
3            printf("Bueno adios\n");     \
4        }                                \

Otro problema es el siguiente:

1#define MIMACRO 17
2
3int main() {
4    int MIMACRO = 33;
5    return 0;
6}

No podemos utilizar MIMACRO como nombre de variable porque el preprocesador lo substituye por 17. Y 17 no es un nombre de variable válido.

Usar macros como si fuesen funciones puede ser tentador puesto que el código se inserta directamente en el resultado final en vez de tener que hacer llamadas a subrutinas y retornos. El resultado es un programa más rápido a costa de que es mucho más grande. Pero no es recomendable. Perdemos (entre otras cosas) la seguridad de tipos (type safety).

Además, con los compiladores tan optimizados actuales no es necesario, son capaces de optimizar funciones de la misma forma (inline).

#undef

La definición de una macro (definida con #define) dura hasta el final del archivo o hasta que su definición es cancelada con #undef.

#include

Esta directiva recibe como parámetro un nombre de un fichero, generalmente .h o .hpp. El parámetro se puede dar entre comillas dobles " " o entre símbolos de mayor y menor < >.

Esta distinción se hace para diferencias de las cabeceras que el usuario ha creado (comillas) y de las librerías externas añadidas (símbolos de mayor y menor):

1#include <stdio.h>    // librería estándar
2#include <SDL2/SDL.h> // librería externa
3#include "MiJuego.h"  // librería interna

Esta directiva lee ese fichero y lo inserta en el fichero que está procesando, en el punto en que se encontraba la directiva.

Nótese que estos ficheros insertados también son preprocesados y pueden a su vez tener otros #include con lo que pude haber una cascada de código insertado.

#ifdef, #ifndef, #elifdef, #elifndef

Estas cuatro directivas permiten controlar qué código va a ser compilado en función de si las macros utilizadas como parámetros están o no definidas.

Por ejemplo, si pasamos el siguiente código al preprocesador:

 1#include <stdio.h>
 2#define LINUX
 3
 4int main() {
 5#ifdef LINUX
 6    printf("Mi SO es Linux\n");
 7#else
 8    printf("Mi SO no es Linux\n");
 9#endif
10
11#ifndef MAC
12    printf("No uso Macintosh\n");
13#else
14    printf("Sí uso Macintosh\n");
15#endif
16
17    return 0;
18}

Será traducido como:

1int main() {
2    printf("Mi SO es Linux\n");
3    printf("No uso Macintosh\n");
4    return 0;
5}

Es posible anidar estas directivas de forma similar a como se hace con los if de C++.

#if, #else, #elif, #endif

#if actúa como #ifdef pero comparando una macro con un valor. #elif permite encadenar varias comprobaciones seguidas sin tener que anidarlas y necesitando solo un #endif al final. Por ejemplo:

 1#define LINUX    1
 2#define NINTENDO 2
 3#define VR_VIVE  3
 4
 5bool disparoPulsado() {
 6#if SYSTEM == LINUX
 7    return (key[KEY_SPACE]);
 8#elif SYSTEM == NINTENDO
 9    return buttonPressed(KEY_A);
10#elif SYSTEM == VR_VIVE
11    return pushGesture();
12#else
13#   error Sistema desconocido
14#endif
15}

El código anterior sería el de una hipotética función que determina si un jugador ha pulsado el botón de disparo. Debemos definir la macro SYSTEM con un valor según compilemos para Linux, Nintendo o Realidad Virtual con VIVE. Y según el valor de esa macro se llamará a un código u otro.

Esto también lo podríamos haber hecho con un if normal de código de C++. Pero esto tiene un inconveniente, es muy posible que la función buttonPressed de Nintendo no exista en Linux y falle la compilación del sistema para Linux. No tiene sentido que falle la compilación por un código que no se va a usar, para eso usamos estas directivas de procesador.

#error, #warning

Esta directiva provoca que falle la compilación y el compilador emita el mensaje de error que acompaña a esta directiva.

#line

Esta es una macro que posiblemente nunca uses.

1#line 17 nombreFichero

Cuando se sigue de un número, causa que el compilador trate la siguiente línea como si fuese ese número de línea. Esto afecta a los mensajes de aviso o error que emita el compilador y a la información de ayuda a depuradores pero no afecta al código generado.

Si además añades un nombre, el compilador actuará como si el fichero que está procesando tuviese este nombre.

Esta directiva es útil para programas de transformación de código. Por ejemplo, los primeros compiladores de C++ traducían el código C++ a C y después lo compilaban con un compilador normal de C. Así se ahorraban escribir el compilador entero. Pero con frecuencia una línea de C se convertía en muchas de C++, y para que el compilador de C diese los números de línea correctos en los mensajes de error usaban esta directiva.

#pragma

1#pragma parametros

Esta es la directiva de control dependiente de la implementación. Cada compilador puede definir sus propios pragmas con su propio significado.

Un pragma común es #pragma once (ver Guardas de cabecera). Si aparece en la primera línea de un fichero indica que solo se debe incluir una vez. Es una directiva que no existe en todos los sistemas y que no funciona bien con ficheros montados en red, hardlinks o similares.

##

No es una directiva como tal, sino un operador de macro. Toma dos tokens distintos y los pega para formar un solo token. El token resultado puede ser un nombre de variable, de clase o cualquier otro identificador.

Por ejemplo:

1#define DECLARA_Y_ASIGNA(tipo, nombrevar, valor) \
2        tipo nombrevar = valor;                  \
3        tipo original_#nombrevar = valor;        \
4
5DECLARA_Y_ASIGNA(int, area, 2 * 6);

El #define anterior se utilizaría como ayuda a la depuración. Te permite declarar y asignar una variable. Pero declarando siempre otra segunda variable que empieza por original_ y sigue con el mismo nombre y a la que se asigna el mismo valor.

En tu código usarías la variable con el nombre que pusiste en la macro. Y si mientras estás depurando quieres consultar el valor original puedes consultar la otra variable que se ha declarado con la macro.

Guardas de cabecera

Esta es una técnica muy habitual en los archivos de cabecera. Es común que un archivo de cabecera sea utilizado por varios archivos de cabecera y se termine copiando y pegando el mismo archivo. Por ejemplo:

1// Fichero persona.h
2typedef struct { /* ... */ } Persona;
1// Fichero calculos.h
2#include "persona.h"
3int envejecer(Persona p, int tiempo);
1// Fichero impresiones.h
2#include "persona.h"
3void muestraPersona(Persona p);
1// Fichero main.c
2#include "calculos.h"
3#include "impresiones.h"
4
5/* El resto del código */

Al compilar el fichero main.c vamos a tener un error:

1error: redefinition of ‘class Persona’

Esto se debe a que main.c incluye calculos.h e impresiones.h y cada uno de estos dos a su vez incluye a persona.h. Con lo que persona.h ha quedado incluido dos veces y el compilador esto no lo permite.

Esto podemos solucionarlo así en el fichero persona.h :

1#ifndef PERSONA_H
2#define PERSONA_H
3
4// Fichero persona.h
5typedef struct {} Persona;
6
7#endif

La primera vez que se incluya el fichero la macro no estará definida por lo que el #ifndef se evaluará a cierto y todo el código se incluirá. El código incluido a su vez define la macro PERSONA_H. La siguiente vez que se incluya la macro sí está definida por lo que el #ifndef se evalua a falso y no se incluirá el código.

De esta forma nos aseguramos que el código del fichero se incluye una sola vez aunque el archivo se incluya múltiples veces.

Macros predefinidos

Los siguientes macros deberían estar siempre presentes:

__cplusplusDenota la versión del estándar de C++ en uso.
__FILE__Nombre del archivo actual (se puede cambiar con #line)
__LINE__Línea actual del archivo (se puede cambiar con #line)
__DATE__Fecha de la compilación en formato Mmm dd yyyy
__TIME__Hora de la compilación en formato hh:mm:ss

Entre otros.

Anterior: Sintaxis básica Volver a C/C++ Siguiente: Programación Estructurada en C