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]
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 #include
y #define
para 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]
El preprocesamiento se define mediante las primeras cuatro (de ocho) fases de traducción especificadas en el Estándar C.
_Pragma
operadores.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 .h
o .hpp
. Sin embargo, no existe ningún requisito que obligue a respetar esto. Los archivos con una .def
extensió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).
#include
A menudo obliga al uso de #include
protectores o #pragma once
a evitar la doble inclusión.
Las directivas if–else#if
, #ifdef
, #ifndef
, #else
, #elif
, y #endif
se pueden usar para la compilación condicional . #ifdef
y #ifndef
son 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 WIN32
en su lugar. Para aquellos compiladores que no definen implícitamente la _WIN32
macro, 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 _WIN32
se ha definido una macro. Si es así, <windows.h>
se incluye el archivo.
Un ejemplo más complejo #if
puede 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 #error
directiva:
#if RUBY_VERSION == 190 #error 1.9.0 no compatible #endif
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 const
calificador 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 constexpr
palabra 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 x
se 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
La expansión macro funcional ocurre en las siguientes etapas:
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)
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 WHERESTR
argumento se concatena con la cadena que lo sigue. Los valores de __FILE__
y __LINE__
se pueden manipular con la #line
directiva . La #line
directiva 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 printf
funció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 __cplusplus
macro. 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 printf
por 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 .def
en 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".
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.
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;
La #error
directiva emite un mensaje a través del flujo de error.
#error "mensaje de error"
C23 introducirá la #embed
directiva 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 -i
y sin el uso de literales de cadena que tienen un límite de longitud en MSVC . De manera similar, xxd -i
la 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 char
se inicializa utilizando una #embed
directiva, 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, #embed
tambié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 limit
parámetro se utiliza para limitar el ancho de los datos incluidos. Su uso está pensado principalmente para archivos "infinitos" como urandom . Los parámetros prefix
y suffix
permiten 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_empty
pará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.
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]
La #pragma
directiva es una directiva específica del compilador , que los proveedores de compiladores pueden usar para sus propios fines. Por ejemplo, a #pragma
se 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 #pragma
directivas 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.
#warning
al estándar para este propósito). Un uso típico es advertir sobre el uso de algún código antiguo, que ahora está obsoleto y solo se incluye por razones de compatibilidad; por ejemplo:// GNU, Intel e IBM #advertencia "No utilice ABC, que está obsoleto. Utilice XYZ en su lugar".
// Mensaje #pragma de Microsoft ("No utilice ABC, que está obsoleto. Utilice XYZ en su lugar").
#include_next
encadenar encabezados con el mismo nombre. [14]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.
#import
, que es similar #include
pero solo incluye el archivo una vez. Un pragma de un proveedor común con una funcionalidad similar en C es #pragma once
.#
carácter; en cambio, comienzan con import
y module
respectivamente, opcionalmente precedidas por export
.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 .
{{cite journal}}
: Requiere citar revista |journal=
( ayuda )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.