← Programación en C para Sistemas Operativos Bloque 3 · Funciones y organización · Tema 14 de 42

Tema 14

El preprocesador

El preprocesador apareció en casi todos los temas anteriores, y casi siempre con una promesa: «lo veremos en el tema 14». La guarda de inclusión del tema 13, el do { … } while (0) del tema 9, los paréntesis alrededor de las banderas del tema 8, _POSIX_C_SOURCE y -DLIMITE del tema 4. Este tema, que cierra el bloque 3, salda esas deudas. El preprocesador es una herramienta poderosa y peligrosa a la vez, porque trabaja con texto y no entiende C: reemplaza palabras sin saber de tipos, de precedencias ni de alcances. Veremos cómo funcionan sus macros, cada una de sus trampas con su solución, cómo compilar partes del programa según la plataforma o la configuración, y cómo las macros de la biblioteca deciden qué funciones ve nuestro programa.

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:

TrabajoDirectivasEjemplo
Incluir archivos#includeCopiar stdio.h en el fuente (temas 3 y 13).
Definir macros#define, #undefReemplazar TAM_BUFFER por 4096.
Compilar según condiciones#if, #ifdef, #ifndef, #elif, #else, #endifIncluir un bloque solo en Linux, o solo al depurar.
Informar y controlar#error, #warning, #pragma, #lineDetener 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:

macros objeto
#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:

terminal · rd.c: #define TAM 10 y luego #define TAM 20
$ 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:

parentesis.c
#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;
}
terminal · el resultado, y lo que recibió el compilador
$ ./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)));
La expansión de dos macros: CUADRADO_MAL(a + 1) se convierte en a + 1 * a + 1, que por la precedencia vale a + a + 1, es decir 7; CUADRADO(a + 1) se convierte en ((a + 1) * (a + 1)), que vale 16 lo que escribimos lo que recibe el compilador a = 3 CUADRADO_MAL(a + 1) a + 1 * a + 1 7 CUADRADO(a + 1) ((a + 1) * (a + 1)) 16 El preprocesador pega el texto del argumento; la precedencia la aplica después el compilador.
Sin paréntesis, el texto pegado se agrupa según la precedencia del tema 7, no según lo que el programador quiso decir.

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:

  1. Cada parámetro, entre paréntesis, en cada uso: (x), para que un argumento como a + 1 se agrupe completo. parámetros
  2. Todo el cuerpo, entre paréntesis: ((x) * (x)), para que la expresión no se mezcle con lo que la rodea, como en 100 / 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:

doble.c
#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;
}
terminal
$ ./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:

sentencias.c
#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;
}
terminal
$ 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:

depurar.c
#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;
}
terminal · el resultado, y la expansión de MOSTRAR y CONTADOR
$ 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:

NombreValeComentario
__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:

terminal · depurar.c con DEPURACION definida
$ 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.

DirectivaIncluye el bloque si…
#ifdef NOMBREla macro NOMBRE está definida, con cualquier valor.
#ifndef NOMBREla macro no está definida. Es la base de las guardas del tema 13.
#if expresiónla expresión, con enteros y macros, vale distinto de cero. defined(NOMBRE) vale 1 si la macro existe.
#elif expresiónlas condiciones anteriores fallaron y esta vale distinto de cero.
#elsetodas 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:

buffer.c
#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;
}
terminal · con el valor por omisión, con otro, con uno inválido y con C23
$ 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:

afirmar.c
#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;
}
terminal · con assert activo, desactivado con -DNDEBUG, y un static_assert que falla
$ 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:

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:

terminal · caracteristicas.c usa strdup y fileno
$ 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:

MacroHabilita
_POSIX_C_SOURCE 200809LLas interfaces de POSIX.1-2008. La que usamos en el curso.
_XOPEN_SOURCE 700POSIX más las extensiones X/Open (XSI).
_DEFAULT_SOURCELo que glibc muestra por omisión sin -std estricto: POSIX y extensiones de BSD.
_GNU_SOURCETodo, 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:

terminal · man 3 strdup, el bloque de requisitos
   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:

terminal · car3.c: el #define después de los #include
$ 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:

terminal · qué es errno
$ 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:

terminal · las macros de depurar.c, tal como quedaron definidas
$ 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…UsaPor qué
Una constante enteraenum o #defineenum respeta el alcance; #define sirve también en #if.
Una constante de otro tipostatic const o, en C23, constexprTienen tipo y se ven en gdb.
Una operación pequeña y rápidastatic inlineComprueba tipos, evalúa cada argumento una vez y es igual de rápida.
Compilar según la plataforma o la configuración#if, #ifdefSolo el preprocesador puede quitar código antes de compilar.
Saber dónde está el códigoUna macro con __FILE__ y __LINE__Solo una macro se expande en el lugar de la llamada.
Usar el texto de una expresiónUna macro con #Una función solo recibe valores.
Proteger un encabezadoUna guarda o #pragma onceEs 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

ErrorQué pasaCorrecció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éntesisCUADRADO_MAL(a + 1) da 7.(x) en cada uso.
Cuerpo sin paréntesis10 * DOBLE_MAL(a) da 33.Todo el cuerpo entre paréntesis.
Argumentos con efectos secundariosMAXIMO(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 bloquesSigue valiendo hasta el final del archivo.#undef, o una constante con alcance.
Un nombre mal escrito en #ifVale 0 sin advertencia.#ifdef, y -Wundef.
Efectos secundarios en assertDesaparecen al compilar con NDEBUG.Solo condiciones sin efectos.
Usar assert para errores de ejecuciónCon NDEBUG, el error ya no se comprueba.if y un mensaje de error.
_POSIX_C_SOURCE después de un #includeNo tiene efecto: las funciones siguen ocultas.En la primera línea, o con -D.
Demasiados #ifdef repartidosCódigo ilegible, con combinaciones nunca probadas.Concentrar las diferencias en pocos archivos.

15. Resumen

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

  1. Escribe una macro ABS(x) correcta, y comprueba con gcc -E su expansión para ABS(a - b) y -ABS(a). Luego escribe la versión static inline y compara los resultados con ABS(i++).
  2. Escribe una macro CANTIDAD(arreglo) que calcule la cantidad de elementos de un arreglo con sizeof. Ú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?
  3. Agrega a depurar.c un nivel de detalle: compila con -DDEPURACION=2 y haz que una macro DEPURAR2 solo muestre mensajes si el nivel es al menos 2, usando #if DEPURACION >= 2.
  4. Reescribe LOG(fmt, ...) con __VA_OPT__ para C23, y prueba qué pasa con LOG("inicio") si la escribes sin __VA_OPT__, con -std=c17.
  5. Agrega a tamano, el proyecto del tema 13, un static_assert que compruebe que off_t tiene 8 bytes, y un assert en formato_tamano que verifique que destino no es NULL y que tam es mayor que 0.
  6. Busca en /usr/include/stdio.h la definición de stdin y en /usr/include/x86_64-linux-gnu/bits/waitstatus.h las de WIFEXITED y WIFSIGNALED. Explica cada una con lo visto en este tema y en el tema 8.
  7. Compila caracteristicas.c con -std=gnu17 sin ninguna macro de prueba. ¿Por qué compila? Consulta man 7 feature_test_macros para encontrar la respuesta.