El preprocesador en C — teoría con un ascensor, #define, macros y #ifdef (y las seis personas que pesaban 154 kg)
En la práctica de IC2 nos pidieron un programita para el panel de un ascensor. Yo, muy orgulloso, escribí una macro para calcular el peso de la gente que entraba: PESO_PERSONAS(n), que multiplicaba por 75 kg. Lo probé con 4 adultos y 2 niños y el programa me dijo esto:
#include <stdio.h>
#define CARGA_MAXIMA 1500 /* kg que aguanta la cabina */
#define PESO_MEDIO 75 /* kg que calculamos por persona */
#define PESO_PERSONAS(n) n * PESO_MEDIO /* ¡sin paréntesis! */
int main(void) {
int adultos = 4, ninos = 2;
int peso = PESO_PERSONAS(adultos + ninos); /* esperaba 6 * 75 = 450 */
printf("Personas: %d\n", adultos + ninos);
printf("Peso estimado: %d kg\n", peso);
printf("Caben todavía: %d kg\n", CARGA_MAXIMA - peso);
return 0;
}
Y la salida:
Personas: 6 Peso estimado: 154 kg Caben todavía: 1346 kg
Seis personas, 154 kg. O mis compañeros eran de papel, o algo no cuadraba. Estuve un rato buscando el fallo en la multiplicación, en los int, en el printf… y no estaba en ninguno de esos sitios. Estaba en una línea que empieza por # y que el compilador no llega a ver nunca. Esa línea la procesa otro programa antes: el preprocesador.
Hoy vas a entender qué hace exactamente ese programa con tu código: #include, #define, las macros con parámetros (y por qué los paréntesis son obligatorios), la compilación condicional con #ifdef y gcc -D, las guardas de los archivos .h y las macros que ya vienen hechas, como __FILE__ y __LINE__. Si vienes del tema anterior, Structs y cadenas en C, ya llevas un buen rato usando #include <stdio.h> sin preguntarte qué hace. Y cuando termines, la práctica del preprocesador te espera con tres programas del mismo ascensor.
Tabla de Contenidos
Qué es el preprocesador
El preprocesador es un programa que forma parte del compilador y que prepara el código fuente antes de traducirlo a binario. No entiende de tipos, ni de funciones, ni de prioridades de operadores: solo trabaja con texto. Mira las líneas que empiezan por # (las directivas), hace lo que le piden y entrega al compilador un archivo nuevo, sin ninguna directiva y sin comentarios.
Fíjate en el dibujo: tu .c entra por la izquierda, el preprocesador hace cuatro cosas y lo que sale por la derecha es lo único que ve el compilador.
¿Y cómo se ve eso que sale por la derecha? gcc tiene una opción para pararse justo después del preprocesador: gcc -E. Si quito el #include <stdio.h> del gancho (para que no se peguen las 800 líneas de stdio.h) y le paso gcc -E -P gancho.c (la -P quita unas marcas de línea que no nos interesan), esto es lo que recibe el compilador:
int main(void) {
int adultos = 4, ninos = 2;
int peso = adultos + ninos * 75;
printf("Personas: %d\n", adultos + ninos);
printf("Peso estimado: %d kg\n", peso);
printf("Caben todavía: %d kg\n", 1500 - peso);
return 0;
}
Ahí está el culpable, a la vista: int peso = adultos + ninos * 75;. Los #define han desaparecido, los comentarios también, CARGA_MAXIMA se ha convertido en 1500 y la macro se ha cambiado por un texto en el que la multiplicación va antes que la suma. 4 + 2 · 75 = 4 + 150 = 154. El compilador hizo bien la cuenta; la que estaba mal era la cuenta que le dimos.
Cuando una macro haga algo raro, no adivines: gcc -E -P archivo.c | tail -20 te enseña el final del código ya preprocesado, que es donde está tu main.
Las directivas más importantes que vas a usar (la tabla que viene en tus apuntes, traducida):
| Directiva | Qué hace |
|---|---|
#include | Pega en ese punto el contenido de otro archivo |
#define | Define un símbolo o una macro que se sustituye por un texto |
#undef | Borra la definición de un símbolo |
#ifdef | Deja pasar un bloque si el símbolo está definido |
#ifndef | Deja pasar un bloque si el símbolo no está definido |
#if, #elif, #else, #endif | Compilación condicional comparando valores |
#error | Para la compilación con un mensaje |
Las directivas parecen parte de C porque las ves en todos los programas, pero en realidad son de un lenguaje aparte que solo entiende el preprocesador. Por eso no llevan punto y coma y ocupan una línea entera.
La directiva #include
#include va seguida del nombre de un archivo, y su efecto es muy literal: cambia esa línea por el contenido entero del archivo. Tiene dos formas:
#include <stdio.h>: con<y>, el archivo se busca en los directorios del sistema. Es para las bibliotecas estándar:<stdio.h>(entrada y salida:printf,scanf,fopen…),<string.h>(cadenas:strcpy,strlen…) o<stdlib.h>(memoria:malloc,free…).#include "ascensor.h": con comillas, se busca primero en la carpeta del programa que estás compilando. Es para tus archivos.
Los archivos que se incluyen suelen llevar la extensión .h (de header, cabecera) y guardan definiciones que necesitan varios .c: constantes, macros, struct… En vez de copiarlas en cada archivo, las escribes una vez en un .h y lo incluyes donde haga falta. Además, antes de pegar el contenido, el preprocesador procesa las directivas que hay dentro, así que un .h puede incluir a su vez a otros (lo vas a ver en las guardas, más abajo).
Los .h no se escriben en el comando de compilación. gcc guardas.c y listo: el preprocesador decide qué archivos usar cuando encuentra cada #include. Si tus cabeceras están en otra carpeta, la opción -I carpeta le dice dónde más buscar.
La directiva #define
#define tiene dos versiones. Si va seguida de una sola palabra, el preprocesador solo anota que ese símbolo existe. Si va seguida de dos cosas, a partir de esa línea cambia cada aparición de la primera por la segunda. Mira las dos en el programa del ascensor:
#include <stdio.h>
#define SISTEMA_ASCENSOR /* versión 1: solo anota que el símbolo existe */
#define CARGA_MAXIMA 1500 /* versión 2: símbolo -> texto que lo sustituye */
#define PLANTAS 10
#define NOMBRE "Ascensor A" /* el texto de sustitución puede ser una cadena */
#define CARGA_AVISO CARGA_MAXIMA * 9 / 10 /* un #define puede usar otro */
int main(void) {
printf("%s: %d plantas, carga máxima %d kg\n", NOMBRE, PLANTAS, CARGA_MAXIMA);
printf("Aviso a partir de %d kg\n", CARGA_AVISO);
#ifdef SISTEMA_ASCENSOR /* solo pregunta si existe, no cuánto vale */
printf("SISTEMA_ASCENSOR está definido\n");
#endif
/* Dentro de una cadena entre comillas NO se sustituye nada */
printf("Esto no cambia: \"CARGA_MAXIMA\"\n");
return 0;
}
La salida:
Ascensor A: 10 plantas, carga máxima 1500 kg Aviso a partir de 1350 kg SISTEMA_ASCENSOR está definido Esto no cambia: "CARGA_MAXIMA"
Vamos por partes:
#define SISTEMA_ASCENSORno tiene valor. No sirve para meterlo en una cuenta, pero sí para preguntar más tarde con#ifdef SISTEMA_ASCENSOR, que es justo lo que hace el programa (y por eso sale la tercera línea).#define CARGA_MAXIMA 1500es la versión con valor: donde pongaCARGA_MAXIMA, el compilador verá1500. Igual conPLANTASy conNOMBRE, que se sustituye por la cadena"Ascensor A"con sus comillas.CARGA_AVISOusaCARGA_MAXIMAdentro. La sustitución se hace en todo el texto, también en lo que sale de otra sustitución, así queCARGA_AVISOacaba siendo1500 * 9 / 10: 13500 / 10 = 1350. Es lo mismo que el ejemplo de tus apuntes con#define UNO 1y#define OTRO_UNO UNO:OTRO_UNOacaba valiendo 1.- La última línea demuestra la excepción: dentro de una cadena entre comillas no se sustituye nada.
"CARGA_MAXIMA"sale tal cual.
Tres convenciones que verás siempre (no las comprueba nadie, pero todo el mundo las sigue):
- Los símbolos de
#definese escriben en MAYÚSCULAS, para distinguirlos de las variables de un vistazo. - Los
#definevan al principio del archivo, o en un.hsi los usan varios archivos. - Sin punto y coma al final: el
;también es texto y se copiaría donde uses el símbolo (en los ejercicios verás lo que pasa).
Pensar que #define CARGA_MAXIMA 1500 crea una variable. No hay variable: no ocupa memoria, no tiene tipo y no puedes cambiarla con CARGA_MAXIMA = 2000; (el compilador vería 1500 = 2000; y se quejaría). Es una búsqueda y sustitución, como la de un editor de texto.
**¿#define o const int?** Las dos sirven para tener constantes con nombre, pero no son lo mismo:
#define CARGA_MAXIMA 1500 | const int CARGA_MAXIMA = 1500; | |
|---|---|---|
| Quién lo procesa | El preprocesador (texto) | El compilador (variable) |
| Tiene tipo | No | Sí, int |
| Ocupa memoria | No | Sí, como cualquier variable |
| Sirve para el tamaño de un array global | Sí | No en C (en C++ sí) |
Se puede preguntar con #ifdef | Sí | No |
En IC2 y en el código de sistemas que vas a leer verás sobre todo #define, así que es lo que usamos en este tema.
Macros con parámetros
El #define tiene un truco extra: el símbolo puede llevar parámetros, como una función. En tus apuntes aparece este ejemplo:
#define DEMASIADO_GRANDE(v) (v >= 1000)
Cuando escribes DEMASIADO_GRANDE(i), el preprocesador cambia v por lo que hayas puesto entre paréntesis (i) y el compilador ve (i >= 1000). Parece una función, pero no lo es: no se evalúa nada, solo se copia texto. Y eso tiene consecuencias. Este programa compara macros bien y mal escritas:
#include <stdio.h>
#define PESO_MEDIO 75
#define DEMASIADO_GRANDE(v) ((v) >= 1000) /* la de la práctica, con paréntesis */
#define PESO_MAL(n) n * PESO_MEDIO /* falta paréntesis en el parámetro */
#define PESO_BIEN(n) ((n) * PESO_MEDIO) /* paréntesis en n y en todo */
#define LIBRE_MAL(c) 1500 - (c) /* falta el paréntesis exterior */
#define LIBRE_BIEN(c) (1500 - (c))
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int main(void) {
int adultos = 4, ninos = 2;
printf("PESO_MAL(adultos + ninos) = %d\n", PESO_MAL(adultos + ninos));
printf("PESO_BIEN(adultos + ninos) = %d\n", PESO_BIEN(adultos + ninos));
printf("2 * LIBRE_MAL(500) = %d\n", 2 * LIBRE_MAL(500));
printf("2 * LIBRE_BIEN(500) = %d\n", 2 * LIBRE_BIEN(500));
printf("¿1200 kg demasiado? %d\n", DEMASIADO_GRANDE(1200));
printf("¿800 kg demasiado? %d\n", DEMASIADO_GRANDE(800));
int planta = 3;
int mayor = MAX(planta++, 2); /* planta++ aparece DOS veces al expandir */
printf("mayor = %d, planta = %d\n", mayor, planta);
return 0;
}
La salida:
PESO_MAL(adultos + ninos) = 154 PESO_BIEN(adultos + ninos) = 450 2 * LIBRE_MAL(500) = 2500 2 * LIBRE_BIEN(500) = 2000 ¿1200 kg demasiado? 1 ¿800 kg demasiado? 0 mayor = 4, planta = 5
Cada pareja de líneas enseña una trampa distinta. El dibujo sigue la primera paso a paso: a la izquierda, la macro sin paréntesis; a la derecha, la buena.
Trampa 1: paréntesis alrededor de cada parámetro. PESO_MAL(adultos + ninos) se convierte en adultos + ninos * 75, y como * va antes que + sale 4 + 150 = 154. PESO_BIEN pone (n) entre paréntesis, así que se convierte en ((adultos + ninos) * 75) = 6 · 75 = 450. Exactamente mi gancho.
Trampa 2: paréntesis alrededor de todo. LIBRE_MAL(500) se convierte en 1500 - (500), que solo parece bien hasta que lo multiplicas: 2 * LIBRE_MAL(500) es 2 * 1500 - (500) = 3000 − 500 = 2500, cuando lo que querías era 2 · (1500 − 500) = 2000. LIBRE_BIEN lleva el paréntesis exterior y da 2000.
Trampa 3: los parámetros se copian, no se calculan. MAX(planta++, 2) se convierte en ((planta++) > (2) ? (planta++) : (2)). El planta++ aparece dos veces: el primero compara 3 > 2 (cierto) y deja planta en 4; el segundo devuelve ese 4 y deja planta en 5. Por eso sale mayor = 4, planta = 5, cuando con una función de verdad habría salido mayor = 3, planta = 4.
Y DEMASIADO_GRANDE funciona: 1200 >= 1000 es cierto (1) y 800 >= 1000 es falso (0). Le añadí paréntesis alrededor de v respecto a la versión de los apuntes, porque DEMASIADO_GRANDE(a + b) ya funcionaba, pero con un operador de menos prioridad dentro (por ejemplo, DEMASIADO_GRANDE(x = 1200)) dejaría de hacerlo.
Escribir una macro «como una función» sin paréntesis. La regla es mecánica: cada parámetro entre paréntesis y la expresión entera entre paréntesis. #define CUADRADO(x) ((x) * (x)), nunca x * x.
Nunca pases a una macro algo con efectos (i++, una llamada que imprime, scanf…). Guarda antes el valor en una variable y pasa la variable.
¿Macro o función?
| Situación | Mejor… | Por qué |
|---|---|---|
| Una constante con nombre | #define | Se puede usar en #if y en tamaños de arrays |
| Una cuenta de una línea con números | Macro con paréntesis | No hay llamada, y vale para int o double |
| Algo con varios pasos, bucles o variables | Función | Se comprueban los tipos y se puede depurar |
Argumentos con efectos (i++, llamadas) | Función | La macro los repetiría |
Compilación condicional: #ifdef, #ifndef, #if
Además de sustituir, el preprocesador puede quitar o dejar trozos de código según los símbolos definidos. La estructura básica es:
#ifdef SIMBOLO
/* bloque 1: llega al compilador si SIMBOLO está definido */
#else
/* bloque 2: llega al compilador si NO lo está */
#endif
Parece un if/else de C, pero la diferencia es enorme: este lo decide el preprocesador al compilar, y el bloque descartado se borra antes de que el compilador lo vea. Si ese bloque tuviera un error de sintaxis, el programa compilaría igual. Este programa lo usa de cuatro maneras:
#include <stdio.h>
#define VERSION 2
int main(void) {
int carga = 1320;
#ifdef DEBUG /* solo existe si compilas con -DDEBUG */
printf("[DEBUG] carga leída del sensor: %d kg\n", carga);
#endif
#ifndef CARGA_MAXIMA /* si nadie la ha definido antes... */
#define CARGA_MAXIMA 1500 /* ...le damos un valor por defecto */
#endif
#if VERSION >= 2 /* #if sí compara valores (enteros) */
printf("Panel v%d: carga %d de %d kg\n", VERSION, carga, CARGA_MAXIMA);
#else
printf("Panel antiguo: %d kg\n", carga);
#endif
#if 0
esto no es C: el compilador nunca lo ve
#endif
#ifdef DEBUG
printf("[DEBUG] fin de la comprobación\n");
#else
printf("Modo normal: sin trazas\n");
#endif
return 0;
}
Y aquí viene lo interesante: el mismo archivo da dos programas distintos. Compilado normal, con gcc condicional.c:
Panel v2: carga 1320 de 1500 kg Modo normal: sin trazas
Compilado con gcc -DDEBUG condicional.c:
[DEBUG] carga leída del sensor: 1320 kg Panel v2: carga 1320 de 1500 kg [DEBUG] fin de la comprobación
La opción -DDEBUG equivale a escribir #define DEBUG al principio del archivo, sin tocarlo. Es la que aparece en tus apuntes como gcc -DMAEMO para elegir el mensaje de bienvenida de cada sistema. Repasemos los cuatro bloques:
- **
#ifdef DEBUG** (líneas 8 y 26): solo pregunta siDEBUGexiste. Sin-DDEBUGdesaparecen las dos trazas y queda el «Modo normal»; con-DDEBUGsalen las dos trazas y desaparece el «Modo normal». - **
#ifndef CARGA_MAXIMA(línea 12): «si no está definido». Sirve para dar un valor por defecto** que se puede cambiar desde fuera:gcc -DCARGA_MAXIMA=2000 condicional.charía que el bloque se saltase y el panel dijera 2000. - **
#if VERSION >= 2** (línea 16):#ifsí mira el valor, y puede comparar enteros con==,!=,<,>=,&&,||… ComoVERSIONvale 2, se queda la primera rama. Solo sirve con constantes que el preprocesador conozca: no puede mirar variables, porque las variables existen cuando el programa ya se ejecuta. - **
#if 0** (línea 22): la condición siempre es falsa, así que lo de dentro no llega nunca al compilador. Por esoesto no es C: el compilador nunca lo veno da error. Es una forma rápida de «apagar» un bloque entero de código mientras pruebas.
Confundir #ifdef X con #if X. #ifdef pregunta si existe; #if pregunta cuánto vale. Con #define DEBUG 0, #ifdef DEBUG es cierto (existe, aunque valga 0). Lo verás en directo en los ejercicios.
¿Cuál uso?
| Quiero… | Uso |
|---|---|
Activar algo solo si compilo con -DNOMBRE | #ifdef NOMBRE |
Dar un valor por defecto que se pueda cambiar con -D | #ifndef NOMBRE + #define NOMBRE valor |
| Elegir entre varias versiones según un número | #if NOMBRE == 1 … #elif … #else … #endif |
| Desactivar un bloque mientras pruebo | #if 0 … #endif |
| Parar si alguien compila con un valor imposible | #error "mensaje" dentro del #else |
Guardas de inclusión en los .h
Ahora junta dos ideas: #include pega archivos y un .h puede incluir a otros. ¿Qué pasa si un archivo acaba incluido dos veces? Si dentro hay un struct, el compilador lo vería definido dos veces y daría un error de redefinición. Pasa más de lo que parece: guardas.c incluye ascensor.h y panel.h, y panel.h también necesita ascensor.h. La solución son las guardas de inclusión, un #ifndef que envuelve todo el archivo:
/* ascensor.h: definiciones compartidas del ascensor */
#ifndef ASCENSOR_H /* ¿primera vez que entramos en este archivo? */
#define ASCENSOR_H /* lo anotamos: la próxima vez nos saltamos todo */
#define CARGA_MAXIMA 1500
#define PLANTAS 10
struct Viaje { /* definir dos veces un struct es un error */
int origen;
int destino;
int carga;
};
#endif /* ASCENSOR_H */
Así funciona: la primera vez que se incluye, ASCENSOR_H no existe, así que el contenido pasa y, de paso, se define ASCENSOR_H. La segunda vez, ASCENSOR_H ya existe y el #ifndef se salta todo hasta el #endif. panel.h lleva su propia guarda e incluye ascensor.h:
/* panel.h: lo que necesita la pantalla del panel */ #ifndef PANEL_H #define PANEL_H #include "ascensor.h" /* el panel también usa struct Viaje */ #define FORMATO_VIAJE "Viaje %d -> %d (%d kg)\n" #endif /* PANEL_H */
Y el programa principal, que los incluye a los dos:
#include <stdio.h>
#include "ascensor.h" /* comillas: se busca primero en la carpeta del programa */
#include "panel.h" /* vuelve a incluir ascensor.h: la guarda lo deja vacío */
int main(void) {
struct Viaje v = {0, 7, 640};
printf(FORMATO_VIAJE, v.origen, v.destino, v.carga);
printf("Carga libre: %d kg de %d\n", CARGA_MAXIMA - v.carga, CARGA_MAXIMA);
return 0;
}
La salida:
Viaje 0 -> 7 (640 kg) Carga libre: 860 kg de 1500
Compila sin quejarse y la carga libre cuadra: 1500 − 640 = 860. El dibujo enseña el recorrido de los dos #include:
Para comprobarlo, el mismo programa sin <stdio.h> pasado por gcc -E -P:
struct Viaje {
int origen;
int destino;
int carga;
};
int main(void) {
struct Viaje v = {0, 7, 640};
printf("Viaje %d -> %d (%d kg)\n", v.origen, v.destino, v.carga);
return 0;
}
struct Viaje aparece una sola vez, aunque ascensor.h se incluyó dos veces. Las líneas #define FORMATO_VIAJE también han hecho su trabajo: el printf ya lleva la cadena copiada.
Pon la guarda en todos tus .h, aunque hoy solo los incluya un archivo. El nombre suele ser el del archivo en mayúsculas con _H: ascensor.h → ASCENSOR_H. Muchos compiladores aceptan también #pragma once, pero no es estándar: en IC2, #ifndef.
Macros predefinidas: __FILE__, __LINE__ y compañía
El estándar de C ya trae algunas macros hechas que el preprocesador rellena solo. No las definas tú ni intentes cambiarlas:
| Macro | Se sustituye por |
|---|---|
__FILE__ | El nombre del archivo, como cadena |
__LINE__ | El número de la línea donde aparece, como entero |
__DATE__ | La fecha de compilación, "Mmm dd aaaa" |
__TIME__ | La hora de compilación, "hh:mm:ss" |
__STDC_VERSION__ | La versión del estándar (201112L con -std=c11) |
Y una que no es macro pero se usa con ellas: __func__, que el compilador convierte en el nombre de la función en la que estás. Este programa las usa todas:
#include <stdio.h>
/* __FILE__ y __LINE__ se sustituyen donde se USA AVISO, no donde se define */
#define AVISO(msg) printf("[%s:%d] %s\n", __FILE__, __LINE__, msg)
void comprobar_puerta(void) {
printf("Función actual: %s\n", __func__); /* __func__ lo pone el compilador */
AVISO("puerta abierta");
}
int main(void) {
printf("Archivo: %s\n", __FILE__);
printf("Línea: %d\n", __LINE__);
printf("Compilado el %s a las %s\n", __DATE__, __TIME__);
printf("Estándar C: %ld\n", __STDC_VERSION__); /* 201112L = C11 */
AVISO("sobrecarga detectada");
comprobar_puerta();
return 0;
}
La salida (la fecha y la hora son las de cuando lo compilé):
Archivo: predefinidas.c Línea: 13 Compilado el Oct 24 2026 a las 23:14:47 Estándar C: 201112 [predefinidas.c:16] sobrecarga detectada Función actual: comprobar_puerta [predefinidas.c:8] puerta abierta
Lo que hay que mirar:
__LINE__vale 13 porque está en la línea 13 del archivo. Cuenta las líneas del código y verás que cuadra.AVISOes una macro que usa__FILE__y__LINE__por dentro. Como la sustitución se hace donde se usa la macro, el primer aviso dice línea 16 (donde se llama dentro demain) y el segundo dice línea 8 (dentro decomprobar_puerta), no la línea 4, donde está el#define.__DATE__y__TIME__se congelan al compilar: si ejecutas el programa mañana, seguirá diciendo la hora de la compilación.__STDC_VERSION__es201112L(unlong, por eso el%ld), que significa C11, el estándar que pedimos con-std=c11.
La macro AVISO es la idea que hay detrás de cualquier sistema de trazas: decir dónde ha pasado algo sin escribir el número de línea a mano. En la práctica la vas a combinar con #ifdef DEBUG para que solo exista cuando la necesitas.
Errores típicos con el preprocesador
Macros sin paréntesis. PESO(a + b) sin (n) multiplica solo b, y 2 * LIBRE(500) sin paréntesis exterior multiplica solo el 1500. Cada parámetro y la expresión entera, siempre entre paréntesis.
Punto y coma al final del #define. #define CARGA_MAXIMA 1500; copia también el ; en cada uso. A veces da un error de compilación y otras, lo que es peor, compila y hace otra cosa.
Pasar i++ (o cualquier cosa con efectos) a una macro. Si el parámetro aparece dos veces en la macro, el incremento se hace dos veces.
Usar #ifdef cuando querías comparar un valor. #ifdef solo mira si el símbolo existe; para comparar, #if.
Un .h sin guarda incluido desde dos sitios. El struct acaba definido dos veces y el compilador protesta con redefinition of 'struct …'.
Míralo paso a paso en Python Tutor
Python Tutor ejecuta código línea a línea y dibuja las variables y la memoria. Admite C: elige C en el desplegable de lenguaje y pega este programa, que mete en dos variables la macro mala y la buena:
#include <stdio.h>
#define PESO_MEDIO 75
#define PESO_MAL(n) n * PESO_MEDIO
#define PESO_BIEN(n) ((n) * PESO_MEDIO)
int main(void) {
int adultos = 4;
int ninos = 2;
int mal = PESO_MAL(adultos + ninos); /* adultos + ninos * 75 */
int bien = PESO_BIEN(adultos + ninos); /* ((adultos + ninos) * 75) */
int libre = 1500 - bien;
printf("mal = %d, bien = %d, libre = %d\n", mal, bien, libre);
return 0;
}
Su salida:
mal = 154, bien = 450, libre = 1050
Qué observar paso a paso:
- **Las líneas de
#defineno son pasos.** Python Tutor empieza directamente enmain: el preprocesador ya ha hecho su trabajo antes de que el programa exista. Es la prueba de que las macros no «se ejecutan». - **Cuando se crea
mal**, el valor que aparece es 154. Mira la línea:PESO_MAL(adultos + ninos)se ha convertido enadultos + ninos * 75, y 4 + 2 · 75 = 154. - **Cuando se crea
bien**, sale 450: los paréntesis dePESO_BIENobligan a sumar primero, (4 + 2) · 75. - **
librevale 1050** (1500 − 450). Con el valor malo habría salido 1346 y el ascensor dejaría subir a casi veinte personas más de la cuenta.
Resumen
| Concepto | Qué hace | Ejemplo |
|---|---|---|
| Preprocesador | Transforma el texto antes de compilar | gcc -E -P archivo.c lo enseña |
#include <…> / "…" | Pega un archivo del sistema / tuyo | #include "ascensor.h" |
#define sin valor | Anota que un símbolo existe | #define SISTEMA_ASCENSOR |
#define con valor | Sustituye texto en todo el archivo | #define CARGA_MAXIMA 1500 |
| Macro con parámetros | Copia los argumentos en el texto | #define PESO(n) ((n) * 75) |
#ifdef / #ifndef | Deja o quita código según si existe un símbolo | #ifdef DEBUG |
#if / #elif | Deja o quita código según un valor | #if VERSION >= 2 |
gcc -D | Define un símbolo desde fuera | gcc -DDEBUG prog.c |
| Guarda de inclusión | Evita incluir dos veces un .h | #ifndef ASCENSOR_H |
| Macros predefinidas | Archivo, línea, fecha, versión | __FILE__, __LINE__ |
Tres ideas para llevarte:
- El preprocesador copia texto: no calcula, no entiende tipos ni prioridades. Si una macro hace algo raro, mira el resultado con
gcc -E. - En las macros, paréntesis en cada parámetro y alrededor de todo, y nada con efectos como argumento.
- Con
#ifdef,#ifygcc -D, un mismo archivo da varios programas: con trazas o sin ellas, para un modelo u otro.
Ahora toca practicar: en la práctica del preprocesador montamos el panel de configuración del ascensor con su config.h, un registro de depuración que se enciende con -DDEBUG y tres modelos de ascensor salidos del mismo archivo. Y después del preprocesador, en IC2 llegan los procesos de Linux con fork().
¿Te ha servido este artículo? Así puedes apoyar SergioLearns →

2 comentarios