El preprocesador en C — práctica con el panel de un ascensor, un registro de depuración y tres modelos en un solo archivo (y la entrega que llegó llena de trazas)

La primera vez que entregué una práctica de C al corrector automático saqué un cero. El programa hacía bien las cuentas, pero yo había ido sembrando printf("llego aquí, total = %d\n", total); por todas partes para encontrar un fallo, y se me olvidó quitarlos. El corrector comparaba mi salida con la esperada, línea a línea, y mi salida tenía treinta líneas de más. La lección me costó una nota: las trazas de depuración tienen que poder encenderse y apagarse sin tocar el código. Y eso, en C, se hace con el preprocesador.

En esta práctica vas a poner en marcha lo que viste en la teoría del preprocesador con tres programas del mismo ascensor, todos de nivel medio: un panel de configuración con sus constantes y macros en un config.h, un registro de depuración que solo existe cuando compilas con -DDEBUG (el que me habría salvado aquel cero) y tres modelos de ascensor salidos de un único archivo. Los datos se parecen a los de verdad: viajes con plantas que no existen, grupos que se pasan de peso por 50 kg y una mudanza que no cabe.

Antes de empezar

Conviene tener claro:

  • Que #define sustituye texto: no crea variables ni hace cuentas.
  • Que las macros con parámetros llevan paréntesis en cada parámetro y alrededor de todo.
  • La diferencia entre #ifdef (¿existe?) y #if (¿cuánto vale?), y que gcc -DNOMBRE define un símbolo desde fuera.
  • Cómo funciona una guarda de inclusión (#ifndef ARCHIVO_H) en un .h.

Si alguno te suena dudoso, repasa la teoría antes de seguir.

El programa 1 tiene dos archivos, config.h y panel.c. Guárdalos en la misma carpeta y compila solo el .c: gcc -Wall -std=c11 panel.c -o panel. El #include "config.h" con comillas hace que el preprocesador lo busque ahí. Los programas 2 y 3 se compilan varias veces con opciones distintas; te digo el comando en cada salida.

Programa 1: El panel de configuración

El ascensor de un edificio con dos sótanos y ocho plantas recibe una lista de seis viajes: a qué planta va cada uno, cuántas personas suben y cuántos kilos extra llevan (maletas, carros de la compra, un palé…). El programa tiene que decidir si cada viaje es válido. Lo que lo complica es que **las reglas no van en el .c**: todas las constantes y las comprobaciones viven en un config.h, para que cambiar el ascensor de edificio sea editar un solo archivo.

La idea

Primero se escribe el config.h con tres tipos de cosas: constantes (carga máxima, peso medio, plantas mínima y máxima, número de viajes), una constante calculada (PERSONAS_MAX, que sale de dividir la carga entre el peso medio) y macros que preguntan (SUPERA_CARGA, PLANTA_VALIDA) o calculan (PESO_PERSONAS). Después, el panel.c recorre los viajes y, para cada uno, calcula la carga y comprueba primero la planta y luego el peso: si la planta no existe, da igual lo que pese.

Antes de programar, piensa en cómo se usará cada macro. PLANTA_VALIDA va a ir detrás de un !, y PLANTA_MIN es un número negativo. Las dos cosas te obligan a poner paréntesis donde parece que sobran.

El código

Primero, la configuración:

/* config.h: la configuración del ascensor en un solo sitio */
#ifndef CONFIG_H
#define CONFIG_H

#define CARGA_MAXIMA 1500        /* kg */
#define PESO_MEDIO 75            /* kg por persona */
#define PLANTA_MIN (-2)          /* entre paréntesis: es un número negativo */
#define PLANTA_MAX 8
#define NUM_VIAJES 6

/* Constante calculada: el preprocesador copia la cuenta, la hace el compilador */
#define PERSONAS_MAX (CARGA_MAXIMA / PESO_MEDIO)

/* Macros con parámetros: paréntesis en cada parámetro y alrededor de todo */
#define PESO_PERSONAS(n) ((n) * PESO_MEDIO)
#define SUPERA_CARGA(kg) ((kg) > CARGA_MAXIMA)
#define PLANTA_VALIDA(p) ((p) >= PLANTA_MIN && (p) <= PLANTA_MAX)

#endif  /* CONFIG_H */

Y el programa que la usa:

#include <stdio.h>
#include "config.h"   /* todas las constantes y macros salen de aquí */

int main(void) {
    /* Cada viaje: planta de destino, personas y kg extra (maletas, carros...) */
    int destino[NUM_VIAJES]  = {3, 9, -1, 5, -3, 0};
    int personas[NUM_VIAJES] = {4, 2, 19, 6, 1, 12};
    int extra[NUM_VIAJES]    = {0, 40, 0, 120, 0, 650};
    int aceptados = 0;

    printf("Capacidad: %d kg (%d personas)\n", CARGA_MAXIMA, PERSONAS_MAX);
    printf("Plantas de %d a %d\n\n", PLANTA_MIN, PLANTA_MAX);

    for (int i = 0; i < NUM_VIAJES; i++) {
        int carga = PESO_PERSONAS(personas[i]) + extra[i];
        printf("Viaje %d -> planta %2d, %2d pers., %4d kg: ",
               i + 1, destino[i], personas[i], carga);

        /* El ! funciona porque PLANTA_VALIDA lleva paréntesis exteriores */
        if (!PLANTA_VALIDA(destino[i])) {
            printf("RECHAZADO (planta fuera de rango)\n");
        } else if (SUPERA_CARGA(carga)) {
            printf("RECHAZADO (sobran %d kg)\n", carga - CARGA_MAXIMA);
        } else {
            printf("OK (quedan %d kg)\n", CARGA_MAXIMA - carga);
            aceptados++;
        }
    }
    printf("\nAceptados: %d de %d\n", aceptados, NUM_VIAJES);
    return 0;
}

Salida

Con gcc -Wall -std=c11 panel.c -o panel y ./panel:

Capacidad: 1500 kg (20 personas)
Plantas de -2 a 8

Viaje 1 -> planta  3,  4 pers.,  300 kg: OK (quedan 1200 kg)
Viaje 2 -> planta  9,  2 pers.,  190 kg: RECHAZADO (planta fuera de rango)
Viaje 3 -> planta -1, 19 pers., 1425 kg: OK (quedan 75 kg)
Viaje 4 -> planta  5,  6 pers.,  570 kg: OK (quedan 930 kg)
Viaje 5 -> planta -3,  1 pers.,   75 kg: RECHAZADO (planta fuera de rango)
Viaje 6 -> planta  0, 12 pers., 1550 kg: RECHAZADO (sobran 50 kg)

Aceptados: 3 de 6

Comprobemos las cuentas a mano. PERSONAS_MAX es 1500 / 75 = 20 personas. Viaje a viaje:

ViajePlantaCarga¿Planta válida?¿Supera 1500?Resultado
134 · 75 + 0 = 300SíNoOK, quedan 1500 − 300 = 1200
292 · 75 + 40 = 190No (9 > 8)No se miraRECHAZADO (planta)
3−119 · 75 + 0 = 1425SíNoOK, quedan 75
456 · 75 + 120 = 570SíNoOK, quedan 930
5−31 · 75 + 0 = 75No (−3 < −2)No se miraRECHAZADO (planta)
6012 · 75 + 650 = 1550SíSíRECHAZADO, sobran 50

Tres aceptados de seis. El viaje 3 es el más ajustado: 19 personas caben por 75 kg, justo el peso de una más. El dibujo pone las seis cargas frente a la línea de los 1500 kg:

Los seis viajes del programa 1 Barras con la carga de cada viaje frente a la línea de 1500 kg: V1, V3 y V4 son correctos; V2 (planta 9) y V5 (planta -3) se rechazan por la planta; V6, con 1550 kg, por sobrecarga. Los seis viajes del panel (CARGA_MAXIMA = 1500 kg) V1 planta 3 300 kg V2 planta 9 190 kg V3 planta -1 1425 kg V4 planta 5 570 kg V5 planta -3 75 kg V6 planta 0 1550 kg 1500 OK planta fuera de -2..8 sobrecarga
Fíjate en V2: pesa poquísimo, pero se rechaza antes por la planta. El orden de los if decide el motivo.

El viaje 2 es el que demuestra que el orden de los if importa: pesa poquísimo, pero se rechaza por la planta antes de que el programa llegue a mirar el peso.

Ahora, el zoom a la línea que más trabajo esconde, la comprobación de la planta:

/* El ! funciona porque PLANTA_VALIDA lleva paréntesis exteriores */
if (!PLANTA_VALIDA(destino[i])) {
    printf("RECHAZADO (planta fuera de rango)\n");

Después del preprocesador, !PLANTA_VALIDA(destino[i]) se convierte en:

if (!((destino[i]) >= (-2) && (destino[i]) <= 8)) {

Los paréntesis exteriores de la macro hacen que el ! niegue toda la condición. Sin ellos, el ! se pegaría solo a destino[i] (lo verás estallar en los ejercicios). Y (-2) va entre paréntesis porque -2 también es una expresión (el operador menos aplicado al 2): la costumbre de poner entre paréntesis todo #define que no sea un número suelto te ahorra tener que pensar en prioridades cada vez que lo uses.

Preguntas que te puedes hacer

**¿Por qué PERSONAS_MAX lleva paréntesis si no tiene parámetros?** Porque es una cuenta. CARGA_MAXIMA / PESO_MEDIO funciona sola, pero si alguien escribe 100 % PERSONAS_MAX sin paréntesis quedaría 100 % 1500 / 75, que C calcula como (100 % 1500) / 75 = 1, en vez de 100 % 20 = 0. Cualquier #define que sea una expresión, entre paréntesis.

¿Quién hace la división 1500 / 75, el preprocesador o el programa? Ninguno de los dos en el sentido que imaginas: el preprocesador solo copia (1500 / 75) en el código, y el compilador, al ver dos constantes, hace la cuenta al compilar. El programa ya lleva el 20 escrito.

**¿Por qué los arrays se declaran con NUM_VIAJES y el bucle también?** Para que el tamaño esté en un solo sitio. Si mañana hay ocho viajes, cambias NUM_VIAJES en config.h y el array y el bucle crecen a la vez. Esto con una const int no se puede hacer en un array así en C.

**¿Qué pasa si incluyo config.h dos veces?** Nada: la guarda #ifndef CONFIG_H hace que la segunda inclusión se salte entera. Pruébalo añadiendo otra línea #include "config.h" y verás que compila igual.

**¿Por qué SUPERA_CARGA usa > y no >=?** Porque 1500 kg exactos es la carga máxima permitida, no un exceso. Con >=, un grupo de 20 personas (justo 1500) se rechazaría.

Escribir #define PLANTA_VALIDA(p) p >= PLANTA_MIN && p <= PLANTA_MAX, sin los paréntesis. Con !PLANTA_VALIDA(destino[i]) el ! se aplica solo al primer destino[i], y el viaje a la planta 9 pasaría como válido.

Si quieres ver el if tal y como lo recibe el compilador, gcc -E -P panel.c | grep "if (!" te lo enseña sin las 800 líneas de stdio.h.

Pruébalo tú

  • Añade a config.h una macro PERSONAS_LIBRES(kg) que diga cuántas personas más caben con esa carga, y úsala en la línea del OK («quedan 1200 kg, caben 16 personas más»).
  • Cambia el edificio: sin sótanos (PLANTA_MIN 0) y con 12 plantas. ¿Qué viajes cambian de resultado? Hazlo tocando solo config.h.

Programa 2: El registro de depuración

Ahora el ascensor tiene que comprobar dos cargas: un grupo de vecinos y una mudanza con palés. El programa suma los pesos y dice si pueden subir. Lo que lo complica es que queremos trazas de depuración para ver la suma paso a paso, pero que desaparezcan solas en la versión que se entrega. Exactamente lo que me faltó a mí.

La idea

Se define una macro LOG(msg, valor) con dos versiones dentro de un #ifdef DEBUG: si compilas con -DDEBUG, LOG es un printf que dice archivo, línea, función y el valor; si no, LOG se convierte en ((void)0), una expresión que no hace nada. El resto del código usa LOG con normalidad y nunca cambia: lo que cambia es el comando de compilación.

Antes de programar, piensa en qué tiene que valer LOG(...) cuando está apagado. Si lo dejas vacío (#define LOG(msg, valor)), la línea queda como ; suelto, que compila, pero ((void)0) es más seguro: sigue siendo una expresión y funciona en cualquier sitio donde iría un printf.

El código

#include <stdio.h>

#define CARGA_MAXIMA 1500

#ifdef DEBUG
/* Con -DDEBUG: cada traza dice archivo, línea, función y el valor */
#define LOG(msg, valor) \
    printf("  [%s:%d %s] %s = %d\n", __FILE__, __LINE__, __func__, msg, valor)
#else
/* Sin DEBUG: LOG se convierte en una expresión que no hace nada */
#define LOG(msg, valor) ((void)0)
#endif

int carga_total(const int pesos[], int n) {
    int total = 0;
    for (int i = 0; i < n; i++) {
        total += pesos[i];
        LOG("total parcial", total);   /* una traza por persona o palé */
    }
    return total;
}

void comprobar(const char *grupo, const int pesos[], int n) {
    LOG("elementos", n);
    int total = carga_total(pesos, n);
    printf("%s: %d kg -> %s\n", grupo, total,
           total > CARGA_MAXIMA ? "SOBRECARGA" : "puede subir");
}

int main(void) {
    int vecinos[] = {82, 64, 91, 70};
    int mudanza[] = {650, 420, 480};
    /* sizeof del array entre sizeof de un elemento = número de elementos */
    comprobar("Vecinos", vecinos, sizeof vecinos / sizeof vecinos[0]);
    comprobar("Mudanza", mudanza, sizeof mudanza / sizeof mudanza[0]);
    return 0;
}

Salida

Compilado normal, gcc -Wall -std=c11 registro.c -o registro:

Vecinos: 307 kg -> puede subir
Mudanza: 1550 kg -> SOBRECARGA

Compilado con trazas, gcc -Wall -std=c11 -DDEBUG registro.c -o registro:

  [registro.c:24 comprobar] elementos = 4
  [registro.c:18 carga_total] total parcial = 82
  [registro.c:18 carga_total] total parcial = 146
  [registro.c:18 carga_total] total parcial = 237
  [registro.c:18 carga_total] total parcial = 307
Vecinos: 307 kg -> puede subir
  [registro.c:24 comprobar] elementos = 3
  [registro.c:18 carga_total] total parcial = 650
  [registro.c:18 carga_total] total parcial = 1070
  [registro.c:18 carga_total] total parcial = 1550
Mudanza: 1550 kg -> SOBRECARGA

Mismo archivo, dos programas. La versión normal es la que entregarías: dos líneas, ni una más. La versión con -DDEBUG enseña cada paso de la suma. Comprobamos: 82 + 64 = 146, + 91 = 237, + 70 = 307 kg, que no pasa de 1500. La mudanza: 650 + 420 = 1070, + 480 = 1550 kg, que se pasa por 50. El dibujo resume qué hace -DDEBUG con el código:

LOG con y sin DEBUG El mismo registro.c compilado sin flags convierte LOG en ((void)0) y solo imprime el resultado; compilado con -DDEBUG convierte LOG en un printf que imprime archivo, línea, función y valor. Un solo registro.c, dos programas distintos registro.c #ifdef DEBUG #define LOG(msg, valor) printf(…) #else #define LOG(msg, valor) ((void)0) #endif total += pesos[i]; LOG(«total parcial», total); gcc registro.c LOG(…) → ((void)0) Vecinos: 307 kg -> puede subir gcc -DDEBUG registro.c LOG(…) → printf(…) [registro.c:18 carga_total] total parcial = 82
-DDEBUG no cambia ni una letra del archivo: cambia qué rama del #ifdef llega al compilador.

La traza de carga_total vuelta a vuelta para los vecinos:

Vuelta (i)pesos[i]total despuésTraza con -DDEBUG
08282total parcial = 82
164146total parcial = 146
291237total parcial = 237
370307total parcial = 307

Zoom a la parte que hace la magia, la macro con sus dos versiones:

#ifdef DEBUG
/* Con -DDEBUG: cada traza dice archivo, línea, función y el valor */
#define LOG(msg, valor) \
    printf("  [%s:%d %s] %s = %d\n", __FILE__, __LINE__, __func__, msg, valor)
#else
/* Sin DEBUG: LOG se convierte en una expresión que no hace nada */
#define LOG(msg, valor) ((void)0)
#endif

Fíjate en tres detalles. El \ al final de la línea 7 dice al preprocesador que la definición sigue en la línea siguiente (una directiva ocupa una sola línea lógica). __LINE__ sale como 18 y 24 en las trazas, que son las líneas donde se usa LOG, no la línea 8 donde se define. Y __func__ dice en qué función estamos, así que en la traza se ve que elementos sale de comprobar y los totales salen de carga_total.

Preguntas que te puedes hacer

**¿Por qué no uso un if (debug) normal con una variable?** Porque las trazas seguirían dentro del programa: ocupan sitio, gastan tiempo comprobando el if y basta un cambio de la variable para que salgan. Con #ifdef, en la versión normal los printf de las trazas ni existen: el compilador no los llega a ver.

**¿Para qué sirve el (void) de ((void)0)?** Para dejar claro que esa expresión no devuelve nada útil y que no salte ningún aviso de «valor sin usar». Es la forma estándar de escribir «aquí no hay nada» donde C espera una expresión.

**¿Por qué el número de elementos se calcula con sizeof?** sizeof vecinos es el tamaño del array en bytes (4 enteros · 4 bytes = 16) y sizeof vecinos[0] el de un elemento (4): 16 / 4 = 4. Así, si añades un vecino al array, el número se actualiza solo. Ojo: solo funciona con el array original, no con un puntero dentro de una función.

¿Puedo tener varios niveles de traza? Sí: en vez de #ifdef DEBUG, #if NIVEL >= 2 y compilas con -DNIVEL=2. Es lo que hace el programa 3 con los modelos.

Poner un efecto dentro del LOG, por ejemplo LOG("siguiente", i++). En la versión normal, LOG desaparece y el i++ con él: el programa se comporta distinto con y sin -DDEBUG. Dentro de una traza, solo cosas que se puedan leer sin cambiar nada.

Muchos proyectos reales usan la convención contraria: NDEBUG («no debug») para apagar comprobaciones. La macro assert de <assert.h>, por ejemplo, desaparece si compilas con -DNDEBUG.

Pruébalo tú

  • Añade una traza en main que diga el total de las dos cargas juntas y comprueba que solo sale con -DDEBUG.
  • Cambia el #ifdef DEBUG por #if NIVEL >= 1 y haz que con -DNIVEL=2 salga además una traza por cada comprobar, con el nombre del grupo.

Programa 3: Tres modelos en un solo archivo

La empresa del ascensor fabrica tres modelos: uno básico de cuatro plantas, uno residencial de diez con pantalla y uno de hospital de doce, con pantalla y un modo camilla que deja las puertas abiertas más tiempo. El programa enseña la ficha del modelo. Lo que lo complica es que no queremos tres archivos casi iguales: **un solo paneles.c tiene que dar los tres programas**, y si alguien pide un modelo que no existe, no debe compilar.

La idea

El número de modelo llega desde fuera con -DMODELO=2. Un #ifndef MODELO pone el 1 por defecto. Después, una cadena #if MODELO == 1 … #elif MODELO == 2 … #elif MODELO == 3 define las constantes de cada modelo y, solo en los que la tienen, los símbolos sin valor CON_PANTALLA y CON_CAMILLA. Un #else con #error para la compilación si el número es otro. En main, los #ifdef CON_PANTALLA y #ifdef CON_CAMILLA deciden qué líneas existen.

Antes de programar, piensa en qué símbolos necesitan valor (PLANTAS, CARGA_MAXIMA: se meten en cuentas) y cuáles solo tienen que existir (CON_PANTALLA: solo se pregunta por ellos con #ifdef).

El código

#include <stdio.h>

#ifndef MODELO
#define MODELO 1        /* sin -DMODELO=..., se compila el básico */
#endif

#if MODELO == 1
#define NOMBRE_MODELO "Básico"
#define PLANTAS 4
#define CARGA_MAXIMA 630
#elif MODELO == 2
#define NOMBRE_MODELO "Residencial"
#define PLANTAS 10
#define CARGA_MAXIMA 1000
#define CON_PANTALLA
#elif MODELO == 3
#define NOMBRE_MODELO "Hospital"
#define PLANTAS 12
#define CARGA_MAXIMA 2500
#define CON_PANTALLA
#define CON_CAMILLA
#else
#error "MODELO tiene que ser 1, 2 o 3"
#endif

#define PERSONAS_MAX (CARGA_MAXIMA / 75)   /* división entera: se redondea hacia abajo */

int main(void) {
    printf("Modelo %d (%s)\n", MODELO, NOMBRE_MODELO);
    printf("Plantas: 0-%d | Carga: %d kg | Personas: %d\n",
           PLANTAS - 1, CARGA_MAXIMA, PERSONAS_MAX);

#ifdef CON_PANTALLA
    printf("Pantalla: muestra la planta y la carga\n");
#else
    printf("Sin pantalla: solo botones\n");
#endif

#ifdef CON_CAMILLA
    printf("Modo camilla: puertas abiertas 20 s\n");
#endif
    return 0;
}

Salida

Sin -D, gcc -Wall -std=c11 paneles.c -o paneles (modelo 1 por defecto):

Modelo 1 (Básico)
Plantas: 0-3 | Carga: 630 kg | Personas: 8
Sin pantalla: solo botones

Con gcc -Wall -std=c11 -DMODELO=2 paneles.c -o paneles:

Modelo 2 (Residencial)
Plantas: 0-9 | Carga: 1000 kg | Personas: 13
Pantalla: muestra la planta y la carga

Con -DMODELO=3:

Modelo 3 (Hospital)
Plantas: 0-11 | Carga: 2500 kg | Personas: 33
Pantalla: muestra la planta y la carga
Modo camilla: puertas abiertas 20 s

Y con -DMODELO=4, que no existe, ni siquiera se genera el programa. Esto es lo que dice gcc (sin el número de columna):

paneles.c:23: error: #error "MODELO tiene que ser 1, 2 o 3"
paneles.c: In function 'main':
paneles.c:29: error: 'NOMBRE_MODELO' undeclared (first use in this function)
paneles.c:29: note: each undeclared identifier is reported only once for each function it appears in
paneles.c:31: error: 'PLANTAS' undeclared (first use in this function)
paneles.c:31: error: 'CARGA_MAXIMA' undeclared (first use in this function)

Comprobamos las fichas: el básico tiene 4 plantas, numeradas de 0 a 3 (por eso PLANTAS - 1), y 630 / 75 = 8,4 → 8 personas, porque la división entera corta los decimales. El residencial: 1000 / 75 = 13,3 → 13. El de hospital: 2500 / 75 = 33,3 → 33. El dibujo pone los tres programas que salen del mismo archivo:

Tres modelos con el mismo código El mismo paneles.c compilado con -DMODELO=1, 2 o 3 da tres ascensores distintos: básico de 4 plantas y 630 kg, residencial de 10 plantas y 1000 kg con pantalla, y hospital de 12 plantas y 2500 kg con pantalla y camilla. paneles.c: un archivo, tres ascensores paneles.c (un solo archivo) -DMODELO=1 (o nada) Básico Plantas 0-3 630 kg · 8 pers. sin pantalla -DMODELO=2 Residencial Plantas 0-9 1000 kg · 13 pers. pantalla -DMODELO=3 Hospital Plantas 0-11 2500 kg · 33 pers. pantalla + camilla -DMODELO=4 → #error «MODELO tiene que ser 1, 2 o 3» y no se genera programa
#if MODELO == 2 compara el valor que le das con -D; las funciones extra solo existen en el modelo que las define.

En el error del modelo 4, la primera línea es nuestro #error (línea 23) y el resto son consecuencias: como ninguna rama definió NOMBRE_MODELO, PLANTAS ni CARGA_MAXIMA, el compilador encuentra esos nombres sin definir en el main. Cuando veas una cascada de errores así, lee siempre el primero.

Zoom a la cadena de modelos, que es la parte nueva:

#if MODELO == 1
#define NOMBRE_MODELO "Básico"
#define PLANTAS 4
#define CARGA_MAXIMA 630
#elif MODELO == 2
#define NOMBRE_MODELO "Residencial"
#define PLANTAS 10
#define CARGA_MAXIMA 1000
#define CON_PANTALLA
#elif MODELO == 3
#define NOMBRE_MODELO "Hospital"
#define PLANTAS 12
#define CARGA_MAXIMA 2500
#define CON_PANTALLA
#define CON_CAMILLA
#else
#error "MODELO tiene que ser 1, 2 o 3"
#endif

Cada rama define su juego de constantes, y solo una llega al compilador. #elif es el else if del preprocesador: se evalúa solo si las ramas anteriores eran falsas. Y fíjate en que CON_PANTALLA se define en dos ramas: no pasa nada, porque solo una de ellas existe en cada compilación.

Preguntas que te puedes hacer

**¿Por qué #if MODELO == 2 y no #ifdef MODELO?** Porque MODELO siempre existe (si no lo pasas, el #ifndef lo pone a 1). Lo que distingue un modelo de otro es su valor, y eso solo lo mira #if.

**¿Qué hace -DMODELO=2 exactamente?** Lo mismo que escribir #define MODELO 2 en la primera línea del archivo. Sin el =2, -DMODELO lo definiría con valor 1.

**¿Por qué no basta con un if (modelo == 2) en tiempo de ejecución?** Se podría, pero el programa del modelo básico llevaría dentro el código del hospital sin usarlo nunca. Con el preprocesador, cada modelo lleva solo lo suyo, y el #error impide fabricar uno que no existe: el fallo se ve al compilar, no cuando un cliente pulsa un botón.

**¿Por qué el #ifndef MODELO va antes de la cadena?** Porque la cadena compara MODELO con números. Si no estuviera definido, el preprocesador lo trataría como 0 en el #if y acabaríamos en el #error sin saber por qué.

Escribir #if MODELO = 2 con un solo =. En #if, igual que en C, la comparación es ==; con un = el preprocesador da un error.

gcc -dM -E paneles.c | grep CON_ te lista los símbolos CON_… que quedan definidos para un modelo. Con -DMODELO=3 verás los dos.

Pruébalo tú

  • Añade un modelo 4, «Mercancías», de 6 plantas y 4000 kg, con un símbolo SOLO_CARGA que cambie la línea de personas por «No apto para personas».
  • Añade un #if CARGA_MAXIMA > 2000 en main que imprima «Requiere revisión anual reforzada». ¿En qué modelos sale?

Errores típicos en esta práctica

  • **Macros sin paréntesis exteriores detrás de un !.** !PLANTA_VALIDA(p) solo funciona si la macro está entera entre paréntesis.
  • **Olvidar la guarda en config.h.** Hoy no falla porque se incluye una vez; el día que otro .h lo incluya, sí.
  • **Efectos dentro de LOG.** Lo que va dentro de una traza desaparece en la versión normal.
  • **Confundir #ifdef y #if.** #ifdef MODELO es cierto con cualquier modelo; para elegir uno hace falta #if MODELO == n.
  • **Un \ con espacios detrás** en una macro de varias líneas. El \ tiene que ser lo último de la línea, o la definición se corta.
  • Leer el último error de una cascada. Con #error (o cualquier fallo), el que cuenta es el primero.

Míralo paso a paso en Python Tutor

Python Tutor ejecuta el programa línea a línea y dibuja las variables. Elige C en el desplegable. Como allí no puedes compilar con -DDEBUG, el programa lo define en su primera línea:

#include <stdio.h>

#define DEBUG                      /* en Python Tutor no hay -D: lo definimos aquí */
#define CARGA_MAXIMA 1500
#define PESO_PERSONAS(n) ((n) * 75)
#define SUPERA_CARGA(kg) ((kg) > CARGA_MAXIMA)

int main(void) {
    int personas[3] = {4, 19, 12};
    int extra[3] = {0, 0, 650};
    for (int i = 0; i < 3; i++) {
        int carga = PESO_PERSONAS(personas[i]) + extra[i];
#ifdef DEBUG
        printf("viaje %d: %d kg\n", i + 1, carga);
#endif
        if (SUPERA_CARGA(carga))
            printf("  rechazado\n");
    }
    return 0;
}

Su salida:

viaje 1: 300 kg
viaje 2: 1425 kg
viaje 3: 1550 kg
  rechazado

Qué observar paso a paso:

  1. **Los arrays personas y extra** aparecen con sus tres valores antes de entrar en el bucle; las macros no aparecen por ningún sitio.
  2. **En cada vuelta, al crearse carga**, comprueba la cuenta: 4 · 75 = 300, 19 · 75 = 1425 y 12 · 75 + 650 = 1550.
  3. **La línea del printf de depuración** se ejecuta en las tres vueltas porque DEBUG está definido. Borra el #define DEBUG, vuelve a visualizar y verás que esa línea ya no es un paso: el preprocesador la ha quitado.
  4. En la tercera vuelta, SUPERA_CARGA(carga) es cierto (1550 > 1500) y se ejecuta el «rechazado»; en las otras dos, el programa salta directamente a la siguiente vuelta.

Resumen

  • Un config.h con guarda junta todas las constantes y macros del programa: cambiar de edificio es cambiar un archivo.
  • Las macros que se niegan o se combinan necesitan paréntesis exteriores, y los números negativos van entre paréntesis.
  • Una macro LOG con dos versiones dentro de #ifdef DEBUG da **trazas que se encienden con -DDEBUG** sin tocar el código.
  • __FILE__, __LINE__ y __func__ dicen dónde se ha escrito cada traza, y se sustituyen donde se usa la macro.
  • Con #if MODELO == n, #elif y gcc -DMODELO=n, un solo archivo da varios programas, y #error impide compilar uno imposible.
  • gcc -E (y gcc -dM -E) son la forma de comprobar qué ha hecho el preprocesador.

Checklist: ¿lo has practicado?

  • ¿Has ejecutado los tres programas y comprobado las salidas?
  • ¿Has compilado registro.c con y sin -DDEBUG y visto la diferencia?
  • ¿Sabes explicar por qué !PLANTA_VALIDA(9) funciona y qué pasaría sin los paréntesis?
  • ¿Has probado paneles.c con un modelo que no existe y leído el primer error?
  • ¿Has mirado algún programa con gcc -E -P?
  • ¿Has intentado los «Pruébalo tú» sin mirar la teoría?

Si has respondido que sí a casi todo, estás listo para los ejercicios del preprocesador, donde ya no te doy el código hecho. ¡Nos vemos allí!

Siguiente paso: ejerciciosEl preprocesador en C — ejercicios con el aforo de un ascensor, un limitador de plantas y un simulador con trazas (y el DEBUG 0 que no apagaba nada)

¿Te ha servido este artículo? Así puedes apoyar SergioLearns →

Sigue aprendiendo

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *