Menu

Compilación condicional en C: #ifdef, #ifndef, #if y -D

Compila código distinto para builds distintos con #ifdef, #ifndef, #if, #elif y #else: interruptores de depuración, ramas por plataforma, definir macros desde la línea de comandos con -D, y usar #if 0 para desactivar código.

Esta página incluye editores ejecutables: edita, ejecuta y ve el resultado al instante.

La compilación condicional permite que un solo archivo fuente se convierta en varios programas distintos. El preprocesador decide, antes de que corra el compilador, qué bloques de texto sobreviven, así que el código dentro de una rama que no se cumple no solo se salta en tiempo de ejecución: se elimina. Nunca llega a analizarse, así que no aporta nada al ejecutable e incluso puede contener construcciones que el compilador actual rechazaría.

Esa es la diferencia con un if ordinario. Ambos eligen entre caminos; solo uno de ellos elige antes de que el programa exista.

#ifdef e #ifndef

La pregunta más simple es si una macro está definida:

Elimina la línea #define DEBUG y cada impresión de depuración desaparece de la compilación: no desactivada, desaparecida. Ejecútalo una vez tal como está, luego quita esa línea y ejecútalo de nuevo.

#ifndef es la negación: conserva el bloque solo si la macro no está definida. Su uso más común es el guard de inclusión que se cubre en archivos de cabecera, y también es la forma de dar un valor por defecto que quien llama pueda sobrescribir:

Todo #ifdef e #ifndef debe cerrarse con #endif. Anotar el cierre en archivos largos — #endif /* DEBUG */ — ahorra tiempo real más adelante.

#if, #elif, #else

#if toma una expresión entera constante y conserva el bloque cuando es distinta de cero. Eso hace posibles las comparaciones de versión y de nivel:

Cambia LOG_LEVEL a 0 o a 3 y vuelve a ejecutar para ver cómo el programa mismo cambia de forma.

La expresión la evalúa el preprocesador, lo que significa que solo puede usar constantes enteras, operadores aritméticos y de comparación, y macros que se expandan a eso. No puede ver sizeof, valores de enum, variables ni nada que necesite al compilador. Una macro no definida dentro de un #if se evalúa como 0 en lugar de ser un error, lo que resulta conveniente y ocasionalmente sorprendente:

#if FEATURE_X        /* FEATURE_X nunca se define en ningún lado -> 0 -> el bloque se descarta */

defined(NOMBRE) convierte la pregunta de #ifdef en algo utilizable dentro de un #if, así que puedes combinar pruebas:

#if defined(DEBUG) && !defined(NDEBUG)
    /* build de depuración, y las aserciones están activas */
#endif

#if defined(LINUX) || defined(BSD)
    /* cualquiera de los dos sistemas tipo unix */
#endif

#if defined(X) e #ifdef X significan lo mismo; usa el primero cuando necesites &&, || o !.

Definir macros desde la línea de comandos

El verdadero poder de estos interruptores es que el código fuente no necesita cambiar en absoluto. gcc -D define una macro para toda la compilación:

gcc -DDEBUG programa.c -o programa        # DEBUG definido como 1
gcc -DLOG_LEVEL=3 programa.c -o programa  # valor específico
gcc -DDEBUG -DBUFFER_SIZE=512 a.c b.c -o app

-DNOMBRE por sí solo equivale a #define NOMBRE 1. -U NOMBRE quita una definición, lo que importa cuando una cabecera define algo que quieres apagar.

Así que el arreglo habitual es: el código fuente contiene valores por defecto protegidos con #ifndef, y el comando de construcción selecciona una configuración.

/* config.h */
#ifndef LOG_LEVEL
#define LOG_LEVEL 1        /* silencioso por defecto */
#endif

#ifndef MAX_CONNECTIONS
#define MAX_CONNECTIONS 64
#endif
# build de desarrollo
gcc -DDEBUG -DLOG_LEVEL=3 -Wall -Wextra -g src/*.c -o app-dev

# build de producción
gcc -DNDEBUG -O2 src/*.c -o app

NDEBUG está estandarizado: definirlo desactiva cada assert() del programa, porque <assert.h> está escrito él mismo con compilación condicional. Ese es el patrón en miniatura: una cabecera que compila a cosas distintas según lo que haya definido el build.

Interruptores de plataforma

Los compiladores predefinen macros que identifican el sistema de destino, así que un solo archivo fuente puede llamar a la API correcta en cada uno:

#if defined(_WIN32)
    #include <windows.h>
    #define CLEAR_SCREEN "cls"
#elif defined(__APPLE__)
    #include <unistd.h>
    #define CLEAR_SCREEN "clear"
#elif defined(__linux__)
    #include <unistd.h>
    #define CLEAR_SCREEN "clear"
#else
    #error "Plataforma no soportada"
#endif

Las comunes: _WIN32 (definida tanto en Windows de 32 como de 64 bits), _WIN64, __linux__, __APPLE__, __unix__, __ANDROID__. La identidad del compilador tiene su propio conjunto — __GNUC__, __clang__, _MSC_VER — y la arquitectura también — __x86_64__, __aarch64__.

Una versión ejecutable que reporta sobre qué fue construido:

#error vale la pena conocerlo por sí solo: detiene la compilación con tu mensaje. Terminar una cadena de plataformas con #else / #error "Plataforma no soportada" convierte una compilación mal hecha y silenciosa en un fallo claro al principio del registro de construcción.

Para listar cada macro que tu compilador predefine:

gcc -dM -E - < /dev/null

Esa salida es la respuesta autoritativa a "qué puedo probar en esta máquina".

#if 0 para desactivar código

Comentar un bloque con /* ... */ falla en el momento en que el bloque contiene un comentario propio, porque los comentarios de C no se anidan: el primer */ dentro termina el comentario externo y todo lo que sigue se convierte en código suelto. #if 0 no tiene ese problema:

#if 0
    /* Toda esta región se elimina, comentarios incluidos. */
    legacy_init();
    int n = old_calculation(42);  /* incluso este comentario está bien */
    report(n);
#endif

Cámbialo a #if 1 para traer el código de vuelta. Los editores lo siguen resaltando como C, y puedes anidar un #if 0 dentro de otro #if.

Es una herramienta de depuración, no un sistema de almacenamiento. El código que vive en un #if 0 nunca se compila, así que se pudre en silencio: no va a compilar para cuando alguien mueva el interruptor. Úsalo mientras acotas un problema, luego borra el bloque y deja que el control de versiones lo recuerde.

Dónde sale mal la compilación condicional

Tres modos de fallo explican la mayor parte del dolor.

El código que nunca se compila nunca se verifica. Un error de tipeo dentro de una rama #ifdef inactiva es invisible hasta que alguien construye esa configuración: posiblemente meses después, posiblemente en CI sobre una plataforma que no tienes. Si un proyecto tiene ramas de plataforma interesantes, constrúyelas todas con regularidad.

Entrelazar #ifdef con el flujo de control se vuelve ilegible rápido. Este tipo de cosa es difícil de razonar:

if (ready) {
#ifdef FAST_PATH
    fast_send(buf);
} else {
#endif
    slow_send(buf);
}

Llaves abiertas en una rama y cerradas en otra son legales y horribles. Prefiere condicionales que envuelvan funciones completas, y selecciona entre ellas:

#ifdef FAST_PATH
static void send_data(const char *buf) { fast_send(buf); }
#else
static void send_data(const char *buf) { slow_send(buf); }
#endif

Un if en tiempo de ejecución suele ser mejor. Si ambas ramas compilarían en todas partes, un if (debug_enabled) ordinario mantiene los dos caminos con verificación de tipos, testeables y conmutables sin recompilar. Reserva el preprocesador para lo que solo él puede hacer: código que genuinamente no puede compilar en la otra plataforma.

Para el patrón que combina todo esto — una cabecera de valores por defecto, un -D por compilación y ramas de plataforma envueltas en guards de inclusión — mira archivos de cabecera; para #define en sí, macros.

Preguntas frecuentes

¿Qué es la compilación condicional en C?

Es usar directivas del preprocesador para conservar o descartar bloques de código fuente antes de que corra el compilador. El código dentro de un #ifdef que no se cumple se elimina del texto por completo: nunca se analiza, nunca se compila y nunca aparece en el ejecutable. Así es como un solo archivo fuente soporta varias plataformas o tipos de compilación.

¿Cuál es la diferencia entre #ifdef y #if?

#ifdef NOMBRE solo pregunta si la macro existe, sin importar su valor. #if expresión evalúa una expresión entera constante, así que puede comparar valores: #if VERSION >= 3. Usa #ifdef para banderas que simplemente están o no están, y #if cuando el valor importa. #if defined(NOMBRE) combina ambos y se puede unir con && y ||.

¿Cómo defino una macro desde la línea de comandos con gcc?

Usa -D: gcc -DDEBUG programa.c define DEBUG como si el archivo empezara con #define DEBUG 1, y -DMAX=50 le da un valor específico. Así es como los sistemas de construcción activan funcionalidades sin editar el código fuente, y -U NOMBRE quita una definición.

¿Por qué usar #if 0 en lugar de comentar el código?

Porque los comentarios /* ... */ no se anidan: el primer */ dentro de la región termina el comentario antes de tiempo y el resto se convierte en código roto. #if 0 ... #endif elimina cualquier cantidad de código que contenga cualquier cantidad de comentarios, mantiene vivo el resaltado de sintaxis, y se revierte trivialmente cambiándolo a #if 1.

Coddy programming languages illustration

Aprende a programar con Coddy

COMENZAR