1. Qué hace el preprocesador
Como vimos en el tema 4, el preprocesador es la primera etapa de la compilación: recibe el texto del fuente y entrega otro texto al compilador. Procesa las líneas que empiezan con #, las directivas, y hace cuatro tipos de trabajo:
| Trabajo | Directivas | Ejemplo |
|---|---|---|
| Incluir archivos | #include | Copiar stdio.h en el fuente (temas 3 y 13). |
| Definir macros | #define, #undef | Reemplazar TAM_BUFFER por 4096. |
| Compilar según condiciones | #if, #ifdef, #ifndef, #elif, #else, #endif | Incluir un bloque solo en Linux, o solo al depurar. |
| Informar y controlar | #error, #warning, #pragma, #line | Detener la compilación si la configuración es inválida. |
Lo esencial es que trabaja con texto, dividido en tokens, sin entender lo que significa. No sabe qué es un tipo, una variable o una función; no sabe de precedencias ni de alcances. Reemplaza nombres por texto, y entrega el resultado. Todas las trampas de este tema salen de esa ceguera, y todas se diagnostican de la misma forma: mirando con gcc -E qué texto recibe realmente el compilador.
2. Macros objeto
Una macro objeto es un nombre que el preprocesador reemplaza por un texto fijo:
#define TAM_BUFFER 4096
#define NOMBRE_PROG "tamano"
#define OPT_VERBOSO (1u << 0)
#define PERMISOS_DEF 0644
La convención, como vimos en el tema 3, es escribir las macros en mayúsculas, para que se distingan de las variables y funciones: quien lee TAM_BUFFER sabe que no es una variable, y que puede haber sorpresas. La definición termina al final de la línea, sin punto y coma; si lleva uno, el punto y coma se copia en cada uso, con resultados extraños. Para continuar una definición en la línea siguiente, se termina la línea con una barra invertida.
Una macro vale desde su #define hasta el final del archivo, o hasta un #undef que la elimine. No respeta los bloques: una macro definida dentro de una función sigue valiendo en las funciones siguientes, porque el preprocesador no sabe dónde terminan las funciones. Y redefinirla con otro valor produce una advertencia, la que vimos con -DLIMITE en el tema 4:
$ gcc -std=c17 -Wall -Wextra -c rd.c
rd.c:2:9: warning: ‘TAM’ redefined
2 | #define TAM 20
| ^~~
rd.c:1:9: note: this is the location of the previous definition
1 | #define TAM 10
| ^~~
El tema 6 comparó las tres formas de nombrar una constante: #define, enum y const. La ventaja de #define es que sirve en todos los lugares que exigen una constante, incluidas las directivas #if, donde ninguna de las otras dos funciona, porque el preprocesador no las conoce.
3. Macros con parámetros y sus paréntesis
Una macro puede recibir parámetros, y entonces parece una función. Pero no lo es: sus argumentos no se evalúan, se pegan como texto en el cuerpo. Este programa define tres macros, dos de ellas mal:
#include <stdio.h>
#define DOBLE_MAL(x) x + x
#define CUADRADO_MAL(x) x * x
#define CUADRADO(x) ((x) * (x))
int main(void)
{
int a = 3;
printf("10 * DOBLE_MAL(a) = %d\n", 10 * DOBLE_MAL(a));
printf("CUADRADO_MAL(a + 1) = %d\n", CUADRADO_MAL(a + 1));
printf("CUADRADO(a + 1) = %d\n", CUADRADO(a + 1));
printf("100 / CUADRADO(2) = %d\n", 100 / CUADRADO(2));
return 0;
}
$ ./parentesis
10 * DOBLE_MAL(a) = 33
CUADRADO_MAL(a + 1) = 7
CUADRADO(a + 1) = 16
100 / CUADRADO(2) = 25
$ gcc -E parentesis.c | grep 'printf("' | tail -n 4
printf("10 * DOBLE_MAL(a) = %d\n", 10 * a + a);
printf("CUADRADO_MAL(a + 1) = %d\n", a + 1 * a + 1);
printf("CUADRADO(a + 1) = %d\n", ((a + 1) * (a + 1)));
printf("100 / CUADRADO(2) = %d\n", 100 / ((2) * (2)));
gcc -E deja ver cada error. DOBLE_MAL(a) se convirtió en a + a, y 10 * a + a es 33, no 60: el texto de la macro se mezcló con lo que la rodeaba. CUADRADO_MAL(a + 1) se convirtió en a + 1 * a + 1, que por precedencia es 3 + 3 + 1: el argumento se mezcló con el cuerpo. De ahí salen las dos reglas, que CUADRADO cumple:
- Cada parámetro, entre paréntesis, en cada uso:
(x), para que un argumento comoa + 1se agrupe completo. parámetros - Todo el cuerpo, entre paréntesis:
((x) * (x)), para que la expresión no se mezcle con lo que la rodea, como en100 / CUADRADO(2). cuerpo
Es la misma razón por la que las banderas del tema 8 se definían como (1u << 0) y no 1u << 0: el texto de una macro objeto también se pega en una expresión ajena, y x & OPT_VERBOSO == … se agruparía de otra forma.
4. Argumentos que se evalúan dos veces
Los paréntesis resuelven la precedencia, pero no un problema más profundo. Como el argumento se pega como texto, si el parámetro aparece dos veces en el cuerpo, el argumento se evalúa dos veces:
#include <stdio.h>
#define MAXIMO(a, b) ((a) > (b) ? (a) : (b))
static inline int maximo(int a, int b)
{
return a > b ? a : b;
}
int main(void)
{
int i = 5, j = 3;
int m = MAXIMO(i++, j);
printf("macro: m = %d, i = %d\n", m, i);
i = 5;
m = maximo(i++, j);
printf("función: m = %d, i = %d\n", m, i);
return 0;
}
$ ./doble
macro: m = 6, i = 7
función: m = 5, i = 6
$ gcc -E doble.c | grep 'int m ='
int m = ((i++) > (j) ? (i++) : (j));
La macro, perfectamente parentizada, incrementó i dos veces y devolvió 6: i++ se evaluó una vez en la comparación y otra en la rama elegida. La función, en cambio, recibió una copia del valor y se comportó como se espera. No hay forma de arreglar esto dentro de una macro estándar: la solución es una función static inline, que con optimización es tan rápida como la macro, como vimos en el tema 11, y además comprueba los tipos. El núcleo Linux resuelve el caso del máximo con extensiones de gcc que evalúan cada argumento una vez, y por las mismas razones la biblioteca documenta qué «funciones» son en realidad macros: con ellas, nunca se pasan argumentos con efectos secundarios.
5. Macros de varias sentencias
Una macro que debe ejecutar varias sentencias tiene un problema distinto: se usa como si fuera una sola sentencia, con un punto y coma al final, pero se expande en varias. Este programa lo muestra con dos versiones de una macro de aviso:
#include <stdio.h>
/* versión ingenua: dos sentencias */
#define AVISAR_MAL(msg) fprintf(stderr, "aviso: %s\n", msg); errores++
/* versión correcta: una sola sentencia */
#define AVISAR(msg) \
do { \
fprintf(stderr, "aviso: %s\n", msg); \
errores++; \
} while (0)
int main(void)
{
int errores = 0;
int lleno = 0;
if (lleno)
AVISAR_MAL("disco lleno");
printf("con AVISAR_MAL: errores = %d\n", errores);
errores = 0;
if (lleno)
AVISAR("disco lleno");
else
printf("hay espacio\n");
printf("con AVISAR: errores = %d\n", errores);
return 0;
}
$ gcc -std=c17 -Wall -Wextra -o sentencias sentencias.c
sentencias.c: In function ‘main’:
sentencias.c:4:26: warning: macro expands to multiple statements [-Wmultistatement-macros]
4 | #define AVISAR_MAL(msg) fprintf(stderr, "aviso: %s\n", msg); errores++
| ^~~~~~~
sentencias.c:19:9: note: in expansion of macro ‘AVISAR_MAL’
19 | AVISAR_MAL("disco lleno");
| ^~~~~~~~~~
sentencias.c:18:5: note: some parts of macro expansion are not guarded by this ‘if’ clause
18 | if (lleno)
| ^~
$ ./sentencias
con AVISAR_MAL: errores = 1
hay espacio
con AVISAR: errores = 0
Con lleno en 0, no debería contarse ningún error. Pero AVISAR_MAL se expande en dos sentencias, y el if sin llaves solo controla la primera: el errores++ se ejecutó siempre, el mismo error de sangría engañosa del tema 9, ahora escondido dentro de una macro. Y si se usa AVISAR_MAL antes de un else, el programa ni compila: error: ‘else’ without a previous ‘if’, porque el if terminó en el primer punto y coma.
Envolver el cuerpo en llaves no alcanza: { … }; seguido de else también rompe el if, por el punto y coma. La solución estándar es do { … } while (0): un bucle que se ejecuta exactamente una vez, y que, sintácticamente, es una sola sentencia que exige un punto y coma al final. Se comporta en todos los contextos como una llamada a función. Es el uso de do while que anunciamos en el tema 9, y lo encontrarás en miles de macros del núcleo Linux y de la biblioteca de C.
6. Los operadores # y ##
Dentro del cuerpo de una macro con parámetros hay dos operadores que solo existen en el preprocesador. #parametro convierte el argumento en una cadena literal, con su texto exacto; a ## b pega dos tokens en uno solo. Este programa usa los dos, y prepara una macro de depuración para la sección siguiente:
#include <stdio.h>
/* muestra una expresión y su valor: # convierte el argumento en cadena */
#define MOSTRAR(expr) printf("%s = %ld\n", #expr, (long) (expr))
/* une dos fragmentos en un solo nombre: ## pega tokens */
#define CONTADOR(nombre) static int contador_##nombre = 0
/* mensaje de depuración con el lugar del código, solo si DEPURACION está definida */
#ifdef DEPURACION
#define DEPURAR(...) \
do { \
fprintf(stderr, "[%s:%d %s] ", __FILE__, __LINE__, __func__); \
fprintf(stderr, __VA_ARGS__); \
fputc('\n', stderr); \
} while (0)
#else
#define DEPURAR(...) do { } while (0)
#endif
CONTADOR(lecturas);
static int leer(int bloque)
{
contador_lecturas++;
DEPURAR("leyendo el bloque %d", bloque);
return bloque * 512;
}
int main(void)
{
int tam = 4096;
MOSTRAR(tam / 512);
MOSTRAR(sizeof(long) * 8);
MOSTRAR(leer(3));
MOSTRAR(contador_lecturas);
DEPURAR("terminando, tam = %d", tam);
return 0;
}
$ gcc -std=c17 -Wall -Wextra -o depurar depurar.c && ./depurar
tam / 512 = 8
sizeof(long) * 8 = 64
leer(3) = 1536
contador_lecturas = 1
$ gcc -E depurar.c | grep -E 'printf\("%s = %ld' | head -n 2
printf("%s = %ld\n", "tam / 512", (long) (tam / 512));
printf("%s = %ld\n", "sizeof(long) * 8", (long) (sizeof(long) * 8));
$ gcc -E depurar.c | grep 'contador_lecturas = 0'
static int contador_lecturas = 0;
MOSTRAR usa el argumento dos veces: una como texto, "tam / 512", y otra como expresión a evaluar. Ninguna función puede hacer eso, porque una función recibe valores, no el texto que los produjo. Es la forma de escribir mensajes de diagnóstico que dicen qué se calculó sin repetirlo a mano, y es exactamente lo que hace assert en la sección 10. CONTADOR(lecturas) construyó el nombre contador_lecturas, que luego se usa como una variable común. Pegar tokens sirve para generar familias de nombres relacionados, aunque abusar de ello vuelve el código imposible de buscar con grep.
7. Macros predefinidas y variádicas
El preprocesador define algunas macros por su cuenta, además de las de la plataforma que listamos con gcc -dM -E en el tema 4. Las más útiles para depurar dicen dónde está el código:
| Nombre | Vale | Comentario |
|---|---|---|
__FILE__ | El nombre del archivo, como cadena. | El que se le pasó al compilador. |
__LINE__ | El número de línea, como entero. | La línea donde se usa la macro que lo contiene. |
__func__ | El nombre de la función actual. | No es una macro sino una variable que define el compilador, pero se usa igual. |
__DATE__, __TIME__ | Fecha y hora de la compilación. | Impiden compilaciones reproducibles: -Wdate-time lo advierte. |
__STDC_VERSION__ | La versión del estándar. | Como en entorno.c del tema 2. |
Para que __FILE__ y __LINE__ indiquen el lugar donde se llamó, y no el de una función auxiliar, tienen que usarse dentro de una macro: se expanden donde la macro se expande. Es lo que hace DEPURAR. Además, DEPURAR es una macro variádica: ... en la lista de parámetros acepta cualquier cantidad de argumentos, y __VA_ARGS__ los pega en el cuerpo, como hacía va_list con las funciones del tema 11. Compilado con -DDEPURACION, el programa muestra sus mensajes en el orden en que ocurren:
$ gcc -std=c17 -Wall -Wextra -DDEPURACION -o depurar depurar.c && ./depurar
tam / 512 = 8
sizeof(long) * 8 = 64
[depurar.c:26 leer] leyendo el bloque 3
leer(3) = 1536
contador_lecturas = 1
[depurar.c:38 main] terminando, tam = 4096
Cada mensaje dice archivo, línea y función, y aparece antes del resultado de leer(3) porque printf evalúa sus argumentos, incluida la llamada a leer, antes de imprimir. Una sutileza de las macros variádicas: si el formato es un parámetro aparte, como en LOG(fmt, ...), una llamada sin argumentos extra deja una coma colgando. C23 lo resuelve con __VA_OPT__(,), que pone la coma solo si hay argumentos; antes se usaba la extensión de GNU , ##__VA_ARGS__.
8. La compilación condicional
Las directivas condicionales deciden qué partes del fuente llegan al compilador. Lo que queda fuera se elimina antes de compilar: no se compila, no ocupa lugar en el ejecutable, y ni siquiera tiene que ser código válido.
| Directiva | Incluye el bloque si… |
|---|---|
#ifdef NOMBRE | la macro NOMBRE está definida, con cualquier valor. |
#ifndef NOMBRE | la macro no está definida. Es la base de las guardas del tema 13. |
#if expresión | la expresión, con enteros y macros, vale distinto de cero. defined(NOMBRE) vale 1 si la macro existe. |
#elif expresión | las condiciones anteriores fallaron y esta vale distinto de cero. |
#else | todas las condiciones anteriores fallaron. |
#endif | (cierra el bloque). |
En depurar.c, DEPURAR tiene dos definiciones: la completa, si DEPURACION está definida, y una vacía, do { } while (0), si no. Sin la opción, los mensajes de depuración no solo no se muestran: no existen en el ejecutable, y no cuestan nada. Es un patrón muy común, y la definición vacía mantiene la forma de sentencia para que DEPURAR(…); siga siendo válido después de un if.
En una expresión de #if, un nombre que no es una macro vale 0. Es cómodo pero peligroso: #if DEPURACIN, mal escrito, vale 0 sin ninguna advertencia, salvo con -Wundef. Por la misma razón, conviene #ifdef o #if defined(…) cuando lo que importa es si la macro existe.
9. Configurar desde la línea de órdenes
La opción -D de gcc define una macro antes de procesar el fuente: -DDEPURACION la define con el valor 1, y -DTAM_BUFFER=65536 con el valor indicado. Combinada con la compilación condicional, permite que un programa tenga valores por omisión que se pueden cambiar sin tocar el fuente, lo que el tema 4 dejó pendiente:
#include <stdio.h>
/* valor por omisión, que se puede cambiar con -DTAM_BUFFER=... */
#ifndef TAM_BUFFER
#define TAM_BUFFER 4096
#endif
#if TAM_BUFFER < 512
#error "TAM_BUFFER debe ser de al menos 512 bytes"
#endif
int main(void)
{
static char buffer[TAM_BUFFER];
printf("buffer de %zu bytes\n", sizeof buffer);
#if defined(__linux__)
printf("compilado para Linux\n");
#elif defined(__APPLE__)
printf("compilado para macOS\n");
#else
printf("compilado para otro sistema\n");
#endif
#if __STDC_VERSION__ >= 202311L
printf("con C23 o posterior\n");
#else
printf("con una versión anterior a C23\n");
#endif
return 0;
}
$ gcc -std=c17 -Wall -Wextra -o buffer buffer.c && ./buffer
buffer de 4096 bytes
compilado para Linux
con una versión anterior a C23
$ gcc -std=c17 -Wall -Wextra -DTAM_BUFFER=65536 -o buffer buffer.c && ./buffer | head -n 1
buffer de 65536 bytes
$ gcc -std=c17 -Wall -Wextra -DTAM_BUFFER=100 -o buffer buffer.c
buffer.c:9:2: error: #error "TAM_BUFFER debe ser de al menos 512 bytes"
9 | #error "TAM_BUFFER debe ser de al menos 512 bytes"
| ^~~~~
$ gcc -std=c23 -Wall -o buffer buffer.c && ./buffer | tail -n 1
con C23 o posterior
El programa reúne los tres usos habituales de la compilación condicional. El #ifndef da un valor por omisión que la línea de órdenes puede reemplazar, sin la advertencia de redefinición del tema 4. El #if con #error valida la configuración al compilar: un valor inválido detiene la compilación con un mensaje claro, en lugar de producir un programa defectuoso. Y los #if defined(__linux__) y __STDC_VERSION__ adaptan el código a la plataforma y a la versión del lenguaje, con las macros predefinidas del tema 4. Así se escriben los programas que compilan en varios sistemas, aunque conviene concentrar esas diferencias en pocos lugares: un archivo lleno de #ifdef es muy difícil de leer y de probar, porque cada combinación es un programa distinto.
10. assert y static_assert
El encabezado assert.h ofrece una macro para comprobar, mientras el programa corre, condiciones que deben cumplirse si el programa es correcto. Si la condición es falsa, muestra cuál falló, dónde, y aborta. Y C11 agregó static_assert, que comprueba una condición al compilar:
#include <stdio.h>
#include <assert.h>
#include <limits.h>
static_assert(CHAR_BIT == 8, "este programa supone bytes de 8 bits");
static_assert(sizeof(int) >= 4, "este programa necesita int de 32 bits");
static int promedio(const int datos[], int n)
{
assert(n > 0); /* quien llama debe pasar al menos uno */
long suma = 0;
for (int i = 0; i < n; i++)
suma += datos[i];
return (int) (suma / n);
}
int main(void)
{
int notas[] = {7, 9, 10};
printf("promedio = %d\n", promedio(notas, 3));
printf("promedio = %d\n", promedio(notas, 0));
return 0;
}
$ gcc -std=c17 -Wall -Wextra -o afirmar afirmar.c && ./afirmar
promedio = 8
afirmar: afirmar.c:10: promedio: Assertion `n > 0' failed.
Aborted
$ gcc -std=c17 -Wall -Wextra -DNDEBUG -o afirmar afirmar.c && ./afirmar
promedio = 8
Floating point exception
$ gcc -std=c17 -Wall -Wextra -c sa.c # static_assert(sizeof(long) == 4, …)
sa.c:2:1: error: static assertion failed: "long debe ser de 32 bits"
El mensaje de assert contiene el texto de la condición, n > 0, obtenido con el operador # de la sección 6, más __FILE__, __LINE__ y __func__: assert es una macro, y por eso puede decir todo eso. Con -DNDEBUG, assert se define como nada, y la comprobación desaparece del ejecutable: la división por cero llegó a ejecutarse y terminó con la SIGFPE del tema 7. De ese comportamiento salen dos reglas:
assertsirve para errores de programación, no para errores de ejecución. Comprueba que el código respete sus propias reglas, como «nunca me llaman connigual a 0». Un archivo que no existe, una entrada inválida o un disco lleno no son errores de programación: se comprueban conify se informan, porque conNDEBUGelassertdesaparece.- La condición de un
assertno puede tener efectos secundarios.assert(leer(fd) > 0)deja de leer cuando se compila conNDEBUG.
static_assert no cuesta nada al ejecutar, porque se comprueba al compilar, y es ideal para documentar y verificar las suposiciones sobre la plataforma: tamaños de tipos, como en el tema 5, o el tamaño de una estructura que debe coincidir con un formato binario. En C11 y C17 se escribe _Static_assert, o static_assert si se incluye assert.h; en C23 es una palabra clave.
11. Las macros de prueba de características
En el tema 4 vimos que con -std=c17 funciones como strdup o fileno desaparecen, aunque se incluya el encabezado correcto. Ahora podemos entender por qué, y cómo se resuelve:
$ gcc -std=c17 -Wall -Wextra -o car caracteristicas.c
caracteristicas.c: In function ‘main’:
caracteristicas.c:6:19: error: implicit declaration of function ‘strdup’; did you mean ‘strcmp’? [-Wimplicit-function-declaration]
…
caracteristicas.c:8:53: error: implicit declaration of function ‘fileno’ [-Wimplicit-function-declaration]
$ sed '1i #define _POSIX_C_SOURCE 200809L' caracteristicas.c > car2.c
$ gcc -std=c17 -Wall -Wextra -o car2 car2.c && ./car2
sistema, descriptor de stdout: 1
Los encabezados de glibc contienen declaraciones de muchos estándares a la vez: C, POSIX, X/Open, y extensiones de BSD y de GNU. Cuáles se ven depende de unas macros que el programa define antes de incluir nada, las macros de prueba de características (feature test macros). Con -std=c17 estricto y ninguna de ellas, solo se ve lo que define el estándar de C, y strdup y fileno son de POSIX. Al definir _POSIX_C_SOURCE con el valor de POSIX.1-2008, aparecen. Las más usadas son:
| Macro | Habilita |
|---|---|
_POSIX_C_SOURCE 200809L | Las interfaces de POSIX.1-2008. La que usamos en el curso. |
_XOPEN_SOURCE 700 | POSIX más las extensiones X/Open (XSI). |
_DEFAULT_SOURCE | Lo que glibc muestra por omisión sin -std estricto: POSIX y extensiones de BSD. |
_GNU_SOURCE | Todo, incluidas las extensiones propias de Linux y glibc. |
El manual de cada función dice cuál necesita, en la parte del SYNOPSIS que vimos en el tema 2:
Feature Test Macro Requirements for glibc (see feature_test_macros(7)):
strdup():
_XOPEN_SOURCE >= 500
|| /* Since glibc 2.12: */ _POSIX_C_SOURCE >= 200809L
|| /* glibc <= 2.19: */ _BSD_SOURCE || _SVID_SOURCE
Hay una regla que no admite excepciones: la macro se define antes del primer #include, de cualquier encabezado del sistema. Los encabezados la consultan una sola vez, al incluirse por primera vez, a través de un encabezado interno, features.h; definirla después no tiene efecto:
$ head -n 3 car3.c
#include <stdio.h>
#include <string.h>
#define _POSIX_C_SOURCE 200809L
$ gcc -std=c17 -Wall -Wextra -o car3 car3.c 2>&1 | grep error
car3.c:7:19: error: implicit declaration of function ‘strdup’; did you mean ‘strcmp’? [-Wimplicit-function-declaration]
…
Por eso los programas del curso que usan funciones de POSIX empiezan con #define _POSIX_C_SOURCE 200809L en la primera línea, como sistema.c en el tema 5 o permisos.c en el tema 8. La alternativa es pasarla con -D_POSIX_C_SOURCE=200809L en la línea de órdenes, lo que la aplica a todos los archivos. La página man 7 feature_test_macros explica todas en detalle.
12. Las macros escondidas en la biblioteca
Muchos nombres de la biblioteca que parecen variables o funciones son en realidad macros. El caso más sorprendente es errno, la variable de error del sistema:
$ grep -n 'define errno' /usr/include/errno.h
38:# define errno (*__errno_location ())
$ gcc -E en.c | tail -n 3 # en.c: int f(void) { return errno; }
(*__errno_location ())
# 2 "en.c"
; }
errno no es una variable global común: es una macro que llama a una función, __errno_location, que devuelve la dirección del errno del hilo actual. Así, cada hilo tiene su propio errno, y el error de una llamada en un hilo no pisa el de otro. Lo veremos en el tema 37, pero ya muestra el poder de las macros: la biblioteca cambió por completo cómo funciona errno sin que ningún programa tuviera que cambiar una línea.
Otros nombres conocidos son macros: EXIT_SUCCESS, NULL, EOF, stdin, assert, WEXITSTATUS del tema 8. Y algunas funciones tienen además una versión en macro, como getc o putchar. Para ver todas las macros definidas en un archivo, después de procesarlo, está gcc -dD -E, que muestra las definiciones junto con el resultado:
$ gcc -dD -E depurar.c | grep -E '^#define (MOSTRAR|CONTADOR|DEPURAR)'
#define MOSTRAR(expr) printf("%s = %ld\n", #expr, (long) (expr))
#define CONTADOR(nombre) static int contador_ ##nombre = 0
#define DEPURAR(...) do { } while (0)
Como DEPURACION no estaba definida, quedó la versión vacía de DEPURAR. Ante una macro que no se entiende, la técnica es siempre la misma: gcc -E para ver el resultado, gcc -dD -E o grep en /usr/include para ver la definición.
13. Macros o funciones
Las macros son la herramienta más antigua de C para evitar repetir código, y durante décadas fueron la única forma de obtener «funciones» sin el costo de una llamada. Hoy el lenguaje tiene alternativas mejores para casi todo, y la recomendación general es usar macros solo para lo que nada más puede hacer:
| Para… | Usa | Por qué |
|---|---|---|
| Una constante entera | enum o #define | enum respeta el alcance; #define sirve también en #if. |
| Una constante de otro tipo | static const o, en C23, constexpr | Tienen tipo y se ven en gdb. |
| Una operación pequeña y rápida | static inline | Comprueba tipos, evalúa cada argumento una vez y es igual de rápida. |
| Compilar según la plataforma o la configuración | #if, #ifdef | Solo el preprocesador puede quitar código antes de compilar. |
| Saber dónde está el código | Una macro con __FILE__ y __LINE__ | Solo una macro se expande en el lugar de la llamada. |
| Usar el texto de una expresión | Una macro con # | Una función solo recibe valores. |
| Proteger un encabezado | Una guarda o #pragma once | Es su función. |
Cuando se escribe una macro con parámetros, la lista de comprobación es corta: paréntesis en cada parámetro y en todo el cuerpo; cada parámetro usado una sola vez, o un aviso en mayúsculas de que no; do { … } while (0) si hay varias sentencias; y un nombre en mayúsculas que advierta al lector.
14. Errores frecuentes
| Error | Qué pasa | Corrección |
|---|---|---|
#define TAM 100; | El punto y coma se copia en cada uso: int v[TAM]; no compila. | Las directivas no llevan punto y coma. |
| Parámetros sin paréntesis | CUADRADO_MAL(a + 1) da 7. | (x) en cada uso. |
| Cuerpo sin paréntesis | 10 * DOBLE_MAL(a) da 33. | Todo el cuerpo entre paréntesis. |
| Argumentos con efectos secundarios | MAXIMO(i++, j) incrementa dos veces. | Una función static inline. |
Varias sentencias sin do { } while (0) | Un if solo controla la primera; con else, no compila. | do { … } while (0). |
| Esperar que una macro respete los bloques | Sigue valiendo hasta el final del archivo. | #undef, o una constante con alcance. |
Un nombre mal escrito en #if | Vale 0 sin advertencia. | #ifdef, y -Wundef. |
Efectos secundarios en assert | Desaparecen al compilar con NDEBUG. | Solo condiciones sin efectos. |
Usar assert para errores de ejecución | Con NDEBUG, el error ya no se comprueba. | if y un mensaje de error. |
_POSIX_C_SOURCE después de un #include | No tiene efecto: las funciones siguen ocultas. | En la primera línea, o con -D. |
Demasiados #ifdef repartidos | Código ilegible, con combinaciones nunca probadas. | Concentrar las diferencias en pocos archivos. |
15. Resumen
- El preprocesador trabaja con texto, antes del compilador: incluye archivos, expande macros y elimina bloques condicionales.
gcc -Emuestra el resultado. - Una macro objeto se reemplaza por un texto fijo, vale hasta el final del archivo o un
#undef, y se escribe en mayúsculas. - En una macro con parámetros, los argumentos se pegan como texto: cada parámetro y todo el cuerpo van entre paréntesis.
- Un parámetro usado dos veces evalúa dos veces su argumento; para eso se usa una función
static inline. - Las macros de varias sentencias se envuelven en
do { … } while (0)para comportarse como una sola sentencia. #convierte un argumento en cadena;##pega dos tokens.__FILE__,__LINE__y__func__dentro de una macro indican el lugar de la llamada;...y__VA_ARGS__hacen macros variádicas.#if,#ifdef,#ifndef,#elify#elsedeciden qué código se compila; lo excluido no existe en el ejecutable.-Ddefine macros desde la línea de órdenes; con#ifndefse dan valores por omisión, y#errorvalida la configuración.assertcomprueba errores de programación al ejecutar y desaparece conNDEBUG;static_assertcomprueba al compilar.- Las macros de prueba de características, como
_POSIX_C_SOURCE, deciden qué declaraciones muestran los encabezados, y van antes de cualquier#include. errno,NULL,EOFyassertson macros;errnose expande a una llamada que devuelve elerrnode cada hilo.- Las macros se reservan para lo que solo ellas pueden hacer; para lo demás hay
enum,consteinline.
16. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. ¿Por qué CUADRADO_MAL(a + 1), con a igual a 3, vale 7?
Porque el preprocesador pega el texto del argumento en el cuerpo, y x * x se convierte en a + 1 * a + 1. El compilador aplica después la precedencia: la multiplicación primero, 1 * a, y queda 3 + 3 + 1 = 7. Con ((x) * (x)), el argumento se agrupa completo y el resultado es 16.
2. ¿Por qué todo el cuerpo de una macro va entre paréntesis, y no solo los parámetros?
Porque el resultado de la macro también se pega en una expresión ajena, y sin paréntesis se mezcla con los operadores que la rodean. 10 * DOBLE_MAL(a), con x + x, se convierte en 10 * a + a, que la precedencia agrupa como (10 * a) + a.
3. ¿Por qué MAXIMO(i++, j) deja i en 7 si empezó en 5?
Porque el parámetro a aparece dos veces en el cuerpo, y el argumento i++ se pega en los dos lugares: una vez en la comparación y otra en la rama elegida por el operador condicional. Se incrementa dos veces. Una función recibe el valor una sola vez, y no tiene este problema.
4. ¿Para qué sirve do { … } while (0) en una macro?
Para que una macro de varias sentencias se comporte como una sola sentencia que exige un punto y coma. Así funciona correctamente como cuerpo de un if sin llaves, incluso seguido de un else. Con las sentencias sueltas, el if solo controlaría la primera; con llaves solas, el punto y coma que sigue rompería el else.
5. ¿Qué hace #expr en MOSTRAR, y por qué una función no podría hacerlo?
Convierte el texto del argumento en una cadena literal: MOSTRAR(tam / 512) produce "tam / 512" además de evaluar la expresión. Una función recibe solo el valor ya calculado del argumento, no el texto que lo produjo.
6. ¿Por qué __FILE__ y __LINE__ se usan dentro de una macro como DEPURAR y no dentro de una función auxiliar?
Porque se reemplazan por el archivo y la línea donde se expanden. Dentro de una macro, se expanden en el lugar de cada llamada, y muestran dónde se usó DEPURAR. Dentro de una función, mostrarían siempre la línea de la función auxiliar.
7. En depurar.c sin -DDEPURACION, ¿qué queda en el ejecutable de las llamadas a DEPURAR?
Nada. Sin DEPURACION, el #else define DEPURAR(...) como do { } while (0), un bloque vacío que el compilador elimina. Los mensajes, sus cadenas y sus llamadas a fprintf no existen en el ejecutable.
8. ¿Cómo permite buffer.c cambiar TAM_BUFFER sin modificar el fuente, y qué pasa con un valor inválido?
Define TAM_BUFFER dentro de un #ifndef TAM_BUFFER: si se compiló con -DTAM_BUFFER=65536, la macro ya existe y el valor por omisión se ignora. Después, un #if TAM_BUFFER < 512 con #error detiene la compilación con un mensaje claro si el valor es demasiado chico.
9. ¿Por qué afirmar compilado con -DNDEBUG terminó con Floating point exception?
Porque con NDEBUG definida, assert se expande a nada, y la comprobación n > 0 desapareció. La función llegó a dividir por n, que valía 0, y la división entera por cero produjo SIGFPE. Es la razón por la que assert no sirve para validar datos de ejecución.
10. ¿Por qué #define _POSIX_C_SOURCE 200809L no funciona si se escribe después de #include <string.h>?
Porque los encabezados de glibc consultan las macros de prueba de características al incluirse por primera vez, y deciden en ese momento qué declaraciones mostrar. Si la macro se define después, el encabezado ya se procesó sin ella, y las guardas de inclusión impiden que se procese de nuevo. Debe definirse antes de cualquier #include, o con -D.
11. ¿Qué es en realidad errno, y por qué está hecho así?
Una macro que se expande a (*__errno_location ()): una llamada a una función que devuelve la dirección del errno del hilo que la llama. Así cada hilo tiene su propio errno, y los errores de un hilo no pisan los de otro, sin que los programas tengan que cambiar la forma en que usan errno.
12. ¿Cuándo conviene una macro en lugar de una función static inline?
Cuando hace falta algo que solo el preprocesador puede hacer: compilar código según condiciones, usar el texto de un argumento con #, construir nombres con ##, o capturar __FILE__ y __LINE__ del lugar de la llamada. Para una operación común, la función es mejor: comprueba tipos, evalúa cada argumento una vez y, optimizada, es igual de rápida.
Para practicar
- Escribe una macro
ABS(x)correcta, y comprueba congcc -Esu expansión paraABS(a - b)y-ABS(a). Luego escribe la versiónstatic inliney compara los resultados conABS(i++). - Escribe una macro
CANTIDAD(arreglo)que calcule la cantidad de elementos de un arreglo consizeof. Úsala dentro de una función que recibe el arreglo como parámetro: ¿qué advierte el compilador, y por qué, según el tema 11? - Agrega a
depurar.cun nivel de detalle: compila con-DDEPURACION=2y haz que una macroDEPURAR2solo muestre mensajes si el nivel es al menos 2, usando#if DEPURACION >= 2. - Reescribe
LOG(fmt, ...)con__VA_OPT__para C23, y prueba qué pasa conLOG("inicio")si la escribes sin__VA_OPT__, con-std=c17. - Agrega a
tamano, el proyecto del tema 13, unstatic_assertque compruebe queoff_ttiene 8 bytes, y unassertenformato_tamanoque verifique quedestinono esNULLy quetames mayor que 0. - Busca en
/usr/include/stdio.hla definición destdiny en/usr/include/x86_64-linux-gnu/bits/waitstatus.hlas deWIFEXITEDyWIFSIGNALED. Explica cada una con lo visto en este tema y en el tema 8. - Compila
caracteristicas.ccon-std=gnu17sin ninguna macro de prueba. ¿Por qué compila? Consultaman 7 feature_test_macrospara encontrar la respuesta.