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.

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.

Del código fuente al ejecutable El archivo .c pasa primero por el preprocesador, que pega los include, sustituye los define, decide los ifdef y borra comentarios; el compilador recibe código limpio, por ejemplo a + n * 75. Lo que pasa cuando escribes gcc gancho.c Tu archivo .c #include <stdio.h> #define PESO_MEDIO 75 /* comentario */ #ifdef DEBUG … #endif PESO_PERSONAS(a + n) Preprocesador pega los #include sustituye los #define decide los #ifdef borra los comentarios Código limpio (gcc -E) …800 líneas de stdio.h sin # ni comentarios solo el bloque elegido a + n * 75 Compilador → traduce a binario → programa ejecutable (aquí ya no queda ningún #)
El compilador nunca ve tus #define: recibe la columna de la derecha, que es lo que te enseña gcc -E.

¿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):

DirectivaQué hace
#includePega en ese punto el contenido de otro archivo
#defineDefine un símbolo o una macro que se sustituye por un texto
#undefBorra la definición de un símbolo
#ifdefDeja pasar un bloque si el símbolo está definido
#ifndefDeja pasar un bloque si el símbolo no está definido
#if, #elif, #else, #endifCompilación condicional comparando valores
#errorPara 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_ASCENSOR no 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 1500 es la versión con valor: donde ponga CARGA_MAXIMA, el compilador verá 1500. Igual con PLANTAS y con NOMBRE, que se sustituye por la cadena "Ascensor A" con sus comillas.
  • CARGA_AVISO usa CARGA_MAXIMA dentro. La sustitución se hace en todo el texto, también en lo que sale de otra sustitución, así que CARGA_AVISO acaba siendo 1500 * 9 / 10: 13500 / 10 = 1350. Es lo mismo que el ejemplo de tus apuntes con #define UNO 1 y #define OTRO_UNO UNO: OTRO_UNO acaba 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):

  1. Los símbolos de #define se escriben en MAYÚSCULAS, para distinguirlos de las variables de un vistazo.
  2. Los #define van al principio del archivo, o en un .h si los usan varios archivos.
  3. 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 1500const int CARGA_MAXIMA = 1500;
Quién lo procesaEl preprocesador (texto)El compilador (variable)
Tiene tipoNoSí, int
Ocupa memoriaNoSí, como cualquier variable
Sirve para el tamaño de un array globalSíNo en C (en C++ sí)
Se puede preguntar con #ifdefSí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.

Expansión de una macro paso a paso PESO_MAL(adultos + ninos) se convierte en adultos + ninos * 75, que vale 154 porque la multiplicación va primero; PESO_BIEN se convierte en ((adultos + ninos) * 75), que vale 450. PESO_PERSONAS con 4 adultos y 2 niños Sin paréntesis: PESO_MAL Con paréntesis: PESO_BIEN 1. Llamada PESO_MAL(adultos + ninos) PESO_BIEN(adultos + ninos) 2. n se cambia por el texto adultos + ninos * PESO_MEDIO ((adultos + ninos) * PESO_MEDIO) 3. PESO_MEDIO se cambia por 75 adultos + ninos * 75 ((adultos + ninos) * 75) 4. Calcula el compilador 4 + (2 * 75) = 154 (4 + 2) * 75 = 450
El preprocesador copia texto sin entender de prioridades: los paréntesis son los que protegen la cuenta.

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ónMejor…Por qué
Una constante con nombre#defineSe puede usar en #if y en tamaños de arrays
Una cuenta de una línea con númerosMacro con paréntesisNo hay llamada, y vale para int o double
Algo con varios pasos, bucles o variablesFunciónSe comprueban los tipos y se puede depurar
Argumentos con efectos (i++, llamadas)FunciónLa 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 si DEBUG existe. Sin -DDEBUG desaparecen las dos trazas y queda el «Modo normal»; con -DDEBUG salen 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.c haría que el bloque se saltase y el panel dijera 2000.
  • **#if VERSION >= 2** (línea 16): #if sí mira el valor, y puede comparar enteros con ==, !=, <, >=, &&, ||… Como VERSION vale 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 eso esto no es C: el compilador nunca lo ve no 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:

Guardas de inclusión guardas.c incluye ascensor.h, que se copia y define ASCENSOR_H; luego incluye panel.h, que vuelve a incluir ascensor.h, pero como ASCENSOR_H ya existe se salta entero y struct Viaje aparece una vez. Qué hace la guarda #ifndef ASCENSOR_H guardas.c #include «ascensor.h» #include «panel.h» ascensor.h (1.ª vez) ¿ASCENSOR_H definido? No → se copia y se define panel.h #include «ascensor.h» ascensor.h (2.ª vez) ASCENSOR_H ya existe → se salta entero 1 2 Resultado (gcc -E): struct Viaje aparece una sola vez → no hay error de redefinición
La primera inclusión define ASCENSOR_H; la segunda lo encuentra definido y no copia nada.

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:

MacroSe 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.
  • AVISO es 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 de main) y el segundo dice línea 8 (dentro de comprobar_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__ es 201112L (un long, 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:

  1. **Las líneas de #define no son pasos.** Python Tutor empieza directamente en main: el preprocesador ya ha hecho su trabajo antes de que el programa exista. Es la prueba de que las macros no «se ejecutan».
  2. **Cuando se crea mal**, el valor que aparece es 154. Mira la línea: PESO_MAL(adultos + ninos) se ha convertido en adultos + ninos * 75, y 4 + 2 · 75 = 154.
  3. **Cuando se crea bien**, sale 450: los paréntesis de PESO_BIEN obligan a sumar primero, (4 + 2) · 75.
  4. **libre vale 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

ConceptoQué haceEjemplo
PreprocesadorTransforma el texto antes de compilargcc -E -P archivo.c lo enseña
#include <…> / "…"Pega un archivo del sistema / tuyo#include "ascensor.h"
#define sin valorAnota que un símbolo existe#define SISTEMA_ASCENSOR
#define con valorSustituye texto en todo el archivo#define CARGA_MAXIMA 1500
Macro con parámetrosCopia los argumentos en el texto#define PESO(n) ((n) * 75)
#ifdef / #ifndefDeja o quita código según si existe un símbolo#ifdef DEBUG
#if / #elifDeja o quita código según un valor#if VERSION >= 2
gcc -DDefine un símbolo desde fueragcc -DDEBUG prog.c
Guarda de inclusiónEvita incluir dos veces un .h#ifndef ASCENSOR_H
Macros predefinidasArchivo, línea, fecha, versión__FILE__, __LINE__

Tres ideas para llevarte:

  1. El preprocesador copia texto: no calcula, no entiende tipos ni prioridades. Si una macro hace algo raro, mira el resultado con gcc -E.
  2. En las macros, paréntesis en cada parámetro y alrededor de todo, y nada con efectos como argumento.
  3. Con #ifdef, #if y gcc -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().

Siguiente paso: prácticaEl 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)

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

Sigue aprendiendo

2 comentarios

Deja una respuesta

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