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.
Tabla de Contenidos
Antes de empezar
Conviene tener claro:
- Que
#definesustituye 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 quegcc -DNOMBREdefine 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:
| Viaje | Planta | Carga | ¿Planta válida? | ¿Supera 1500? | Resultado |
|---|---|---|---|---|---|
| 1 | 3 | 4 · 75 + 0 = 300 | Sí | No | OK, quedan 1500 − 300 = 1200 |
| 2 | 9 | 2 · 75 + 40 = 190 | No (9 > 8) | No se mira | RECHAZADO (planta) |
| 3 | −1 | 19 · 75 + 0 = 1425 | Sí | No | OK, quedan 75 |
| 4 | 5 | 6 · 75 + 120 = 570 | Sí | No | OK, quedan 930 |
| 5 | −3 | 1 · 75 + 0 = 75 | No (−3 < −2) | No se mira | RECHAZADO (planta) |
| 6 | 0 | 12 · 75 + 650 = 1550 | Sí | 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:
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.huna macroPERSONAS_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 soloconfig.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:
La traza de carga_total vuelta a vuelta para los vecinos:
Vuelta (i) | pesos[i] | total después | Traza con -DDEBUG |
|---|---|---|---|
| 0 | 82 | 82 | total parcial = 82 |
| 1 | 64 | 146 | total parcial = 146 |
| 2 | 91 | 237 | total parcial = 237 |
| 3 | 70 | 307 | total 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
mainque diga el total de las dos cargas juntas y comprueba que solo sale con-DDEBUG. - Cambia el
#ifdef DEBUGpor#if NIVEL >= 1y haz que con-DNIVEL=2salga además una traza por cadacomprobar, 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:
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_CARGAque cambie la línea de personas por «No apto para personas». - Añade un
#if CARGA_MAXIMA > 2000enmainque 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.hlo incluya, sí. - **Efectos dentro de
LOG.** Lo que va dentro de una traza desaparece en la versión normal. - **Confundir
#ifdefy#if.**#ifdef MODELOes 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:
- **Los arrays
personasyextra** aparecen con sus tres valores antes de entrar en el bucle; las macros no aparecen por ningún sitio. - **En cada vuelta, al crearse
carga**, comprueba la cuenta: 4 · 75 = 300, 19 · 75 = 1425 y 12 · 75 + 650 = 1550. - **La línea del
printfde depuración** se ejecuta en las tres vueltas porqueDEBUGestá 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. - 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.hcon 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
LOGcon dos versiones dentro de#ifdef DEBUGda **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,#elifygcc -DMODELO=n, un solo archivo da varios programas, y#errorimpide compilar uno imposible. gcc -E(ygcc -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.ccon y sin-DDEBUGy visto la diferencia? - ¿Sabes explicar por qué
!PLANTA_VALIDA(9)funciona y qué pasaría sin los paréntesis? - ¿Has probado
paneles.ccon 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 →
