Preprocesador C

Preprocesador C/C++ ( ing.  preprocesador , preprocesador): un programa que prepara el código del programa en C / C++ para la compilación .

Funciones básicas del preprocesador

El preprocesador hace lo siguiente:

La compilación condicional le permite elegir qué código compilar en función de:

Pasos del preprocesador:

El lenguaje del preprocesador C/C++ no está completo en Turing, aunque solo sea porque es imposible hacer que el preprocesador se cuelgue usando directivas. Ver función recursiva (teoría de la computabilidad) .

Sintaxis de directivas

Una directiva de preprocesador (línea de comando) es una línea en el código fuente que tiene el siguiente formato: #ключевое_слово параметры:

Lista de palabras clave:

Descripción de directivas

Inserción de archivos (#include)

Cuando se encuentran directivas #include "..."y #include <...>, donde "..." es un nombre de archivo, el preprocesador lee el contenido del archivo especificado, ejecuta directivas y sustituciones (sustituciones), reemplaza la directiva #includecon una directiva #liney el contenido del archivo procesado.

Para #include "..."buscar un archivo, se realiza en la carpeta actual y las carpetas especificadas en la línea de comando del compilador. Para #include <...>buscar un archivo, se realiza en carpetas que contienen archivos de biblioteca estándar (las rutas a estas carpetas dependen de la implementación del compilador).

Si se encuentra una directiva que #include последовательность-лексем no coincide con ninguna de las formas anteriores, considera la secuencia de tokens como texto, que, como resultado de todas las sustituciones de macros, debe dar #include <...>o #include "...". La directiva generada de esta manera se interpretará posteriormente de acuerdo con el formulario recibido.

Los archivos incluidos normalmente contienen:

La directiva #includegeneralmente se especifica al principio del archivo (en el encabezado), por lo que los archivos incluidos se denominan archivos de encabezado .

Un ejemplo de inclusión de archivos de la biblioteca estándar de C.

#include <math.h> // incluye declaraciones de funciones matemáticas #include <stdio.h> // incluye declaraciones de funciones de E/S

El uso de un preprocesador se considera ineficiente por las siguientes razones:

  • cada vez que se incluyen archivos, se ejecutan directivas y sustituciones (sustituciones); el compilador podría almacenar los resultados del preprocesamiento para uso futuro;
  • las inclusiones múltiples del mismo archivo deben evitarse manualmente mediante directivas de compilación condicional; el compilador podría hacer esta tarea por sí mismo.

A partir de la década de 1970 comenzaron a aparecer métodos que reemplazaban la inclusión de archivos. Los lenguajes Java y Common Lisp usan paquetes (keyword package) (ver paquete en Java ),  Pascal usa el inglés.  units (palabras clave unity uses), en módulos Modula , OCaml , Haskell y Python  . Diseñado para reemplazar los lenguajes C y C++ , D usa las palabras clave y . moduleimport

Constantes y Macros #define

Las constantes del preprocesador y las macros se utilizan para definir pequeñas piezas de código.

// constante #define BUFFER_SIZE ( 1024 ) // macro #define NUMBER_OF_ARRAY_ITEMS(arreglo) (tamaño(arreglo) / tamaño(*(arreglo)))

Cada constante y cada macro se sustituye por su correspondiente definición. Las macros tienen parámetros similares a funciones y se utilizan para reducir la sobrecarga de las llamadas a funciones en los casos en que la pequeña cantidad de código que llama la función es suficiente para causar un impacto notable en el rendimiento.

Ejemplo. Definición de la macro max , que toma dos argumentos: a y b .

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

Una macro se llama como cualquier función.

z = máx ( x , y );

Después de reemplazar la macro, el código se verá así:

z = ( ( x ) > ( y ) ? ( x ) : ( y ) );

Sin embargo, junto con las ventajas de usar macros en lenguaje C, por ejemplo, para definir tipos de datos genéricos o herramientas de depuración, también reducen un poco la eficiencia de su uso e incluso pueden dar lugar a errores.

Por ejemplo, si f y g  son dos funciones, la llamada

z = máx ( f (), g () );

no evaluará f() una vez y g() una vez , y pondrá el valor más grande en z , como es de esperar. En su lugar, una de las funciones se evaluará dos veces. Si una función tiene efectos secundarios, es probable que su comportamiento sea diferente al esperado.

Las macros de C pueden ser como funciones, creando una nueva sintaxis hasta cierto punto, y también se pueden aumentar con texto arbitrario (aunque el compilador de C requiere que el texto esté en código C sin errores o formateado como un comentario), pero tienen algunas limitaciones. como estructuras de software. Las macros similares a funciones, por ejemplo, se pueden llamar como funciones "reales", pero una macro no se puede pasar a otra función usando un puntero, porque la macro en sí no tiene dirección.

Algunos lenguajes modernos no suelen utilizar este tipo de metaprogramación utilizando macros como terminaciones de cadenas de caracteres, confiando en el cableado automático o manual de funciones y métodos, sino en otras formas de abstracción como plantillas , funciones genéricas o polimorfismo paramétrico . En particular, las funciones en línea una de las principales deficiencias de las macros en las versiones modernas de C y C++, ya que una función en línea brinda la ventaja de las macros al reducir la sobrecarga de una llamada de función, pero su dirección se puede pasar en un puntero para indirecta. llama o se utiliza como parámetro. Asimismo, el problema de las evaluaciones múltiples mencionado anteriormente en la macro max es irrelevante para las funciones integradas.

Puede reemplazar #define constantes con enumeraciones y macros con funciones inline.

Operadores # y ##

Estos operadores se utilizan al crear macros. El operador # antes de un parámetro de macro lo encierra entre comillas dobles, por ejemplo:

#define make_str( bar ) # bar printf ( make_str ( 42 ) );

preprocesador se convierte en:

imprimirf ( "42" );

El operador ## en las macros concatena dos tokens, por ejemplo:

#define MarcarPosición( x ) x##X, x##Y, x##Ancho, x##Altura int MarcarPosición ( Objeto );

preprocesador se convierte en:

int ObjectX , ObjectY , ObjectWidth , ObjectHeight ; Descripción formal de macro sustituciones

1) La línea de control del siguiente formulario obliga al preprocesador a reemplazar el identificador con una secuencia de tokens en el resto del texto del programa:

#define identificador token_sequence

En este caso, se descartan los espacios en blanco al principio y al final de la secuencia de fichas. Una línea #define repetida con el mismo identificador se considera un error si las secuencias de tokens no son idénticas (las discrepancias en los espacios en blanco no importan).

2) Una cadena de la siguiente forma, donde no debe haber espacios en blanco entre el primer identificador y el paréntesis de apertura, es una definición de macro con parámetros especificados por la lista de identificadores.

#define identificador(lista_de_identificadores) secuencia_de_tokens

Como en la primera forma, los caracteres de espacio en blanco al principio y al final de la secuencia del token se descartan, y la macro solo se puede redefinir con la misma lista de parámetros de número y nombre y la misma secuencia del token.

Una línea de control como esta le dice al preprocesador que "olvide" la definición dada al identificador:

identificador #undef

Aplicar la directiva #undef a un identificador previamente no definido no se considera un error.

{

  • Si la definición de macro se especificó en la segunda forma, entonces cualquier otra cadena de caracteres en el texto del programa, que consta de un identificador de macro (posiblemente seguido de espacios en blanco), un paréntesis de apertura, una lista de tokens separados por comas y un paréntesis de cierre, constituye una macro invocación.
  • Los argumentos de llamadas a macros son tokens separados por comas, y las comas entre comillas o paréntesis anidados no participan en la separación de argumentos.
  • (!) Al agrupar argumentos, la expansión de macros no se realiza en ellos.
  • El número de argumentos en la llamada de macro debe coincidir con el número de parámetros de definición de macro.
  • Después de extraer los argumentos del texto, los espacios en blanco que los rodean se descartan.
  • Luego, en la secuencia de reemplazo de tokens de macro, cada parámetro de identificador sin comillas se reemplaza por el argumento real correspondiente del texto.
  • (!) Si el parámetro no está precedido por el signo # en la secuencia de reemplazo, y ni antes ni después está el signo ##, entonces se verifica la presencia de llamadas a macro en los tokens de argumento; si los hay, entonces se realiza en él la expansión de las macros correspondientes antes de que se sustituya el argumento.

El proceso de sustitución se ve afectado por dos signos de operadores especiales.

  • Primero, si un parámetro en una cadena de reemplazo de tokens está precedido por un signo #, entonces se colocan comillas de cadena (") alrededor del argumento correspondiente y luego el identificador del parámetro, junto con el signo #, se reemplaza por el literal de cadena resultante. .
    • Se inserta automáticamente una barra invertida antes de cada carácter " o \ que aparece alrededor o dentro de una cadena o una constante de carácter.
  • En segundo lugar, si una secuencia de tokens en una definición de macro de cualquier tipo contiene el carácter ##, inmediatamente después de la sustitución del parámetro, se descarta junto con los espacios en blanco que lo rodean, por lo que los tokens adyacentes se concatenan, formando así una nueva ficha.
    • El resultado no está definido cuando se generan tokens de idioma no válidos de esta manera, o cuando el texto resultante depende del orden en que se aplica la operación ##.
    • Además, el carácter ## no puede aparecer ni al principio ni al final de una secuencia de fichas de sustitución.

}

  • (!) En macros de ambos tipos, la secuencia de reemplazo de tokens se vuelve a escanear en busca de nuevos identificadores definidos.
  • (!) Sin embargo, si algún identificador ya ha sido reemplazado en el proceso de expansión actual, la reaparición de dicho identificador no hará que sea reemplazado; permanecerá intacto.
  • (!) Incluso si la línea de llamada de macro expandida comienza con el signo #, no se tomará como una directiva de preprocesador.

Un signo de exclamación (!) marca las reglas responsables de la invocación recursiva y las definiciones.

Ejemplo de expansión de macros #definir gato( x, y ) x ## y

La llamada de macro "cat(var, 123)" será reemplazada por "var123". Sin embargo, llamar a "cat(cat(1, 2), 3)" no producirá el resultado deseado. Considere los pasos del preprocesador:

0: gato (gato (1, 2), 3) 1: gato (1, 2) ## 3 2: gato (1, 2) 3

La operación "##" impidió la expansión adecuada de los argumentos de la segunda llamada "gato". El resultado es la siguiente cadena de tokens:

gato ( 1 , 2 ) 3

donde ")3" es el resultado de concatenar el último token del primer argumento con el primer token del segundo argumento, no es un token válido.

Puede especificar el segundo nivel de macro de la siguiente manera:

#define xcat( x, y ) cat( x, y )

La llamada "xcat(xcat(1, 2), 3)" será reemplazada por "123". Considere los pasos del preprocesador:

0: xgato( xgato( 1, 2 ), 3 ) 1: gato( xgato( 1, 2 ), 3 ) 2: gato( gato( 1, 2 ), 3 ) 3: gato (1 ## 2, 3) 4: gato (12, 3) 5:12##3 6:123

Todo salió bien, porque el operador "##" no participó en la expansión de la macro "xcat".

Muchos analizadores estáticos no pueden procesar las macros correctamente, por lo que se reduce la calidad del análisis estático. .

Constantes predefinidas #define

Constantes generadas automáticamente por el preprocesador:

  • __LINE__se reemplaza por el número de línea actual; el número de línea actual puede ser anulado por la directiva #line; utilizado para la depuración ;
  • __FILE__se reemplaza por el nombre del archivo; el nombre del archivo también se puede anular con el #line;
  • __FUNCTION__se reemplaza por el nombre de la función actual;
  • __DATE__se reemplaza por la fecha actual (en el momento en que el preprocesador procesa el código);
  • __TIME__se reemplaza por la hora actual (en el momento en que el preprocesador procesó el código);
  • __TIMESTAMP__se reemplaza por la fecha y hora actuales (en el momento en que el preprocesador procesó el código);
  • __COUNTER__se reemplaza por un número único a partir de 0; después de cada reemplazo, el número aumenta en uno;
  • __STDC__se reemplaza por 1 si la compilación está de acuerdo con el estándar del lenguaje C;
  • __STDC_HOSTED__definido en C99 y superior; se reemplaza por 1 si la ejecución está bajo el control del sistema operativo ;
  • __STDC_VERSION__definido en C99 y superior; para C99 se reemplaza por el número 199901, y para C11 se reemplaza por el número 201112;
  • __STDC_IEC_559__definido en C99 y superior; la constante existe si el compilador admite operaciones de punto flotante IEC 60559;
  • __STDC_IEC_559_COMPLEX__definido en C99 y superior; la constante existe si el compilador admite operaciones de números complejos IEC 60559; el estándar C99 obliga a soportar operaciones con números complejos;
  • __STDC_NO_COMPLEX__definido en C11; se reemplaza por 1 si no se admiten operaciones con números complejos;
  • __STDC_NO_VLA__definido en C11; reemplazado por 1 si las matrices de longitud variable no son compatibles; las matrices de longitud variable deben admitirse en C99;
  • __VA_ARGS__definido en C99 y le permite crear macros con un número variable de argumentos.

Compilación condicional

El preprocesador C proporciona la capacidad de compilar con condiciones. Esto permite la posibilidad de diferentes versiones de un mismo código. Por lo general, este enfoque se usa para personalizar el programa para la plataforma del compilador, el estado (el código depurado se puede resaltar en el código resultante) o la capacidad de verificar la conexión del archivo exactamente una vez.

En general, el programador necesita usar una construcción como:

# ifndef FOO_H # define FOO_H ...( código de archivo de encabezado )... # terminara si

Esta "protección de macros" evita que un archivo de encabezado se incluya dos veces al verificar la existencia de esa macro, que tiene el mismo nombre que el archivo de encabezado. La definición de la macro FOO_H se produce cuando el preprocesador procesa por primera vez el archivo de encabezado. Luego, si se vuelve a incluir este archivo de encabezado, FOO_H ya está definido, lo que hace que el preprocesador omita todo el texto de este archivo de encabezado.

Se puede hacer lo mismo incluyendo la siguiente directiva en el archivo de encabezado:

# pragma una vez

Las condiciones del preprocesador se pueden especificar de varias maneras, por ejemplo:

#ifdef x ... #más ... # terminara si

o

#ifx ... #más ... # terminara si

Este método se usa a menudo en los archivos de encabezado del sistema para probar varias capacidades, cuya definición puede variar según la plataforma; por ejemplo, la biblioteca Glibc utiliza macros de verificación de características para verificar que el sistema operativo y el hardware las admitan (las macros) correctamente mientras mantienen la misma interfaz de programación.

La mayoría de los lenguajes de programación modernos no aprovechan estas características, confiando más en las sentencias condicionales tradicionales if...then...else..., dejando al compilador con la tarea de extraer código inútil del programa que se está compilando.

Dígrafos y trigrafos

Ver dígrafos y trigrafos en lenguajes C/C++.

El preprocesador procesa los dígrafos “ %:” (“ #”), “ %:%:” (“ ##”) y los trígrafos “ ??=” (“ #”), “ ??/” (“ \”).

El preprocesador considera que la secuencia " %:%: " son dos tokens al procesar el código C y un token al procesar el código C++.

Véase también

Notas

Enlaces