1.4.1 El preprocesador
§1 Sinopsis
Hemos señalado ( 1.4)
que en los lenguajes compilados como C++, antes de la compilación propiamente
dicha, el fichero fuente es sometido a un programa denominado preprocesador
que se encarga de realizar ciertas transformaciones en el código escrito por
el programador. Básicamente su tarea consiste en tres tipos de
acciones: incluir ciertas cosas; modificar otras, y eliminar unas terceras. El
resultado es lo que se denomina unidad de
compilación (
1.4.2).
§2 El preprocesador es un programa procesador-traductor
de texto ASCII, recursivo, inteligente y sumamente potente. Por ejemplo,
puede incluir en un punto del fuente el contenido de un fichero de texto, que
en una segunda pasada sufra nuevas modificaciones. Con frecuencia los
programadores experimentados construyen sus propias macros, defines y
directivas condicionales de forma que casi llegan a construir un nuevo
lenguaje dentro del lenguaje. El propio C++ que ve el programador está
construido en gran parte sobre directivas de preprocesado muy sofisticadas,
basta echar un vistazo a los ficheros de cabecera de la Librería
Estándar ( 5) para
comprobar a que niveles de refinamiento puede ser llevado, y percatarse de las
profundas transformaciones que sufre el fuente antes que sea realmente entregado al compilador.
Desde el punto de vista del programador, la parte más interesante es que el
comportamiento del preprocesador está gobernado por pautas explicitadas en las denominadas directivas de preproceso
( 4.9.10).
Estas directivas son sentencias que se sitúan normalmente al principio del
código fuente (tienen validez desde el punto de aparición hasta el final del fichero).
Siguiendo las pautas indicadas en estas directivas, el preprocesador obtiene un código (preprocesado) que será
entregado como una unidad de compilación (
1.4.2) al analizador sintáctico y de este al compilador
(
1.4). Las directivas de
preproceso las más corrientes son los include (incluir trozos de código en el fuente tomados de otros ficheros
4.9.10g)
y las macros (cambiar ciertas palabras por otras o por trozos de código más complejo
4.9.10b). Para dar una
idea de las profundas transformaciones y añadidos que realiza el preprocesador sobre un sencillo fuente, considere que el
preproceso del programa del ejemplo que sigue
(
)
supone el proceso de 18.490 líneas.
Es muy raro que el preprocesador sea invocado como programa independiente. Lo
normal es que sea traído automáticamente a ejecución como primer paso de la
compilación por el programa supervisor (
1.4.0). Sin embargo hay ocasiones, durante la
depuración de errores, en que interesa ver en que cosa se ha convertido
nuestro código después de la fase de preproceso. Por ejemplo, cuando
"inventamos" macros muy sofisticadas que en principio no funcionan
como esperamos. En estos casos el preprocesador puede generar un fichero
de salida con números de línea que se refieren a las originales del código
fuente. Esta unidad de compilación puede ya ser enviada al compilador,
aunque lo normal es que este fichero sea utilizado meramente con fines de comprobación.
En el caso de C++Builder, el preprocesador es el fichero Cpp32.exe; este programa genera un fichero de salida con el mismo nombre del fuente en el mismo directorio y terminación .I, aunque puede especificarse la dirección de salida, la inclusión o no de números de líneas, etc. Por ejemplo, el comando:
cpp32 -ID:;E:\borlandCPP\Include pa.c
procesa el fuente pa.c del ejemplo (
1.4.0) y genera un fichero de salida pa.i de más
de 954 KB frente a los 4 KB originales del texto fuente, o de 307 KB si se procesa con el comando:
cpp32 -ID:;E:\borlandCPP\Include -P -Sr -Ss pa.c
§3 Fases del preprocesado
Aunque en general se considera que el preprocesado es la primera parte de la compilación, y se piensa en él como un solo proceso, en realidad se compone de varias fases que describimos brevemente [1]:
§3.1 Tokenizado léxico
En una primera fase todos los caracteres del fuente son mapeados,
produciéndose una representación interna del fuente y se traducen los
trígrafos ( 3.2.3e)
a sus caracteres equivalentes.
§3.2 Empalmado de líneas
El programa realiza una conversión de líneas físicas a líneas lógicas. Por ejemplo, las líneas terminadas en barra invertida \ (ASCII 92) seguida de nueva línea NL (ASCII 10) son unidas a la siguiente (a menos que esté vacía, cada línea debe terminar en un NL que no esté precedido por la barra invertida).
§3.3 Tokenización
En esta fase, los comentarios
( 3.1) son sustituidos por un espacio
(ASCII 32); los espacios redundantes son eliminados, y el código queda reducido a tokens de preprocesado separados por
caracteres de separación (
1.3.1).
§3.4 Preprocesado
A continuación se realiza el preprocesado propiamente dicho. Se
ejecutan los include (
4.9.10g) y se sustituyen las macros por el código
correspondiente (
4.9.10b). Es importante señalar que el
preprocesador es recursivo, por lo que cualquier texto incluido con un include
es sometido a los pasos anteriores (a su vez puede contener comentarios, otros include, etc.)
§3.5 Mapeo de caracteres
Para la redacción del código fuente puede haberse utilizado un editor con un juego de caracteres arbitrario, pero el
compilador los reduce al juego de caracteres US-ASCII (
2.2.1a); también son traducidas las secuencias de escape
(
3.2.3e) a sus caracteres
correspondientes. El código es reducido al juego de caracteres de ejecución.
§3.6 Concatenación de cadenas
En esta fase, las cadenas literales adyacentes son unidas en una sola
( 3.2.3f).
Por ejemplo: "
Esto es
" " una
" "cadena.
"
queda reducida a: "Esto es una cadena.
".
[1] El detalle puede variar de un compilador a otro; la descripción corresponde a MS Visual C++ 6.0, pero básicamente todos siguen la misma pauta.