stringtranslate.com

Preprocesador C

El preprocesador de C es el preprocesador de macros para varios lenguajes de programación informática , como C , Objective-C , C++ y una variedad de lenguajes Fortran . El preprocesador permite la inclusión de archivos de encabezado , expansiones de macros , compilación condicional y control de línea.

El lenguaje de las directivas del preprocesador está débilmente relacionado con la gramática de C, y por eso a veces se utiliza para procesar otros tipos de archivos de texto . [1]

Historia

El preprocesador se introdujo en C alrededor de 1973 a instancias de Alan Snyder y también en reconocimiento de la utilidad de los mecanismos de inclusión de archivos disponibles en BCPL y PL/I . Su versión original ofrecía únicamente inclusión de archivos y reemplazo simple de cadenas utilizando #includey #definepara macros sin parámetros, respectivamente. Poco después se amplió, primero por Mike Lesk y luego por John Reiser, para incorporar macros con argumentos y compilación condicional. [2]

El preprocesador C fue parte de una larga tradición de lenguajes macro en Bell Labs, iniciada por Douglas Eastwood y Douglas McIlroy en 1959. [3]

Fases

El preprocesamiento se define mediante las primeras cuatro (de ocho) fases de traducción especificadas en el Estándar C.

  1. Reemplazo de trígrafos: el preprocesador reemplaza las secuencias de trígrafos con los caracteres que representan. Esta fase se eliminará en C23 siguiendo los pasos de C++17 .
  2. Empalme de líneas: las líneas de origen físicas que continúan con secuencias de nueva línea con escape se empalman para formar líneas lógicas.
  3. Tokenización: el preprocesador divide el resultado en tokens de preprocesamiento y espacios en blanco . Reemplaza los comentarios con espacios en blanco.
  4. Expansión de macros y manejo de directivas: se ejecutan líneas directivas de preprocesamiento, incluidas la inclusión de archivos y la compilación condicional. El preprocesador expande simultáneamente las macros y, desde la versión de 1999 del estándar C, maneja _Pragmaoperadores.

Incluyendo archivos

Uno de los usos más comunes del preprocesador es incluir otro archivo fuente:

#incluir <stdio.h> int main ( void ) { printf ( "Hola, mundo! \n " ); return 0 ; }    

El preprocesador reemplaza la línea #include <stdio.h>con el contenido textual del archivo 'stdio.h', que declara la printf() función entre otras cosas.

Esto también se puede escribir usando comillas dobles, por ejemplo #include "stdio.h". Si el nombre del archivo está entre corchetes angulares, el archivo se busca en las rutas de inclusión del compilador estándar. Si el nombre del archivo está entre comillas dobles, la ruta de búsqueda se expande para incluir el directorio del archivo fuente actual. Los compiladores de C y los entornos de programación tienen una función que permite al programador definir dónde se pueden encontrar los archivos de inclusión. Esto se puede introducir a través de un indicador de línea de comandos, que se puede parametrizar usando un makefile , de modo que se pueda intercambiar un conjunto diferente de archivos de inclusión para diferentes sistemas operativos, por ejemplo.

Por convención, los archivos incluidos se nombran con una extensión .ho .hpp. Sin embargo, no existe ningún requisito que obligue a respetar esto. Los archivos con una .defextensión pueden indicar archivos diseñados para incluirse varias veces, expandiendo cada vez el mismo contenido repetitivo; #include "icon.xbm"es probable que se refiera a un archivo de imagen XBM (que es al mismo tiempo un archivo fuente C).

#includeA menudo obliga al uso de #includeprotectores o #pragma oncea evitar la doble inclusión.

Compilación condicional

Las directivas if–else#if , #ifdef, #ifndef, #else, #elif, y #endifse pueden usar para la compilación condicional . #ifdefy #ifndefson abreviaturas simples de #if defined(...)y #if !defined(...).

#if VERBOSE >= 2 printf ( "mensaje de seguimiento" ); #endif 

La mayoría de los compiladores que apuntan a Microsoft Windows definen implícitamente _WIN32. [4] Esto permite que el código, incluidos los comandos del preprocesador, se compile solo cuando apunta a sistemas Windows. Algunos compiladores definen WIN32en su lugar. Para aquellos compiladores que no definen implícitamente la _WIN32macro, se puede especificar en la línea de comandos del compilador, utilizando -D_WIN32.

#ifdef __unix__ /* __unix__ suele estar definido por compiladores que apuntan a sistemas Unix */ # include <unistd.h> #elif definido _WIN32 /* _WIN32 suele estar definido por compiladores que apuntan a sistemas Windows de 32 o 64 bits */ # include <windows.h> #endif    

El código de ejemplo prueba si se ha definido una macro __unix__. Si es así, <unistd.h>se incluye el archivo. De lo contrario, prueba si _WIN32se ha definido una macro. Si es así, <windows.h>se incluye el archivo.

Un ejemplo más complejo #ifpuede utilizar operadores; por ejemplo:

#if !(defined __LP64__ ||defined __LLP64__) ||defined _WIN32 && !defined _WIN64 // estamos compilando para un sistema de 32 bits #else // estamos compilando para un sistema de 64 bits #endif

La traducción también puede fallar si se utiliza la #errordirectiva:

#if RUBY_VERSION == 190 #error 1.9.0 no compatible #endif

Definición y expansión de macro

Existen dos tipos de macros: las macros de tipo objeto y las macros de tipo función . Las macros de tipo objeto no toman parámetros, mientras que las macros de tipo función sí (aunque la lista de parámetros puede estar vacía). La sintaxis genérica para declarar un identificador como una macro de cada tipo es, respectivamente:

#define <identificador> <lista de tokens de reemplazo> // macro similar a un objeto #define <identificador>(<lista de parámetros>) <lista de tokens de reemplazo> // macro similar a una función, tenga en cuenta los parámetros

La declaración de macro similar a una función no debe tener ningún espacio en blanco entre el identificador y el primer paréntesis de apertura. Si hay espacios en blanco, la macro se interpretará como similar a un objeto y todo lo que comience a partir del primer paréntesis se agregará a la lista de tokens.

Una definición de macro se puede eliminar con #undef:

#undef <identificador> // eliminar la macro

Siempre que el identificador aparece en el código fuente, se reemplaza con la lista de tokens de reemplazo, que puede estar vacía. En el caso de un identificador declarado como una macro similar a una función, solo se reemplaza cuando el token siguiente también es un paréntesis izquierdo que inicia la lista de argumentos de la invocación de la macro. El procedimiento exacto que se sigue para la expansión de macros similares a funciones con argumentos es sutil.

Las macros similares a objetos se usaban convencionalmente como parte de una buena práctica de programación para crear nombres simbólicos para constantes; por ejemplo:

#define PI 3.14159

en lugar de codificar números de forma rígida en todo el código. Una alternativa tanto en C como en C++, especialmente en situaciones en las que se requiere un puntero al número, es aplicar el constcalificador a una variable global. Esto hace que el valor se almacene en la memoria, en lugar de ser sustituido por el preprocesador. Sin embargo, en el código C++ moderno, se utiliza la constexprpalabra clave, introducida en C++11 :

constexpr doble PI = 3.14159 ;    

Los usos de variables declaradas como constexpr, como macros similares a objetos, pueden reemplazarse con su valor en tiempo de compilación. [5]

Un ejemplo de una macro similar a una función es:

#define RADTODEG(x) ((x) * 57.29578)

Esto define una conversión de radianes a grados que se puede insertar en el código cuando sea necesario; por ejemplo, RADTODEG(34). Esto se expande en el lugar, de modo que la multiplicación repetida por la constante no se muestra en todo el código. La macro aquí está escrita en mayúsculas para enfatizar que es una macro, no una función compilada.

El segundo xse encierra en su propio par de paréntesis para evitar la posibilidad de un orden incorrecto de operaciones cuando se trata de una expresión en lugar de un único valor. Por ejemplo, la expresión se expande correctamente como ; sin paréntesis, da prioridad a la multiplicación.RADTODEG(r + 1)((r + 1) * 57.29578)(r + 1 * 57.29578)

De manera similar, el par de paréntesis externo mantiene el orden correcto de operación. Por ejemplo, se expande a ; sin paréntesis, da prioridad a la división.1 / RADTODEG(r)1 / ((r) * 57.29578)1 / (r) * 57.29578

Orden de expansión

La expansión macro funcional ocurre en las siguientes etapas:

  1. Las operaciones de conversión de cadenas se reemplazan con la representación textual de la lista de reemplazo de sus argumentos (sin realizar expansión).
  2. Los parámetros se reemplazan con su lista de reemplazo (sin realizar expansión).
  3. Las operaciones de concatenación se reemplazan con el resultado concatenado de los dos operandos (sin expandir el token resultante).
  4. Los tokens que se originan a partir de parámetros se expanden.
  5. Los tokens resultantes se expanden de forma normal.

Esto puede producir resultados sorprendentes:

#define HE HI #define LLO _THERE #define HELLO "HOLA" #define CAT(a,b) a##b #define XCAT(a,b) CAT(a,b) #define CALL(fn) fn(HE,LLO) CAT ( HE , LLO ) // "HOLA", porque la concatenación ocurre antes de la expansión normal XCAT ( HE , LLO ) // HI_THERE, porque los tokens que se originan a partir de los parámetros ("HE" y "LLO") se expanden primero CALL ( CAT ) // "HOLA", porque esto evalúa a CAT(a,b)     

Macros y directivas especiales

Se requiere que una implementación defina ciertos símbolos durante el preprocesamiento. Entre ellos se incluyen __FILE__y __LINE__, predefinidos por el propio preprocesador, que se expanden al archivo y número de línea actuales. Por ejemplo, los siguientes:

// depurar macros para que podamos determinar el origen del mensaje de un vistazo // es malo #define WHERESTR "[archivo %s, línea %d]: " #define WHEREARG __FILE__, __LINE__ #define DEBUGPRINT2(...) fprintf(stderr, __VA_ARGS__) #define DEBUGPRINT(_fmt, ...) DEBUGPRINT2(WHERESTR _fmt, WHEREARG, __VA_ARGS__) // O // es bueno #define DEBUGPRINT(_fmt, ...) fprintf(stderr, "[archivo %s, línea %d]: " _fmt, __FILE__, __LINE__, __VA_ARGS__) DEBUGPRINT ( "oye, x=%d \n " , x ); 

Imprime el valor de x, precedido por el número de archivo y línea en el flujo de error, lo que permite un acceso rápido a la línea en la que se produjo el mensaje. Tenga en cuenta que el WHERESTRargumento se concatena con la cadena que lo sigue. Los valores de __FILE__y __LINE__se pueden manipular con la #linedirectiva . La #linedirectiva determina el número de línea y el nombre de archivo de la línea siguiente. Por ejemplo:

#line 314 "pi.c" printf ( "linea=%d archivo=%s \n " , __LINEA__ , __ARCHIVO__ );  

genera la printffunción:

printf ( "linea=%d archivo=%s \n " , 314 , "pi.c" );  

Los depuradores de código fuente también hacen referencia a la posición de origen definida con __FILE__y __LINE__. Esto permite la depuración de código fuente cuando se utiliza C como lenguaje de destino de un compilador, para un lenguaje totalmente diferente. El primer estándar de C especificó que la macro __STDC__se definiera como 1 si la implementación se ajusta al estándar ISO y 0 en caso contrario, y la macro __STDC_VERSION__se definió como un literal numérico que especifica la versión del estándar compatible con la implementación. Los compiladores estándar de C++ admiten la __cplusplusmacro. Los compiladores que se ejecutan en modo no estándar no deben configurar estas macros o deben definir otras para señalar las diferencias.

Otras macros estándar incluyen __DATE__, la fecha actual, y __TIME__, la hora actual.

La segunda edición del estándar C, C99 , agregó soporte para __func__, que contiene el nombre de la definición de función dentro de la cual está contenido, pero debido a que el preprocesador es agnóstico a la gramática de C, esto debe hacerse en el compilador mismo usando una variable local a la función.

Las macros que pueden tomar una cantidad variable de argumentos ( macros variádicas ) no están permitidas en C89, pero fueron introducidas por varios compiladores y estandarizadas en C99 . Las macros variádicas son particularmente útiles cuando se escriben envoltorios para funciones que toman una cantidad variable de parámetros, como printfpor ejemplo, cuando se registran advertencias y errores.

Un patrón de uso poco conocido del preprocesador C se conoce como X-Macros . [6] [7] [8] Un X-Macro es un archivo de encabezado . Comúnmente, estos usan la extensión .defen lugar de la tradicional .h. Este archivo contiene una lista de llamadas de macro similares, a las que se puede hacer referencia como "macros de componentes". Luego se hace referencia al archivo de inclusión repetidamente.

Muchos compiladores definen macros adicionales no estándar, aunque suelen estar mal documentadas. Una referencia habitual para estas macros es el proyecto Pre-defined C/C++ Compiler Macros, que enumera "varias macros de compilador predefinidas que se pueden utilizar para identificar estándares, compiladores, sistemas operativos, arquitecturas de hardware e incluso bibliotecas de ejecución básicas en tiempo de compilación".

Stringificación de tokens

El #operador (conocido como operador de cadena u operador de encadenamiento ) convierte un token en un literal de cadena C , escapando las comillas o barras invertidas de forma adecuada.

Ejemplo:

#define cadena(s) #sstr ( p = "foo \n " ;) // genera "p = \"foo\\n\";" str ( \ n ) // genera "\n"    

Si se desea convertir en cadena la expansión de un argumento macro, se deben utilizar dos niveles de macros:

#define xstr(s) str(s) #define str(s) #s #define foo 4str ( foo ) // genera "foo" xstr ( foo ) // genera "4"    

Un argumento de macro no se puede combinar con texto adicional y luego convertirlo en una cadena de caracteres. Sin embargo, se puede escribir una serie de constantes de cadena adyacentes y argumentos convertidos en cadenas de caracteres: el compilador de C combinará entonces todas las constantes de cadena adyacentes en una cadena larga.

Concatenación de tokens

El ##operador (conocido como "Operador de pegado de token") concatena dos tokens en uno solo.

Ejemplo:

#define DECLARE_STRUCT_TYPE(nombre) typedef struct nombre##_s nombre##_tDECLARE_STRUCT_TYPE ( g_object ); // Salidas: typedef struct g_object_s g_object_t; 

Errores de compilación definidos por el usuario

La #errordirectiva emite un mensaje a través del flujo de error.

#error "mensaje de error"

Inclusión de recursos binarios

C23 introducirá la #embeddirectiva para la inclusión de recursos binarios. [9] Esto permite que los archivos binarios (como imágenes) se incluyan en el programa sin que sean archivos fuente de C válidos (como XBM), sin requerir procesamiento por herramientas externas como xxd -iy sin el uso de literales de cadena que tienen un límite de longitud en MSVC . De manera similar, xxd -ila directiva se reemplaza por una lista separada por comas de números enteros correspondientes a los datos del recurso especificado. Más precisamente, si una matriz de tipo unsigned charse inicializa utilizando una #embeddirectiva, el resultado es el mismo que si el recurso se hubiera escrito en la matriz utilizando fread(a menos que un parámetro cambie el ancho del elemento incrustado a algo distinto de CHAR_BIT). Aparte de la conveniencia, #embedtambién es más fácil de manejar para los compiladores, ya que se les permite omitir la expansión de la directiva a su forma completa debido a la regla como si .

El archivo que se va a incrustar se puede especificar de forma idéntica a #include, es decir, entre comillas simples o entre comillas simples. La directiva también permite que se le pasen ciertos parámetros para personalizar su comportamiento, que siguen al nombre del archivo. El estándar C define los siguientes parámetros y las implementaciones pueden definir los suyos propios. El limitparámetro se utiliza para limitar el ancho de los datos incluidos. Su uso está pensado principalmente para archivos "infinitos" como urandom . Los parámetros prefixy suffixpermiten al programador especificar un prefijo y un sufijo para los datos incrustados, que se utilizan si y solo si el recurso incrustado no está vacío. Finalmente, el if_emptyparámetro reemplaza toda la directiva si el recurso está vacío (lo que sucede si el archivo está vacío o se especifica un límite de 0). Todos los parámetros estándar también se pueden rodear de guiones bajos dobles, al igual que los atributos estándar en C23, por ejemplo, __prefix__es intercambiable con prefix. Los parámetros definidos por la implementación utilizan una forma similar a la sintaxis de atributovendor::attr (por ejemplo, ) pero sin los corchetes. Si bien todos los parámetros estándar requieren que se les pase un argumento (por ejemplo, el límite requiere un ancho), esto generalmente es opcional e incluso el conjunto de paréntesis se puede omitir si no se requiere un argumento, lo que podría ser el caso de algunos parámetros definidos por la implementación.

Implementaciones

Todas las implementaciones de C, C++ y Objective-C proporcionan un preprocesador, ya que el preprocesamiento es un paso necesario para esos lenguajes y su comportamiento está descrito por estándares oficiales para estos lenguajes, como el estándar ISO C.

Las implementaciones pueden proporcionar sus propias extensiones y desviaciones, y variar en su grado de cumplimiento de los estándares escritos. Su comportamiento exacto puede depender de los indicadores de línea de comandos suministrados en la invocación. Por ejemplo, el preprocesador C de GNU puede hacerse más compatible con los estándares suministrando ciertos indicadores. [10]

Características del preprocesador específicas del compilador

La #pragmadirectiva es una directiva específica del compilador , que los proveedores de compiladores pueden usar para sus propios fines. Por ejemplo, a #pragmase usa a menudo para permitir la supresión de mensajes de error específicos, administrar la depuración de pila y montón, etc. Un compilador con soporte para la biblioteca de paralelización OpenMPfor puede paralelizar automáticamente un bucle con #pragma omp parallel for.

C99 introdujo algunas #pragmadirectivas estándar, que adoptan la forma #pragma STDC ..., que se utilizan para controlar la implementación de punto flotante. _Pragma(...)También se agregó la forma alternativa, similar a una macro.

Características del preprocesador específicas del lenguaje

Hay algunas directivas de preprocesador que se han agregado al preprocesador C por las especificaciones de algunos lenguajes y son específicas de dichos lenguajes.

Otros usos

Como el preprocesador C puede ser invocado por separado del compilador con el que se suministra, puede ser utilizado por separado, en diferentes lenguajes. Ejemplos notables incluyen su uso en el ahora obsoleto sistema imake y para preprocesar Fortran . Sin embargo, tal uso como un preprocesador de propósito general es limitado: el lenguaje de entrada debe ser suficientemente parecido a C. [10] El compilador GNU Fortran llama automáticamente al "modo tradicional" (ver abajo) cpp antes de compilar código Fortran si se usan ciertas extensiones de archivo. [17] Intel ofrece un preprocesador Fortran, fpp, para usar con el compilador ifort , que tiene capacidades similares. [18]

CPP también funciona aceptablemente con la mayoría de los lenguajes ensambladores y lenguajes similares a Algol. Esto requiere que la sintaxis del lenguaje no entre en conflicto con la sintaxis de CPP, lo que significa que no debe haber líneas que comiencen con #y que las comillas dobles, que cpp interpreta como cadenas literales y, por lo tanto, ignora, no tengan otro significado sintáctico que ese. El "modo tradicional" (que actúa como un preprocesador C anterior a la ISO) es generalmente más permisivo y más adecuado para tal uso. [19]

El preprocesador C no es Turing-completo , pero se acerca mucho: se pueden especificar cálculos recursivos, pero con un límite superior fijo en la cantidad de recursión realizada. [20] Sin embargo, el preprocesador C no está diseñado para ser, ni funciona bien como, un lenguaje de programación de propósito general. Como el preprocesador C no tiene características de algunos otros preprocesadores, como macros recursivas, expansión selectiva según comillas y evaluación de cadenas en condicionales, es muy limitado en comparación con un procesador de macros más general como m4 .

Véase también

Referencias

  1. ^ Preprocesamiento de texto de propósito general con el preprocesador C. Incluye JavaScript
  2. ^ Ritchie (1993)
  3. ^ "Bell SAP – SAP con macros condicionales y recursivas". HOPL: Enciclopedia histórica en línea de lenguajes de programación .
  4. ^ Lista de macros de implementación predefinidas de ANSI C y Microsoft C++.
  5. ^ Gabriel Dos Reis; Bjarne Stroustrup (22 de marzo de 2010). "Expresiones constantes generales para lenguajes de programación de sistemas, Actas SAC '10" (PDF) . Archivado (PDF) desde el original el 13 de junio de 2018. Consultado el 8 de julio de 2024 .
  6. ^ Wirzenius, Lars. C "Truco del preprocesador para implementar tipos de datos similares". Recuperado el 9 de enero de 2011
  7. ^ Meyers, Randy (mayo de 2001). "Las nuevas macros de C:X". Diario del Dr. Dobb . Consultado el 1 de mayo de 2008 .
  8. ^ Beal, Stephan (agosto de 2004). «Supermacros» . Consultado el 27 de octubre de 2008 . {{cite journal}}: Requiere citar revista |journal=( ayuda )
  9. ^ "WG14-N3017: #embed: un mecanismo de inclusión de recursos binarios escaneable y fácil de usar". open-std.org . 27 de junio de 2022. Archivado desde el original el 24 de diciembre de 2022.
  10. ^ ab "El preprocesador C: descripción general" . Consultado el 17 de julio de 2016 .
  11. ^ "WG14-N3096: Borrador para ISO/IEC 9899:2023" (PDF) . open-std.org . 1 de abril de 2023. Archivado (PDF) del original el 2 de abril de 2023.
  12. ^ "Borrador de trabajo, estándar para el lenguaje de programación C++" (PDF) . 22 de marzo de 2023.
  13. ^ Características obsoletas del CCG
  14. ^ "Encabezados envolventes (el preprocesador C)".
  15. ^ "N4720: Borrador de trabajo, extensiones a C++ para módulos" (PDF) . Archivado (PDF) del original el 30 de abril de 2019.
  16. ^ "P1857R1 – Descubrimiento de dependencia de módulos".
  17. ^ "1.3 Preprocesamiento y compilación condicional". Proyecto GNU.
  18. ^ "Uso del preprocesador fpp". Intel . Consultado el 14 de octubre de 2015 .
  19. ^ "Descripción general (el preprocesador de C)". gcc.gnu.org . Dicho esto, a menudo se puede salirse con la suya usando cpp en cosas que no son C. Otros lenguajes de programación similares a Algol suelen ser seguros (Ada, etc.), al igual que el lenguaje ensamblador, con precaución. El modo -traditional-cpp conserva más espacio en blanco y, por lo demás, es más permisivo. Muchos de los problemas se pueden evitar escribiendo comentarios de estilo C o C++ en lugar de comentarios de lenguaje nativo y manteniendo las macros simples.
  20. ^ "¿Está completo el preprocesador C99 de Turing?". Archivado desde el original el 24 de abril de 2016.

Fuentes

Enlaces externos