El preprocesador (parte 2)

En el segundo episodio de la T2 del Tutorial de C, hablamos de macros, de condicionales en tiempo de compilación mediante otras directivas del preprocesador, y de la desconocida directiva error.

En el mundo de la programación en C, el preprocesador es una herramienta poderosa que nos permite optimizar y controlar el código antes de que llegue al compilador. Una de las funcionalidades más interesantes que podemos aprovechar son las macros parametrizadas, que nos ofrecen una forma eficiente de realizar sustituciones de código sin la sobrecarga que implica una función tradicional.

Imaginemos que queremos calcular el área de un rectángulo. Normalmente, escribiríamos una función que reciba el ancho y el alto, y que devuelva el producto de ambos. Sin embargo, en sistemas con procesadores limitados, como dispositivos empotrados o máquinas con recursos muy reducidos, llamar a una función puede suponer una sobrecarga significativa. Esto se debe a que cada llamada implica pasar parámetros, gestionar la pila y cambiar zonas de memoria, lo que puede ralentizar el programa.

Aquí es donde las macros parametrizadas entran en juego. Podemos definir una macro con #define que reciba parámetros, sin necesidad de especificar tipos, y que simplemente sustituya el código en el lugar donde se invoque. Por ejemplo, para calcular el área de un rectángulo, podríamos definir:

#define AREA_RECTANGULO(X, Y) ((X) * (Y))

Cuando usemos AREA_RECTANGULO(ancho, alto), el preprocesador reemplazará esa llamada por (ancho) * (alto), evitando la llamada a función y mejorando la eficiencia en tiempo de ejecución. Es importante usar paréntesis para asegurar que la sustitución respete el orden correcto de las operaciones.

Además de las macros, el preprocesador nos ofrece condicionales que funcionan en tiempo de compilación, no en tiempo de ejecución. Esto significa que podemos incluir o excluir bloques de código según ciertas condiciones, lo que es muy útil para adaptar el programa a diferentes entornos o configuraciones sin modificar el código fuente principal.

Por ejemplo, si definimos un límite con #define LIMITE 80, podemos usar condicionales para compilar diferentes fragmentos de código según el valor de ese límite:

#if LIMITE < 50
    printf("Límite por debajo de 50\n");
#else
    printf("Límite de 50 o por encima de 50\n");
#endif

El preprocesador evaluará la condición y solo incluirá en la compilación el bloque que corresponda, ignorando el otro. Esto ayuda a reducir el tamaño del ejecutable y a eliminar código innecesario.

También podemos anidar condicionales para crear estructuras más complejas, evaluando múltiples condiciones de forma jerárquica:

#if LIMITE < 100
    printf("Límite por debajo de 100\n");
    #if LIMITE < 50
        printf("Límite por debajo de 50\n");
    #endif
#else
    printf("Límite de 100 o más\n");
#endif

Otra característica muy útil es la posibilidad de comprobar si un identificador ha sido definido o no, usando #if defined o sus atajos #ifdef y #ifndef. Esto nos permite incluir código solo si ciertas macros están definidas, facilitando la gestión de versiones o características opcionales.

Por ejemplo, si definimos #define PREMIUM, podemos escribir:

#ifdef PREMIUM
    printf("Estás usando la versión premium\n");
#else
    printf("Estás usando la versión gratuita\n");
#endif

Si PREMIUM está definido, se compilará el primer bloque; si no, el segundo. Aunque #ifdef es muy común, en algunos compiladores como GCC puede no estar completamente soportado, por lo que usar #if defined es una opción más segura:

#if defined(PREMIUM)
    printf("Estás usando la versión premium\n");
#else
    printf("Estás usando la versión gratuita\n");
#endif

De forma similar, #ifndef nos permite incluir código solo si una macro no está definida:

#ifndef SUPERPREMIUM
    printf("No tienes la superpremium\n");
#endif

Es importante tener en cuenta que estas directivas se resuelven en tiempo de compilación, por lo que no podemos cambiar su comportamiento en tiempo de ejecución. Esto implica que cualquier error o mal uso puede generar bugs difíciles de depurar, por lo que debemos manejar el preprocesador con cuidado.

Además, el preprocesador nos permite forzar errores de compilación con la directiva #error, lo que puede ser útil para detectar configuraciones incorrectas o condiciones no deseadas:

#error No compiles esto

Si el compilador encuentra esta línea, detendrá la compilación y mostrará el mensaje, ayudándonos a controlar mejor el proceso de construcción del programa.

En definitiva, el preprocesador de C nos ofrece herramientas para optimizar el código, controlar qué partes se compilan y adaptar el programa a diferentes escenarios, siempre con la precaución de que estas decisiones se toman antes de la compilación y no pueden modificarse en tiempo de ejecución. Esto es especialmente valioso en entornos con recursos limitados o cuando queremos mantener un código limpio y eficiente.

Lista de reproducción
  1. 1
    Instalar CodeBlocks
    15 minutos
  2. 2
    Funciones y hola mundo
    17 minutos
  3. 3
    Variables y tipos de datos
    17 minutos
  4. 4
    Condicionales y operadores lógicos
    16 minutos
  5. 5
    Bucles
    11 minutos
  6. 6
    Punteros
    12 minutos
  7. 7
    Arrays
    14 minutos
  8. 8
    Estructuras
    12 minutos
  9. 9
    Otras construcciones de C
    9 minutos
  10. 10
    Memoria dinámica
    8 minutos
  11. 11
    El preprocesador (parte 1)
    10 minutos
  12. 12
    El preprocesador (parte 2)
    9 minutos
  13. 13
    Archivos de cabecera y múltiples .c (parte 1)
    8 minutos
  14. 14
    Archivos de cabecera y múltiples .c (parte 2)
    9 minutos
  15. 15
    C desde la línea de comandos (parte 1)
    8 minutos
  16. 16
    C desde la línea de comandos (parte 2)
    9 minutos
  17. 17
    Break y continue
    10 minutos
  18. 18
    Goto
    13 minutos
  19. 19
    Manipulación de bits
    15 minutos
  20. 20
    Máscaras de bit
    19 minutos
  21. 21
    Archivos (1): fopen y fclose
    13 minutos
  22. 22
    Archivos (2): leer con fgetc
    9 minutos
  23. 23
    Archivos (3): fseek y ftell
    11 minutos
  24. 24
    Archivos (4): leer con fgets
    9 minutos
  25. 25
    Archivos (5): fputc y fputs
    7 minutos
  26. 26
    Archivos (6): volcar en archivos con fwrite
    10 minutos
  27. 27
    Archivos (7): fread, fwrite y los arrays
    10 minutos
  28. 28
    Archivos (8): entrada estándar y salida estándar
    9 minutos
  29. 29
    Archivos (9): buffers
    14 minutos
  30. 30
    Archivos (y 10): otras funciones útiles con archivos
    5 minutos
  31. 31
    printf (1)
    18 minutos
  32. 32
    printf (parte 2)
    12 minutos
  33. 33
    scanf (parte 1)
    17 minutos
  34. 34
    scanf (parte 2)
    17 minutos
  35. 35
    fprintf, sprintf y snprintf
    8 minutos
  36. 36
    Tipos de datos opacos
    13 minutos
  37. 37
    Bibliotecas estáticas
    13 minutos
  38. 38
    Bibliotecas dinámicas
    15 minutos
  39. 39
    Más flags: i mayúscula (include), wall, werror, pedantic...
    12 minutos
  40. 40
    pkg-config
    12 minutos
  41. 41
    Make
    17 minutos
  42. 42
    GDB
    21 minutos
  43. 43
    Variables globales
    6 minutos
  44. 44
    extern
    9 minutos
  45. 45
    Funciones variádicas
    12 minutos
  46. 46
    El optimizador de GCC y la opción -O
    12 minutos
  47. 47
    Volatile
    6 minutos