Preprocesador de C

De Wikipedia, la enciclopedia libre
Saltar a: navegación, búsqueda

El preprocesador de C (cpp) es el preprocesador para el lenguaje de programación C. Es el primer programa invocado por el compilador y procesa directivas como #include, #define y #if. Estas directivas no son específicas de C. En realidad pueden ser usadas con cualquier tipo de archivo.

El preprocesador utiliza 4 etapas denominadas Fases de traducción. Aunque alguna implementación puede elegir hacer alguna o todas las fases simultáneamente, debe comportarse como si fuesen ejecutadas paso a paso.

Fases[editar]

  1. Tokenizado léxico - El preprocesador reemplaza la secuencia de trigrafos por los caracteres que representan.
  2. Empalmado de líneas - Las líneas de código que continúan con secuencias de escape de nueva línea son unidas para formar líneas lógicas.
  3. Tokenización - Reemplaza los comentarios por espacios en blanco. Divide cada uno de los elementos a preprocesar por un carácter de separación.
  4. Expansión de macros y gestión de directivas - Ejecuta las líneas con directivas de preprocesado incluyendo las que incluye otros archivos y las de compilación condicional. Además expande las macros. Desde la versión de C de 1999 también gestiona los operadores #Pragma.

Ejemplos[editar]

Esta sección trata con más detalle ejemplos de uso del preprocesador de C. Es crucial que existan buenas prácticas de programación cuando se escriben macros de C. Sobre todo cuando se trabaja en equipo.

Incluyendo archivos[editar]

La forma más común de usar el preprocesador es incluir otro archivo:

#include <stdio.h>
 
int main (void)
{
    printf ("¡Hola Mundo!\n");
    return 0;
}

El preprocesador reemplaza la línea #include <stdio.h> con el archivo de cabecera del sistema con ese nombre. En dicha cabecera se declara, entre otras cosas, la función printf(). Más concretamente donde se pone la directiva #include se sustituirá por el contenido completo del archivo 'stdio.h'.

También puede escribirse usando dobles comillas: #include "stdio.h". Originalmente esta distinción conseguía diferenciar entre los archivos de cabecera del sistema (<>) y los de usuario (""). Los compiladores de C y los entornos de desarrollo actuales disponen de facilidades para indicar dónde se encuentran los distintos archivos de cabecera. Sin embargo se sigue recomendando usar la misma nomenclatura por cuestiones de claridad en el código. La localización de los archivos de cabecera pueden ser indicados desde la línea de comandos o desde un archivo makefile para hacer el código más portable.

Se puede utilizar cualquier extensión para los archivos de cabecera. Pero, por convención, se utiliza la extensión .h para los archivos de cabecera y .c para los archivos que no son incluidos por ningún otro. También pueden encontrarse otras extensiones. Por ejemplo, los archivos con extensión .def suelen ser archivos cuyo contenido está diseñado para ser incluido muchas veces.

#include normalmente obliga a usar protectores de #include o la directiva #pragma once para prevenir la doble inclusión, porque si se incluye más de 1 vez el mismo archivo, (dependiendo del contenido) puede causar que se intente declarar varias veces las mismas funciones o tipos de variable, lo que va a generar un error al compilar, esto se intenta prevenir de la siguiente forma:

#ifndef __ARCHIVO_H__
#define __ARCHIVO_H__
 
/*... declaraciones de funciones, etc. ...*/
 
#endif

Como resultado, al intentar inclurse el archivo por segunda vez, la operación "ifndef" va a dar falso porque __ARCHIVO_H__ ya estaba definido de la primera vez que se incluyó, y a consecuencia se saltea todo el bloque hasta llegar al "endif" que suele estar al final del archivo.

Compilación condicional[editar]

Las directivas #ifdef, #ifndef, #else, #elif y #endif pueden usarse para realizar compilaciones condicionales.

#define __WINDOWS__
 
#ifdef __WINDOWS__
#include <windows.h>
#else
#include <unistd.h>
#endif

La primera línea define una macro __WINDOWS__. Las macros pueden estar definidas por el compilador, se pueden especificar en la línea de comandos al compilador o pueden controlar la compilación del programa desde un archivo makefile.

El código siguiente comprueba si la macro __WINDOWS__ está definida. Si es así, como en el ejemplo, se incluye el archivo <windows.h>, en caso contrario, se incluye <unistd.h>.

Ejemplos de otros usos[editar]

Como el preprocesador puede invocarse independientemente del proceso de compilación de código fuente también puede utilizarse como un preprocesador de propósito general para otros tipos de procesamiento de textos. Ver Preprocesadores de propósito general para ver otros ejemplos.

Definición de macros y expansión[editar]

Hay dos tipos de macros: las que son como objetos y las que son como funciones. Las que se asemejan a funciones toman parámetros mientras que las que se asemejan a objetos no. La forma de definir un identificador como una macro de cada tipo es, respectivamente:

#define <identificador> <lista de tokens a reemplazar>
#define <identificador>(<lista de parámetros>) <lista de tokens a reemplazar>

Hay que tener en cuenta que no hay ningún espacio entre el identificador de la macro y el paréntesis izquierdo.

Cada vez que el identificador aparezca en el código fuente será reemplazado por la lista de tokens. Incluso si está vacía. Los identificadores declarados como funciones solo se reemplazan cuando se invoca con los parámetros con los que se definió la macro.

Las macros tipo objetos se usan normalmente como parte de prácticas de buena programación para crear nombres simbólicos para constantes. Por ejemplo:

#define PI 3.14159

en vez de utilizar el número tal cual en el código.

Un ejemplo de macro actuando como función es:

#define RADAGRA(x) ((x) * 57.29578)

Esta macro define la forma de convertir radianes a grados. Después podemos escribir RADAGRA(34). El preprocesador sustituirá la llamada a la macro por la multiplicación definida antes. Las macros aquí mostradas están escritas en mayúsculas. Esto permite distinguir fácilmente entre macros y funciones compiladas en el código fuente.

Precedencia[editar]

Hay que hacer notar que las macros usan paréntesis alrededor de los argumentos y alrededor de toda la expresión. Si se omite alguno de estos pueden darse efectos no deseados más tarde. Por ejemplo:

  • Sin paréntesis en los argumentos:
  • Macro definida como #define RADAGRA(x) (x * 57.29578)
  • RADAGRA(a + b) se expande como (a + b * 57.29578)
  • Sin parámetros en la expresión:
  • Macro definida como #define RADAGRA(x) (x) * 57.29578
  • 1 / RADAGRA(a) se expande como 1 / (a) * 57.29578

probablemente ninguno de los dos se corresponda con el efecto deseado.

Evaluación múltiple de efectos colaterales[editar]

Otro ejemplo de macros tipo función es:

#define MIN(a,b) ((a)>(b)?(b):(a))

Este ilustra uno de los peligros de usar macros como funciones. Uno de los argumentos (a o b) será evaluado dos veces cuando se llame a la "función". Así que si se evalúa la expresión MIN(++primernumero, segundonumero) el argumento primernumero se incrementará dos veces y no uno como se espera.

Una forma más segura de lograr el mismo objetivo es usar la construcción typeof.

#define max(a,b) \
       ({ typeof (a) _a = (a); \
           typeof (b) _b = (b); \
         _a > _b ? _a : _b; })

Esto logra que los argumentos solo se evalúen una vez. Y, además, que no sea específico del tipo de datos. Esta construcción no es legal en ANSI C. Tanto la palabra clave typeof como el situar sentencias compuestas dentro de los paréntesis son extensiones no estándares implementadas en el compilador GNU C (gcc). Si se usa el compilador gcc se puede resolver el mismo problema usando funciones static inline. Estas son igual de eficientes que sus equivalentes #define. Las funciones inline permiten al compilador comprobar el tipo de los parámetros. En este caso particular puede ser una desventaja porque la función 'max' funciona con distintos tipos de parámetros. Pero, en general, suele ser una ventaja.

Con ANSI C no hay una solución general para afrontar el efecto colateral de los argumentos en macros.

Concatenación de cadenas[editar]

La concatenación de cadenas es una de las características más sutiles, y de la que se puede abusar más fácilmente. Dos argumentos pueden unirse usando el operador ##. Esta característica permite concatenar dos cadenas en el código del preprocesador. Esto se puede usar para construir macros muy elaboradas que actúen casi como las templates de C++. Sin muchos de sus beneficios.

Por ejemplo:

#define MYCASE(_elemento,_id) \
   case _id: \
     _elemento##_##_id=_id;\
   break 
 
  switch(x) {
      MYCASE(widget,23);
  }

La línea MYCASE(widget,23) se expande como case 23: widget_23=23; break. El punto y coma después del paréntesis derecho no se expande pero se convierte en el punto y coma que completa la sentencia break.

Hay que tener en cuenta que el _ entre ## es 'literal' mientras que los que aparecen en los argumentos _id y _elemento son los nombres de los 'argumentos' de la macro.

Punto y coma[editar]

Como se ve en el ejemplo anterior el punto y coma de la última línea de la definición de la macro se omite y la macro parece 'natural' cuando se escribe. Se puede incluir en la definición de la macro pero entonces nos encontraremos con líneas en el código sin punto y coma al final. Esto puede despistar a cualquiera que lea el código. O, aún peor, el usuario puede estar tentado a incluir puntos y coma de todas formas. En la mayoría de las ocasiones puede ser inofensivo (un punto y coma adicional denota una sentencia vacía) pero puede causar errores en los bloques de control de flujo:

#define PRINT_FORMATEADO(s) \
   printf ("Mensaje: \"%s\"\n", s);
 
  if (n < 10)
    PRINT_FORMATEADO("n es menor que 10");
  else
    PRINT_FORMATEADO("n es como mínimo 10");

Esto se expande en dos sentencias. La que se prentende printf y una sentencia vacía en cada rama de la construcción if/else. Esto hace que el compilador de un mensaje de error similar a este:

error: expected expression before ‘else’

gcc 4.1.1

Líneas múltiples[editar]

Las macros pueden extenderse tantas líneas como hagan falta. Para ello tan solo habrá que terminar cada línea con una raya a la izquierda (\). La macro terminará en la última línea sin una raya a la izquierda.

Adecuadamente usadas las macros con varias líneas pueden reducir mucho el tamaño y la complejidad del código fuente de un programa en C. Así se aumenta la legibilidad y mantenibilidad del código.

Entrecomillando los argumentos de las macros[editar]

Aunque una cadena pasada a una macro no esté encerrada entre comillas se puede tratar como si lo estuviese usando la directiva "#". Por ejemplo en la macro:

#define ENTRECOMILLAR(x) #x

el código

printf("%s\n", ENTRECOMILLAR(1+2));

se expandirá como

printf("%s\n", "1+2");

Esta capacidad puede usarse con la concatenación de cadenas automáticas para depurar macros. Como en el ejemplo siguiente:

#define dumpme(x, fmt) printf("%s:%u: %s=" fmt, __FILE__, __LINE__, #x, x)
 
int alguna_funcion() {
    int foo;
    /* [aquí va un montón de código complicado] */
    dumpme(foo, "%d");
    /* [y aquí más código complicado] */
}

imprimirá el nombre de la expresión y su valor así como el nombre del archivo y la línea donde se ejecuta.

Macros variables[editar]

El ANSI C no permite macros que tengan un número variable de argumentos. Pero esta opción fue introducida por varios compiladores y se estandarizó en C99. Las macros variables son particularmente útiles cuando escribimos envolturas para printf. Por ejemplo para mostrar mensajes de advertencia o error.

Macros X[editar]

Un patrón de uso poco conocido del preprocesador de C se conoce como "Macros X". Son la práctica de usar varias veces la directiva #include en el mismo archivo de cabecera. Cada vez en un entorno diferente de definición de macros.

Archivo: comandos.def
 
COMANDO(ADD, "Comando para añadir")
COMANDO(SUB, "Comando para restar")
COMANDO(XOR, "Comando O-Exclusivo")
enum command_indices {
#define COMANDO(nombre, descripcion)		COMANDO_##nombre ,
#include "commandos.def"
#undef COMANDO
    NUMERO_COMANDOS /* El número de comandos definidos */
};
 
char *descripciones_comandos[] = {
#define COMMANDO(nombre, descripcion)		descripcion ,
#include "comandos.def"
#undef COMANDO
    NULL
};
 
resultado_t gestor_ADD (estado_t *)
{
  /* código para ADD aquí */
}
 
resultado_t gestor_SUB (estado_t *)
{
  /* código para SUB aquí */
}
 
resultado_t gestor_XOR (estado_t *)
{
  /* código para XOR aquí */
}
 
typedef resultado_t (*gestor_comando_t)(estado_t *);
 
gestor_comando_t gestores_comandos[] = {
#define COMANDO(nombre, descripcion)		&gestor_##nombre,
#include "comandos.def"
#undef COMANDO
    NULL
};

Cuando se quiera añadir un nuevo comando tan solo habrá que modificar el archivo de cabecera donde hemos definido la Macro X (normalmente con la extensión .def) y definir un nuevo gestor para ese comando. La lista de descripciones, la lista de gestores y la enumeración se actualizarán automáticamente por el preprocesador. Sin embargo en muchos casos esta automatización no es posible.

Errores y advertencias de compilación definidas por el usuario[editar]

La directiva #error inserta un mensaje de error en la salida del compilador.

#error "¡ERROR GRAVISIMO!"

Esto muestra "¡ERROR GRAVISIMO!" en la salida del compilador y para la compilación en este punto. Esto es muy útil si no sabes si una línea es compilada o no. También es útil cuando tienes el código muy parametrizado y quieres saber si un #define particular se ha introducido desde el makefile. Como aquí:

#ifdef WINDOWS
    ... /* código específico de Windows */
#elif defined(UNIX)
    ... /* código específico de Unix */
#else
    #error "¿ Cuál es tu Sistema Operativo ?"
#endif

También se pueda usar la directiva #warning para mostrar mensajes en la salida del compilador sin para el proceso. Un uso para esto es mostrar código antiguo que no debería utilizarse pero se hace por razones de compatibilidad.

#warning "ABC está obsoleto. Use XYZ en su lugar."

Visual Studio.NET no soporta esta directiva y debe usarse la directiva #pragma message.

Aunque el texto de las directivas #error y #warning no tiene que estar entrecomillado es una buena práctica usarlo. Si no se usan, se pueden encontrar errores con los apostrofes y otros caracteres que puede intentar interpretar el preprocesador.

Características del procesador específicas del compilador[editar]

Las directivas #pragma son directivas específicas del compilador. Cada compilador puede usarlas para lo que crea necesario. Por ejemplo para suprimir un mensaje de error específico.

Estándar de colocación de macros[editar]

Algunos símbolos están predefinidos en ANSI C. Los más usuales son __FILE__ y __LINE__ que se expande como el nombre del archivo y la línea actual respectivamente.

// depurando macros para poder ver de dónde proviene un mensaje.
#define CADENADONDE "[archivo %s, línea %d] "
#define ARGUMENTOSDONDE __FILE__,__LINE__
 
printf (CADENADONDE ": mira, x=%d\n", ARGUMENTOSDONDE, x);

Esto muestra el valor de x precedido por el archivo y el número de línea. Hay que tener en cuenta que el argumento CADENADONDE se concatena con la cadena que lo sigue.

Macros específicas del compilador predefinidas[editar]

Normalmente hay que revisar la documentación específica del compilador. O ver el proyecto[1] que mantiene una lista con las particularidades de cada compilador. También se puede hacer que el compilador nos diga algunas macros predefinidas útiles:

  • Para el compilador GNU C:
    gcc -dM -E - < /dev/null
    
  • Para el compilador ANSI C de HP-UX donde fred.c es un archivo de prueba cualquiera:
    cc -v fred.c
    

Véase también[editar]

Referencias[editar]

  1. «Pre-defined Compiler Macros» (en inglés). Consultado el 29 de agosto de 2013.

Enlaces externos[editar]