domingo, 19 de enero de 2014

El lenguaje C VII

Hay ocasiones en las cuales tenemos que utilizar una constante a lo largo de la ejecución de un programa. Un ejemplo sencillo consiste en la determinación de la longitud de la circunferencia que envuelve a un círculo cuando se conoce el diámetro del círculo, la cual puede ser evaluada con la fórmula:

circunferencia = 3.14159 * diametro;

Hemos usado aquí la constante numérica para representar la famosa constante pi. Para usar una constante en un programa C, podemos simplemente escribir su valor como lo hemos hecho aquí. Sin embargo, hay buenas razones para usar una “constante simbólica”. Podríamos usar un enunciado como:

circunferencia = pi * diametro;

y hacer que la computadora substituya posteriormente el valor actual. ¿Por qué ésta es una mejor manera de hacer las cosas? En primer lugar, un nombre nos dice mucho más que un número. Compárense los siguientes dos enunciados:

   balance = 1.12 * principal;

   balance = interes_compuesto * principal;

Si estamos leyendo un programa largo que comprende cientos o inclusive miles de enunciados, el significado de la segunda versión es más claro.

En segundo lugar, supóngase que vamos a utilizar una constante en muchos lugares, y en alguna etapa de la construcción del programa se vuelve necesario cambiar su valor. Después de todo, en el dinámico mundo financiero los intereses compuestos cambian con demasiada frecuencia. En tal caso, cuando usamos una constante simbólica podemos simplemente cambiar la definición de la constante simbólica en vez de tener que buscar y encontrar cada ocurrencia de la constante en un programa. Se podría argumentar en contra de esto último que con la capacidad de poder “Reemplazar Todas” (Replace All) disponible en los editores de texto de muchos entornos de programación, esto no es mayor problema, y no lo es si todo el código está puesto bajo un mismo archivo. Sin embargo, si el programa está subdividido y repartido en varios archivos, lo cual es común cuando se trabaja en proyectos grandes, esta operación de búsqueda y reemplazo se tiene que llevar a cabo no en un solo archivo sino en varios, lo cual puede ser extenuante cuando hay varias constantes que tienen que ser cambiadas.

¿Cómo podemos montar una constante simbólica? Una manera de hacerlo es declarar una variable, y hacerla igual a la constante deseada. Podemos hacer lo siguiente:

   float interes_compuesto;

   interes_compuesto = 1.125;

o bien, con mayor brevedad:

   float interes_compuesto = 1.125;

Esto está bien cuando se trata de un programa pequeño, pero es un poco dilapidante de recursos porque la computadora tiene que buscar en la localidad de la memoria interes_compuesto cada vez que es utilizada. Esto es un ejemplo de substitución en tiempo de ejecución, porque la substitución se lleva a cabo cada vez que el programa se está ejecutando. Afortunadamente, hay una mejor alternativa en C, y esa alternativa se llama el preprocesador C.

El preprocesador C es una herramienta extremadamente útil que se encarga de inspeccionar un programa C antes de que el programa empiece a ser compilado, y de ahí su nombre preprocesador. El preprocesador C, siguiendo las instrucciones o directivas al preprocesador dadas por el programador, reemplaza las abreviaturas simbólicas que hay en el programa con las direcciones que representan, buscando otros archivos que sean requisicionados por el programador, e inclusive cambiando las condiciones de la compilación.

En el ejemplo anterior, todo lo que hay que hacer es agregar una línea como la siguiente al principio del archivo que contiene el programa:

   #define  INTERES_COMPUESTO  1.125

De este modo, cuando el programa es compilado, el valor 1.125 será substituído cada vez que interes_compuesto sea usada. Esto se conoce como substitución en tiempo de compilación. Cuando el programa se ejecuta, todas las substituciones ya se han llevado a cabo.

Obsérvese el formato. En primer lugar va puesta la palabra clave #define usada para definir la constante simbólica. La palabra clave debe ir puesta siempre al principio de la línea, en el extremo izquierdo. Tras ello se escribe el nombre simbólico de la constante. No se utiliza semicolon, puesto que no es un enunciado C.

Podemos visualizar de la siguiente manera lo que ocurre a resutas del preprocesamiento:




¿Por qué se usaron mayúsculas al escribir INTERES_COMPUESTO? Podríamos haber usado minúsculas, y estamos en plena libertad de hacerlo. Sin embargo, es una vieja tradición en C definir las constantes usando letras mayúsculas. Entonces, cuando el programador tiene que leer las profundidades de un programa largo, el programador sabrá de inmediato que se trata de una constante y no de una variable. Podemos decir que es una conveniencia para los programadores, y acostumbrarse a esta convención es útil porque hay muchos programas (en especial, programas escritos en C para sistemas operativos de interfaz visual como Windows, Linux o como Mac OS) en los cuales se implementa esta tradición. Sin embargo, hay una que otra constante de uso amplio a las cuales muchos de nosotros nos hemos acostumbrado a usarlas como minúsculas. Una de ellas es la famosa constante “e”, la base de los logaritmos naturales o Neperianos. Es mucho más fácil reconocerla dentro de un programa como minúscula que como una “E” mayúscula que se puede confundir con otra cosa. En tal caso, lo recomendable es hacer:

   #define  e  2.718281

Por otro lado, en algunos compiladore y entornos C podemos aprovechar ventajosamente el hecho de que el conjunto de caracteres ASCII (véase el anexo al final de esta obra titulado “El código ASCII”) fue integrado dentro del conjunto de normas aceptadas por la American National Standards Institute (ANSI) que estandarizó la norma ANSI C, y hay algunos compiladores C que pueden reconocer caracteres ASCII que forman parte del conjunto extendido de caracteres ASCII, con símbolos tales como el símbolo griego “π” usado para representar el número pi, lo cual permite hacer la siguiente definición (en las computadoras personales caseras basadas en las convenciones fijadas desde un principio por IBM, podemos hacer que aparezca dentro de un editor de texto el símbolo π oprimiendo la tecla ALT y manteniéndola oprimida presionar secuencialmente las teclas 2, 2 y 7):

   #define  π  3.14159

Lo mismo podemos hacer con otros caracteres del conjunto ASCII extendido, obteniendo símbolos como el del exponente al cuadrado, el símbolo de mayor o igual a, y el símbolo de más/menos (estos símbolos son usados por C únicamente con fines de impresión de caracteres o hileras, ya que C no les puede dar la interpretación matemática usual que estamos acostumbrados a darle a estos símbolos, aunque lo puede hacer con definiciones macro proporcionadas por el programador):

² (ASCII 253)     ≥ (ASCII 242)    ± (ASCII 241)

con lo cual se puede escribir algo como lo siguiente:

   #define  e  2.718281

   printf("e² = %f\n ", e*e);

Se repite que este tipo de capacidades varía de compilador C a compilador C, y casi todos reconocen el conjunto de caracteres ASCII básico pero no todos reconocen el conjunto extendido de caracteres ASCII que incluye símbolos especiales ASCII del conjunto extendido como los que se han dado arriba. De cualquier modo, aunque el compilador pueda digerir los símbolos especiales del conjunto extendido de caracteres ASCII, queda la interrogante de si el sistema operativo en el cual se ejecutará un programa así elaborado tendrá los tipos de fuente requeridos para reproducir estos caracteres especiales.

El lector alerta reconocerá que de hecho ya hemos estado trabajando en forma intensiva en los ejemplos puestos en las entradas previas con otra instrucción destinada al preprocesador C, la directiva #include.

Todas las directivas que corresponden al preprocesador C empiezan con el símbolo de numeral (gato) #.

Como un anticipo, asentaremos que la gran mayoría de los compiladores C contienen las siguientes directivas.

   #if
   #ifdef
   #ifndef
   #else
   #elif
   #include
   #define
   #undef
   #line
   #error
   #pragma

En su expresión más sencilla la directiva #define es usada para definir un identificador y una secuencia de caracteres que será substituída por el identificador cada vez que sea encontrada en el archivo fuente. El identificador es lo que se conoce como el nombre del macro y el proceso de reemplazo es lo que se conoce como substitución del macro. La sintaxis de esta directiva es la siguiente:

   #define  nombre-del-macro   secuencia-de-caracteres

Obsérvese que no se usa un semicolon al final de este enunciado. Recuérdese que tampoco se usó un semicolon en ninguna de las directivas #include que vimos en los ejemplos anteriores. Las directivas al preprocesador C carecen de semicolon de terminación al final de cada enunciado.

El estándard ANSI C permite que el símbolo # sea precedido por un espacio en blanco o por un espaciado de tabulador (tab), y puede haber cualesquier número de espacios en blanco entre el identificador y la secuencia de caracteres, pero una vez que la secuencia de caracteres ha empezado sólo puede ser terminada con una nueva línea (oprimiendo la tecla [Enter]). Inclusive el estándard ANSI C permite que después del símbolo # haya un espacio en blanco o un espaciado de tabulador, lo cual implica que los siguientes enunciados son equivalentes:

# include <stdio.h>  (espacio en blanco)

# include <stdio.h>   (tabulador)

La directiva #define puede aparecer en cualquier lugar del archivo fuente (el programa C), y la definición dada al macro toma validez a partir del lugar en el cual aparece puesta hasta el final del archivo.

A continuación tenemos otro ejemplo que nos muestra las posibilidades y las propiedades de la directiva #define:

   #define  ERROR  "El nombre del archivo no es valido"

Antes de continuar, se asentará aquí que si la secuencia de caracteres en un enunciado destinado al preprocesador C es más grande que lo que pueda caber en una línea, podemos usar una diagonal inversa (\) puesta el final de cada línea para poder continuar con el enunciado en la siguiente línea:

   #define  MENSAJE "El Castillo de Chapultepec fue construido  \
                        en los tiempos del Emperador Maximiliano."

He aquí otro ejemplo del uso de #define en un programa:


   #include <stdio.h>

   #define PI 3.14159

   void main(void)
   {
      float area, circunferencia, radio;

      printf("Dame un radio: ");
      scanf("%f", &radio);
      area = PI * radio * radio;
      circunferencia = 2.0 * PI * radio;

      printf("Circunferencia = %1.2f\n", circunferencia);
      printf("Area = %1.2f", area);
   }


Si se ejecuta el programa anterior, se obtiene un resultado como el siguiente en una ventana de diálogo:


 Dame un radio: 5
 
Circunferencia = 31.42
 
Area = 78.54


Antes de dejar el ejemplo anterior, obsérvese que para el cálculo del área del círculo, lo cual requiere elevar al cuadrado el radio del círculo, no hay nada disponible en el lenguaje C para elevar una cantidad al cuadrado (ni siquiera para llevar a cabo una exponenciación) usando un símbolo como el carete (^) que se usa en BASIC, y esta es la razón por la cual en la línea para el cálculo del cuadrado del radio aparece el radio multiplicado por sí mismo. Si queremos llevar a cabo exponenciaciones a cualquier potencia (incluso potencias fraccionarias), C nos deja en plena libertad de poder definir funciones matemáticas que hagan tal cosa, pero no lo hace como parte del lenguaje. Por otro lado, obsérvese que en los enunciados de impresión:

   printf("Circunferencia = %1.2f\n", circunferencia);
   printf("Area = %1.2f", area);

hemos usado %1.2f como código de formato, precediendo la “f” con un “%1.2”. El %1.2f hace que el resultado numérico se imprima usando dos lugares decimales, lo cual efectivamente sucedió.

Como ya se anticipó arriba, el enunciado #define puede ser usado no sólo para definir constantes numéricas sino también constantes de caracter y constantes de hileras. Para definir constantes de caracter usamos comillas sencillas, y para definir constantes de hilera usamos comillas dobles:

   #define  BEEP  '\007'

   #define  NULO   '\0'

   #define  ENDOFILE   "FINDEARCHIVO"

Hasta aquí hemos estado elaborando programas que pueden ser guardados en un solo archivo con un nombre de archivo como EJEMPLO.C (la extensión “.C” agregada al nombre del archivo indica que se trata de un programa fuente en C). Pero cuando se trata de programas grandes, es mejor recurrir a la máxima romana “divide y vencerás”. Supóngase que se ha desarrollado un paquete completo de programas que usan el mismo conjunto de constantes. En tal caso, podemos hacer lo siguiente:

(1) Subdividir el programa en varios archivos poniendo las funciones más grandes de las que conste el programa en archivos separados e individuales.

(2) Reunir todos los enunciados #define que haya en los archivos poniéndolos en un solo archivo separado dándole un nombre apropiado como CONST.H.

(3) Al principio de cada archivo de programas, meter el enunciado:

#include "const.h"

Cuando se vaya a compilar el programa, el preprocesador leerá el archivo CONST.H y utilizará todos los enunciados #define que encuentre allí. La extensión .h puesta al final del nombre de archivo es un recordatorio que le dice al programador que se trata de un archivo de cabecera (header file), con información que debe ir a la cabeza del programa completo compuesto por varias funciones (en muchos casos y en muchos entornos, al preprocesador no le importa si usamos la extensión .h en el nombre del programa).

Existen algunas limitaciones en el uso de la directiva #define, y una que hay que tener presente es que las partes de un programa que se encuentren entre comillas dobles son inmunes a la substitución llevada a cabo por el preprocesador C. La siguiente combinación, por ejemplo, no funciona:

   #define  NOMBRE  "Armando"

   printf("Me llamo NOMBRE");

ya que producirá simplemente:

       Me llamo NOMBRE

Sin embargo, lo siguiente sí producirá el resultado deseado en virtud de que el MACRO no está puesto entre las comillas dobles:

   #define  NOMBRE  "Armando"

   printf("Me llamo %s", NOMBRE);

produciendo:

       Me llamo Armando

El siguiente ejemplo nos muestra algunas de las posibilidades disponibles con la directiva #define:

   #include <stdio.h>

   #define DOS 2
   #define MENSAJE "Hola mis amigos."
   #define CUATRO DOS*DOS
   #define PX printf("X es %d.\n", x)
   #define FMT "X es %d.\n"

   void main(void)
   {
      int x = DOS;

      PX;
      x = CUATRO;
      printf( FMT, x);
      printf("%s\n", MENSAJE);
      printf("DOS: MENSAJE.\n");
   }


Si se ejecuta el programa anterior, se imprime lo siguiente:

   x es 2.
   X es 4.
   Hola mis amigos.
   DOS: MENSAJE

Examinemos ahora de cerca la manera en la cual trabaja el programa. El enunciado:

int x = DOS;

se vuelve tras pasar por el preprocesador C:

int x = 2;

al ser substituído 2 en lugar de DOS, tras lo cual el enunciado que marca la primera instrucción de impresión:

PX;

se convierte en (tras la substitución del macro):

printf("X es %d.\n", x);

de modo tal que se lleva a cabo una substitución al mayoreo. Este dá un nuevo giro a #define, ya que hasta este punto habíamos estado usando macros solo para representar constantes. Podemos ver ahora que un macro puede expresar cualquier hilera, inclusive una expresión C completa. Obsérvese sin embargo que esta es una hilera constante, PX sólo imprimirá una variable llamada x.

El siguiente paso también le dá un giro nuevo a la directiva para substitución de macros. Tal vez el lector haya pensado que CUATRO es simplemente reemplazado por 4, pero en el proceso actual:

x = CUATRO;

se vuelve:

x = DOS*DOS;

lo cual a su vez se convierte en:

x = 2*2;

terminando ahí. La multiplicación indicada se lleva a cabo, más no cuando el preprocesador hace su trabajo, sino cuando el compilador C evalúa todas las expresiones constantes.

Obsérvese que la definición de un macro puede incluír otros macros (sin embargo, algunos compiladores C y entornos IDE de C no dan soporte a este tipo de “anidamiento”).

La siguiente línea:

printf( FMT, x);

se vuelve:

printf( "X es %d.\n", x);

siendo FMT reemplazada por la hilera correspondiente. Esta posibilidad puede ser útil si en el programa aparece una hilera larga que hay que usar varias veces en el programa.

En la siguiente hilera MENSAJE es reemplazado por la hilera correspondiente. Las dobles comillas hacen que la hilera de reemplazo sea una constante de hilera de caracteres; esto es, una vez que el programa toma conocimiento de ello será almacenada en un array terminado con un caracter nulo. Por lo tanto:

   #define  ABC  'M'   define una constante de caracter

   #define  XYZ  "Z"   define una constente de hilera: XYZ\0

En general, cuando el preprocesador encuentra algún macro en alguna parte del programa, lo reemplaza literalmente con la hilera equivalente de reemplazo. Si la hilera de reemplazo contiene macros, esos macros también serán reemplazados, la única excepción ocurre cuando el macro es puesto por el programador entre comillas dobles. Por lo tanto:

printf("DOS: MENSAJE.\n");

imprime DOS: MENSAJE literalmente en vez de imprimir:

2: Hola mis amigos.

Otra posibilidad es que podemos usar macros con argumentos. Un macro con argumentos se parece mucho a una función, puesto que los argumentos están encerrados entre paréntesis. A continuación tenemos un ejemplo que ilustra cómo se define y se usa una “función macro”:


   #include <stdio.h>

   #define MINIMO(a,b)  ((a)<(b))  ?  (a) : (b)

   main(void)
   {
     int x, y;

     x = 110;
     y = 50;
     printf("el minimo es: %d", MINIMO(x,y));

     return 0;
   }


La ejecución del programa imprimirá la línea “el mínimo es 50”. Cuando el programa es compilado, la expresión definida por MINIMO(a,b) habrá sido substituída, excepto que x y y serán usados como los operandos. Por lo tanto, el enunciado de impresión printf() será substituído para tomar el siguiente aspecto:

printf("el mínimo es: %d", ((x)<(y)) ? (x) : (y));

La razón para haber puesto paréntesis alrededor de a y b en la definición del macro fue para asegurar que cualesquiera expresiones que sean substituídas para a y b serán evaluadas en su totalidad (en algunos casos el no usar los paréntesis producirá resultados erróneos). Así como usamos la directiva #define para definir un macro usado en la selección del número más pequeño entre dos números, también podemos definir otros macros para cosas tales como la selección del número más grande entre dos números, para eliminar el signo negativo de un número negativo tomando el valor absoluto del mismo, o para obtener un valor igual a 1 -verdadero- si x tiene el carácter de un signo algebraico:

   #define MAXIMO(x,y)  ( (x) > (y) ? (x) : (y) )

   #define ABS(x)  ( (x) < 0 ? -(x) : (x) )

   #define ESSIGNO()  ( (x) == '+' || (x) == '-' ? 1 : 0 )

Una ventaja de usar substituciones macro en lugar de funciones reales es que aumenta la velocidad de ejecución del programa porque no se incurre en la carga adicional de trabajo que implica una invocación a una función, aunque este aumento en velocidad no es lo que pudiera esperarse en virtud del aumento en el tamaño del código al tenerse código duplicado.

Las posibilidades que se pueden lograr con la directiva #define van mucho más allá de lo que el lector se pueda imaginar con los ejemplos sencillos que se han proporcionado arriba. Usando dicha directiva es posible castellanizar en su totalidad un programa fuente en C, permitiéndole a un programador que labore en el mundo de habla hispana escribir su programa no usando las palabras clave inglesas reservadas por C para su uso exclusivo, sino las palabras “clave” creadas para poder elaborar en español un programa C. A manera de ejemplo, podemos tomar el programa anterior, y mediante varios enunciados #define lo podemos convertir al siguiente programa C en el cual hasta la función principal es asentada con su nombre en español:


   #include <stdio.h>

   #define MINIMO(a,b)  ((a)<(b))  ?  (a) : (b)

   #define  principal  main
   #define  nulo  void
   #define  entero  int
   #define  imprimir  printf
   #define  regresar  return

   principal(nulo)
   {
      entero x, y;

      x = 110;
      y = 50;
      imprimir("el minimo es: %d", MINIMO(x,y));

      return 0;
   }


El programa anterior se compilará a un programa ejecutable produciendo exactamente el mismo resultado que su versión en lenguaje C “en inglés”, el usuario final no notará diferencia alguna. Así como en el ejemplo que acabamos de ver, podemos tomar uno de los programas que vimos en una entrada anterior al cubrir el tema de los bucles, un programa que imprime los números del 1 al 100, y cambiar la palabra reservada “for” con nuestra propia palabra reservada “para” reescribiendo el programa como:


   #include <stdio.h>

   #define  principal  main
   #define  nulo  void
   #define  entero  int
   #define  imprimir  printf
   #define  regresar  return
   #define  para  for

   nulo principal(nulo)
   {
      int conteo;

      para(conteo=1; conteo<=100; conteo++)   printf("%d ", conteo);
   }


Si el programador hispano crea su propio archivo de cabecera con un título llamativo como CESP.H metiendo ahí todos los macros #define que le permiten “castellanizar” sus programas C, puede elaborar todos sus programas enteramente en español sin necesidad de tener que acordarse siquiera de esos macros dentro del programa castellanizado, puede proceder a elaborar todos sus trabajos en su propio idioma sin saber decir siquiera “Good morning”. Tal es el enorme margen de libertad que otorga el lenguaje C, un margen de libertad que es imposible de encontrar en otros lenguajes como FORTRAN o Pascal. Esta capacidad es lo que hace que C sea conocido por algunos programadores como “el maestro del disfraz”. Podemos inclusive elaborar macros para tomar un programa fuente escrito en Pascal de modo tal que, después de haber pasado los materiales por el preprocesador C, se puedan compilar programas elaborados en Pascal a archivos ejecutables en lo que en realidad sería un proceso de dos pasos: la conversión del programa fuente en Pascal a su equivalente en C, y la compilación del programa C a un archivo ejecutable.

Podemos, pues, “castellanizar” nuestros programas C, elaborándolos totalmente en español. Pero... ¿en realidad queremos hacer tal cosa? Nuestros programas serían legibles para otros programadores hispanos, pero serían completamente ilegibles para programadores japoneses, programadores franceses, programadores rusos y programadores finlandeses. Las palabras inglesas reservadas en C constituyen un vehículo universal para que los programadores de distintos países se puedan comunicar entre sí, y al igual que el Latín fue el vehículo de comunicación universal en los países de la Europa antigüa, en la actualidad el Inglés es el vehículo universal para la comunicación técnica alrededor del mundo. ¡No en vano Linus Torvalds, siendo finlandés, elaboró el sistema operativo que hoy lleva su nombre usando terminología inglesa en lugar de terminología finlandesa! De haber hecho lo segundo, únicamente los finlandeses se habrían beneficiado de su trabajo, y hoy no tendríamos un sistema operativo gratuito y potente como Android. El poseer un vehículo que pueda levantar 400 kilómetros por hora no significa que vamos a circular en dicho vehículo por una calle pisando el acelerador al máximo, a sabiendas de que de hacer tal cosa podemos terminar muy mal.

Con relación a la directiva #include usada para incluír archivos de cabecera dentro de un programa C, dicha directiva se presenta en dos variedades que son aceptadas por igual en muchos entornos de compilación C:

   #include <stdio.h>

   #include "STDIO.H"

En una máquina operando bajo un sistema operativo UNIX, XENIX, o algún derivado de ellos, los paréntesis angulados le instruyen al preprocesador buscar el archivo a ser incluído en uno o en más de los directorios estándard del sistema operativo, mientras que las dobles comillas le piden buscar primero en el directorio en el cual está ubicado el usuario (o algún otro directorio, si se especifica la ruta completa junto con el nombre del archivo) y en caso de no encontrarlo ahí entonces buscarlo en los otros lugares comunes usados por el sistema operativo para el guardado de archivos:

   #include <stdio.h>   buscar archivo STDIO.H en los directorios del sistema
   #include "armando.h"   buscar en el directorio en uso actual
   #include "/usr/biff/armando.h"   buscar en el directorio /usr/biff

Dependiendo del sistema computacional usado, las dos formas pueden ser sinónimas, y el preprocesador buscará en el disco duro o disposivo indicado:

   #include  "stdio.h"   buscar en el dispositivo predeterminado (disco duro, USB, etc.)
   #include  <stdio.h>   buscar en el dispositivo predeterminado (disco duro, USB, etc.)
   #include  "c:stdio.h"   buscar en el disco duro c:

Como el lector tal vez ya se habrá dado cuenta, una cosa que podemos hacer en la declaración dentro de un programa C de los archivos #include es escribirlos en minúsculas o escribirlos en mayúsculas, y por lo tanto el siguiente fragmento:

   #include <stdio.h>
   #include <stdlib.h>
   #include <bios.h>

es completamente equivalente al siguiente fragmento (en la literatura encontramos ambas formas):

   #include <STDIO.H>
   #include <STDLIB.H>
   #include <BIOS.H>

Pero a estas alturas esto puede parecer desconcertante. ¿Acaso no habíamos dicho previamente en todas las entradas anteriores que en el lenguaje C se establece claramente una distinción entre el nombre de una palabra reservada o de una variable definidas en minúsculas, y los mismos nombres puestos en mayúsculas? Esto último sigue siendo completamente válido. Lo que sucede es que las directivas como #include y #define no forman parte del lenguaje C, son instrucciones dadas al preprocesador C, y el preprocesador C tampoco forma parte del lenguaje C. El preprocesador C así como las directivas que puede procesar son algo que fue creciendo con el paso del tiempo hasta terminar convirtiéndose en una necesidad al elaborarse código para proyectos grandes. Sin embargo, las directivas al preprocesador C tienen que ser invocadas usando minúsculas; si tratamos de usar “#INCLUDE” en lugar de “#include”, se marcará un error.

Como un ejemplo de un archivo de cabecera creado para nuestro propio propósito, supóngase que nos gusta usar valores boleanos, esto es, en lugar de usar 1 como verdadero y 0 como falso preferimos usar las palabras TRUE y FALSE. Podemos crear un archivo de cabecera dándole un nombre como BOOL.H que contenga las siguientes definiciones:

   #define  BOOL  int
   #define  TRUE  1
   #define  FALSE  0

Una vez creado el archivo de cabecera, podemos invocarlo en un programa como el siguiente que estará archivado por separado bajo un nombre como ESPACIOS.C:


   #include <stdio.h>
   #include "bool.h"

   main(void)
   {
      int ch;
      int conteo = 0;
      BOOL espacio_en_blanco(char c); /* declaracion de prototipo */

      while ( (ch = getchar() ) != EOF)
         if( espacio_en_blanco(ch) )
            conteo++;
      printf("Hay %d espacios en blanco.\n", conteo);
   }

   BOOL espacio_en_blanco(char c)
   {
      if ( c == ' ' || '\n' || '\t' )
         return(TRUE);
      else
         return(FALSE);
   }


Podemos hacer las siguiente observaciones acerca de este programa:

(1) Si las dos funciones usadas en el programa, main() y espacio_en_blanco fueran compiladas por separado estando puestas en archivos separados, tendríamos que usar la directiva #include "bool.h" en cada archivo.

(2) La expresión:

if( espacio_en_blanco(ch) )

es lo mismo que:

if( espacio_en_blanco(ch) == TRUE )

puesto que espacio_en_blanco(ch) en sí tiene el valor TRUE o FALSE.

(3) No hemos creado un nuevo tipo de dato BOOL, puesto que BOOL es simplemente un int.

(4) Usando una función para llevar a cabo comparaciones lógicas puede hacer un programa más claro. También puede ahorrar esfuerzo si la comparación es efectuada en más de un lugar en el programa.

(5) Podríamos haber usado un macro en lugar de una función para definir espacio_en_blanco().

Las directivas #include y #define son las más usadas capacidades del preprocesador C, pero no son las únicas, hay otras que fueron mencionadas arriba y que ahora cubriremos con mayor detalle. Estas son las directivas de compilación condicional. Estas nos permiten compilar en forma selectiva porciones del programa fuente C. El proceso conocido como compilación condicional es ampliamente usado por empresas que elaboran software en varias versiones para varios tipos de máquinas y sistemas operativos, como una empresa que elabore un paquete de diseño gráfico para ser usado en computadoras con procesadores de 64 bits y computadoras con procesadores de 128 bits habiendo mucho código C común a ambas versiones pero teniendo cada versión porciones substanciales de código que es diferente de una versión a otra.

La primera directiva de compilación condicional que veremos es la directiva #if. La idea general detrás de dicha directiva es que si la expresión constante que le sigue es verdadera entonces el código que haya entre #if y #endif será compilado, y en caso contrario será ignorado. El #endif es usado para marcar el final de un bloque #if. La sintaxis de la forma general es:

   #if  expresion constente
      bloque de código
   #endif

Si la expresión constante es verdadera, el bloque de código será compilado, de lo contrario será ignorado. El siguiente programa nos proporciona un ejemplo sencillo de dicha directiva en acción:


   #include <stdio.h>

   #define TOPE 100

   main(void)
   {
     #if TOPE>99
        printf("TOPE es mayor que 99\n");
     #endif

     return 0;
   }


Este programa imprime el mensaje “TOPE es mayor que 99” porque, como se #define al principio del programa, TOPE es mayor que 99. Este ejemplo ilustra un punto importante: la expresión que sigue al #if es evaluada en tiempo de compilación, y por lo tanto puede contener constantes e identificadores que han sido definidos previamente; no se puede usar variables.

Otra directiva de compilación condicional es la directiva #else, la cual trabaja de la misma manera en la que trabaja el enunciado else que forma parte del lenguaje C, establece una alternativa en caso de que falle el #if. Esto nos permite tomar el ejemplo anterior para expandirlo de la manera que se muestra a continuación:


   #include <stdio.h>

   #define TOPE 10

   main(void)
   {
     #if TOPE>99
        printf("TOPE es mayor que 99\n");
     #else
        printf("TOPE es menor que 99\n");
     #endif

     return 0;
   }


En este caso se imprimirá “TOPE es menor que 99” al haberse cambiado el valor de TOPE de 100 a 10. Al ser definido TOPE como inferior a 100 la porción #if del código no será compilada, pero la alternativa #else sí será compilada. Obsérvese el #else usado para marcar el final del bloque #if y el comienzo del bloque #else. Esto es necesario porque solo puede haber un #endif asociado con cualquier #if.

Otra directiva de compilación condicional es la directiva #elif que significa “else if”, usada para establecer una escalera if/else/if para obtener así varias opciones de compilación condicional. La directiva #elif es seguida por una expresión constante. Si la expresión es verdadera, entonces ese bloque de código es compilado y no se prueban otras expresiones #elif. En caso contrario, se prueba la siguiente que haya abajo. Esta es la sintaxis de la forma general:

   #if  expresión 1

      bloque de código

   #elif expresión 2

      bloque de código

   #elif expresión 3

      bloque de código

   #elif expresión 4
      .
      .
      .

   #elif expresión N

      bloque de código

   #endif

En el siguiente fragmento usamos el valor de PAIS para definir el idioma que será usado en varias partes del programa en su interacción con el usuario:

   #define  USA  1
   #define  MEXICO  2
   #define  ALEMANIA  3

   #define  PAIS  USA

   #if PAIS == USA
      char IDIOMA[] ="inglés";
   #elif  PAIS == MEXICO
      char IDIOMA[] = "castellano";
   #else
      char IDIOMA[] = "aleman";
   #endif

Podemos anidar los #if y los #elif cuantas veces queramos con el #endif, el #else o el #elif asociándose con el #if o el #elif más cercano. Aquí tenemos una muestra de código que es perfectamente válido:

   #if TOPE>100
      #if CPU6809
         int puerto = 85;
      #else
         int puerto = 143;
      #endif
   #else
      char buffer[200];
   #endif

Aunque la indentación que se ha mostrado no es algo requerido para que se puede efectuar el proceso de compilación, la estructura y la lógica del programa resulta mucho más clara cuando se tienen anidamientos de directivas de compilación condicional, ciertamente mucho más legible que si todas las líneas del programa estuvieran alineadas hacia el extremo derecho del borde de la pantalla.

Hay otro método de compilación condicional que usa las directivas #ifdef e #ifndef, las cuales son abreviaturas de “if defined” (si está definido) e “if not defined” (si no está definido). La sintaxis de la forma general de #ifdef es:

   #ifdef  nombre del macro

      bloque de código

   #endif

Si el nombre del macro ha sido definido previamente en algún enunciado #ifdef, el bloque de código entre #ifdef y #endif será compilado.

La sintaxis de la forma general del #ifndef es:

   #ifndef  nombre del macro

      bloque de código

   #endif

En este caso, si el nombre del macro no ha sido definido previamente mediante un enunciado #define, el bloque de código será compilado.

Tanto la directiva #ifdef como la directiva #ifndef pueden usar un enunciado #else pero no pueden usar la directiva #elif. A continuación tenemos un programa que nos muestra la manera en la que se usa esto:


   #include <stdio.h>

   #define ARMANDO 1920

   main(void)
   {
     #ifdef ARMANDO
        printf("Hola Armando!\n");
     #else
        printf("Hola!\n");
     #endif

     #ifndef LAURA
        printf("LAURA no esta definida\n");
     #endif

     return 0;
   }


El programa anterior imprimirá en dos líneas separadas “Hola Armando!” y “LAURA no esta definida”.

Las directivas #ifdef e #ifndef también se pueden anidar a cualquier nivel como ocurre con los #if.

Otra directiva disponible para uso del preprocesador C es la directiva #undef, la cual actúa en sentido contrario a la directiva #define, ya que remueve una definición que se le haya dado previamente a un macro. La sintaxis de la forma general de #undef es:

   #undef  nombre del macro

El siguiente fragmento nos dá una idea de la manera en la cual trabaja esta directiva:

   #define  ESTATURA  78
   #define  PESO   83

   char  tabla[ESTATURA][PESO];

   #undef  ESTATURA
   #undef  PESO
   /* a partir de este punto ESTATURA y PESO estan indefinidos */

En el fragmento, tanto ESTATURA como PESO permanecen definidos hasta que los enunciados #undef son encontrados.

¿De qué utilidad podría ser la directiva #undef? El objetivo detrás de su razón de ser es permitir que los nombres macros puedan estar localizados y confinados únicamente a aquellas porciones de código que los estarán utilizando. Por otro lado, si hemos definido un macro con #define, no podemos redefinirlo posteriormente en el código subsecuente usando simplemente otro #define, porque el compilador lo rechazará como un intento de darle dos definiciones diferentes a un mismo macro; después de que se le ha dado al macro la primera definición hay que removerle dicha definición antes de tratar de darle una nueva.

Además de las directivas del preprocesador C ya mencionadas, las cuales son de uso general y están disponibles en la gran mayoría de paquetes de compilación C que han estado saliendo a la luz, hay otras directivas de las cuales nos enteramos únicamente a través de la documentación que viene con ciertos paquetes de software.

En el entorno de programación Turbo C++ elaborado por la empresa Borland, es común encontrar una directiva simbolizada como #line (una referencia a cada número de línea que es asignado a un programa), la cual puede ser usada para cambiar el contenido del identificador __LINE__ predefinido en el compilador que lleva la cuenta de cada línea del programa C, y cuya sintaxis es:

#line  numero  nombre_de_archivo

en donde número puede ser cualquier entero positivo que representa un número de línea de un programa y el nombre_de_archivo optativo es cualquier identificador válido de un archivo guardado en la memoria permanente (el disco duro) de la computadora. El número de línea se refiere al número de línea actual y el nombre de archivo es el nombre del archivo fuente. Este tipo de directiva sólo es usada por los programadores para propósitos de depuración de errores y aplicaciones especiales, y no es algo que ayuda a la portabilidad del código hacia otros tipos de máquinas y sistemas operativos. De hecho, esta directiva es ignorada por el entorno integrado de desarrollo, aunque es tomada en cuenta cuando se usa el compilador de líneas de comandos en una ventana DOS. A modo de ejemplo, el siguiente código especifica que el conteo de líneas empezará con la línea 50, y el enunciado printf() imprimirá la línea número 53 porque es la tercera línea dentro del programa

     #include <stdio.h>
   #line 50   /* La primera linea del programa
                 será inicializada a 50 */

   main(void)                    /* Linea 1 del programa */
   {                             /* Linea 2 del programa */
     printf("%d\n", __LINE__);   /* LInea 3 del programa */
                                 /* Linea 4 del programa */
     return 0;                   /* Linea 5 del programa */
   }                             /* Linea 6 del programa */


Si en el entorno IDE Borland C++ se ejecuta el programa anterior, se imprimirá el número “53”. ¿Por qué? La directiva #line al preprocesador C le instruye que el conteo de líneas del programa empezará a partir de 50, y la primera línea del programa C será considerada como la línea 51, y esta es la línea en la que aparece el enunciado main(void). La siguiente línea del programa, en donde aparece el corchete de apertura, será por lo tanto la línea 52. Y así sucesivamente. La línea en la cual aparece la instrucción de impresión es la tercera línea del programa a partir de la primera línea del programa, que es la línea 53, y por lo tanto es el número que se le regresa al programador. Obsérvese que las líneas previas a la línea en la cual se encuentra main() no cuentan para fines de conteo de líneas del programa C, el conteo empieza a partir de la línea en donde empieza el programa en sí.

Hay otro tipo de directivas peculiares además de la directiva #line ya mencionada. Una de ellas es la directiva #pragma que debe ser usada con otro nombre que se le debe proporcionar de acuerdo a la siguiente sintaxis:

#pragma  nombre

En el caso de Turbo C++ y Borland C++, se definen los siguientes nombres usados con la directiva #pragma:

   argused
   exit
   startup
   inline
   option
   saveregs
   warn

Antes de entrar en mayores detalles, se dará a continuación un programa en el cual se utilizan dos de los #pragma que se acaban de mencionar:


   #include <stdio.h>

   void alto(void);
   void iniciar(void);

   #pragma exit alto 101
   #pragma startup iniciar 101
   main(void)
   {
     printf("Dentro del programa principal.\n");
     return 0;
   }

   void alto(void)
   {
     printf("El programa esta terminando.\n");
   }

   void iniciar(void)
   {
     printf("El programa esta empezando.\n");
   }


Si se ejecuta el programa anterior usando el software señalado, se obtiene la siguiente respuesta en la ventana DOS:


 El programa esta empezando.
 
Dentro del programa principal.
 
El programa esta terminando.


Ahora entraremos en los detalles. La directiva exit especifica una o más funciones que serán invocadas cuando termine la ejecución del programa, mientras que la directiva startup especifica una o más funciones que serán invocadas cuando el programa empieza a ejecutarse, y tienen la siguiente sintaxis:

   #pragma exit  nombre  prioridad

   #pragma startup  nombre  prioridad

en donde nombre es el nombre de la función que será invocada, mientras que prioridad es un entero con un valor ubicado entre 64 y 255 (los valores 0 a 63 están reservados para otros fines y no pueden ser usados por el programador). La prioridad determina el orden en el cual serán invocadas las funciones que vayan siendo llamadas cuando haya más de una función. Si no es especifica prioridad alguna, entonces de manera predeterminada la prioridad es puesta a un valor de 100. Todas las funciones usadas como exit y startup tienen que ser funciones declaradas de la siguiente manera sin tomar argumentos y sin producir valor de retorno:

void funcion(void);

Como lo muestra el ejemplo que se acaba de dar, se tiene que proporcionar un prototipo de función para todas las funciones exit y startup antes de que aparezca el enunciado #pragma. Obsérvese bien que ninguna de las funciones alto() e inicio() aparecen invocadas explícitamente en el programa. Esto porque son ejecutadas automáticamente cuando el programa comienza y termina su ejecución.

Otra directiva #pragma es la directiva inline cuya sintaxis general es la siguiente:

   #pragma inline

Esta directiva le informa a Turbo C++/Borland C++ que dentro del programa se encuentra código enlínea (inline code), generalmente código ensamblador. Aunque esto puede ser útil en algunos casos, la inclusión de código en lenguaje ensamblador dentro de un programa C tendrá como consecuencia fatal que el programa una vez compilado sólo podrá ejecutarse en una máquina que contenga el procesador CPU para el cual las instrucciones en lenguaje ensamblador sean válidas. Si se quiere tener portabilidad del programa C hacia otras máquinas y otros sistemas operativos, el uso de código enlínea queda descartado por completo.

En la convención ANSI C encontramos cinco nombres macro predefinidos:

   __TIME__

   __DATE__

   __LINE__

   __FILE__

   __STDC__

Estos nombres macros han sido usados en muchos sistemas, por ello forman parte de la norma ANSI C. EL macro __TIME__ es usado para representar en forma de hilera la hora del día en la cual se llevó a cabo el proceso de compilación, usando el formato hora:minuto:segundo, mientras que el macro __DATE__ contiene una hilera en la forma mes/dia/año que guarda la fecha de la conversión del código fuente a código objeto. El macro __STDC__ está definido como 1 si se compila un programa C con una opción como ANSI Keywords Only activada en el entorno de desarrollo IDE, de lo contrario el macro permanece indefinido.

Además de los macros predefinidos por ANSI C, eventualmente surgieron otros macros no tan portátiles y universales pero que tienen que ver con características propias de ciertos sistemas computacionales, entre los cuales destacan:

   __TINY__

   __SMALL__

   __COMPACT__

   __MEDIUM__

   __LARGE__

   __HUGE__

Estos macros, de los cuales sólo se puede usar uno durante el proceso de compilación, especifica el modelo de memoria a ser usado para producir el programa ejecutable. Esto tiene que ver directamente con la arquitectura de las computadoras personales caseras basadas en el diseño inicial de computadoras compatibles con el modelo IBM basado en la arquitectura de los procesadores Intel 8086. Conforme fue aumentando la memoria RAM disponible en las máquinas, yendo mucho más allá de lo que en los años setenta pudieran haber imaginado, se tuvo que enfrentar la realidad de que la eficiencia de los programas ejecutables dependía en buena medida del modelo usado para el domiciliamiento de la memoria RAM. Un programa podía ser compilado de seis maneras diferentes, y cada manera organizaba de modo diferente la memoria en la computadora, de acuerdo a los seis modelos de memoria. Con el modelo Tiny todo el código, datos y valores en la pila eran puestos dentro de un mismo segmento de memoria de 64 kilobytes, y producía el código más rápido posible. El modelo Small era el modo predeterminado de compilación en ese entonces, y el segmento de código estaba separado del segmento usado para los datos, la pila, y los segmentos adicionales. El modelo Medium era usado para programas grandes que usaban pocos datos. En el modelo Compact el código del programa estaba restringido a un segmento de memoria pero los datos podían ocupar varios segmentos, y era el modo ideal a usar para programas que requerían grandes cantidades de datos pero poco código para el manejo de los datos. El modelo Large era usado cuando se tenían grandes cantidades de código y grandes cantidades de datos. Y el modelo Huge era lo mismo que el modelo Large excepto que los artículos individuales de datos podían exceder los 64 kilobytes, lo cual ocasionaba un deterioro notorio en la velocidad del procesador.

El asunto de los modelos de memoria usados para compilar programas ejecutables tiene que ver con la capacidad de domiciliamiento que tenían los primeros procesadores CPU comercializados en forma masiva. Se trataba de una capacidad de domiciliamiento de 16 bits, capaz de abarcar un espacio RAM de 64 kilobytes, lo cual era algo grande para los estándares de aquellos tiempos (contrástese con los tiempos actuales en los cuales la memoria RAM de estarse midiendo en el orden de los megabytes ha pasado a ser medida en el orden de los gigabytes). Las computadoras tenían en el hardware la opción de una memoria expandible, lo cual fue ayudad con la introducción de nuevos procesadores Intel capaces de domiciliar espacios de RAM mayores de 64 kilobytes, de modo tal que al aumentar la capacidad de las memorias RAM, en vez de 64 kilobytes se podía tener una máquina con 128 kilobytes de RAM, o sea dos segmentos de memoria. De este modo, más que haber empezado con una gigantesca memoria RAM en el orden de los megabytes que se haya ido segmentando, el asunto de los segmentos de memoria empezó con memorias RAM expandibles que fueron aumentando su capacidad en bancos de memoria vendidos en grupos de 64 kilobytes. Y esto fue requiriendo la adopción sucesiva de modelos de memoria usados en el proceso de compilación de programas C. El lenguaje C no impone modelo de memoria alguno, lo deja al criterio de los fabricantes del hardware y los diseñistas de los sistemas operativos y compiladores.

El siguiente programa dará una buena oportunidad al lector de poner en práctica algunos de los conocimientos adquiridos hasta este punto.

PROBLEMA: Determínese cuál será la salida impresa en una ventana DOS por el siguiente programa C:


   #include <stdio.h>

   #define  MACRO1   "Esta es una hilera definida bajo MACRO1\n"
   #define  NUM  250
   #define  IMPRIMIRN  printf("El valor de NUM es %d\n", m)
   #define  MINIMO(a,b)   ((a)<(b))?(a):(b)
   #define  MAXIMO(a,b)   ((a)>(b))?(a):(b)
   #define  e  2.718281

   main(void) {

     int x, y, m = NUM;

     printf(MACRO1);

     IMPRIMIRN;

   #if  NUM>100
     printf("NUM es mayor que 100\n");
   #else
     printf("NUM es menor o igual que 100\n");
   #endif


   #ifdef  e
     printf("e² = %f\n", e*e);
     printf("El valor definido de e es: %f\n", e);
   #else
     printf("No se ha definido el valor de e\n");
   #endif

   #ifndef  gamma
     printf("No se ha definido el valor de gamma\n");
   #else
     printf("El valor definido de gamma es: %f\n", gamma);
   #endif


   #undef  NUM
   #define  NUM  100

   #if  NUM>100
     printf("NUM es mayor que 100\n");
   #else
     printf("NUM es menor o igual que 100\n");
   #endif

   #if  NUM
     printf("NUM tiene ahora el valor %d\n", NUM);
   #endif

     x = -15, y = 4;
     printf("El minimo es: %d\n", MINIMO(x,y));
     printf("El maximo es: %d\n", MAXIMO(x,y));

   /* Anidacion de directivas */

   #ifdef  e
     #define  VERDADERO  1
     #define  FALSO      0
     printf("Verdadero OR Falso = %d\n", VERDADERO||FALSO);
     printf("Verdadero AND Falso = %d\n", VERDADERO&&FALSO);
     #undef  NUM
     #define  NUM  500
   #endif

     printf("NUM tiene ahora el valor %d\n", NUM);

   #ifndef PI
     #define  PI  3.141592
   #endif

     printf("El valor de PI es: %f\n", PI);

     return 0;

   }


La ejecución del programa puede producir la siguiente salida en una ventana DOS:




Todo lo que  hemos visto acerca del lenguaje C nos proporciona una mejor idea sobre cómo se va llevando a cabo el proceso de conversión de un programa fuente en C al archivo final ejecutable que podemos describir ahora en mayor detalle.

El preprocesador C produce una lista (en un archivo) de un programa fuente (almacenado en un archivo que llamaremos TAREA.C) en el que los archivos de cabecera invocados por el programa fuente con la directiva #include son incluídos como parte del programa C (en realidad, no se incluye el contenido completo de cada archivo de cabecera, se toma de cada archivo de cabecera la declaración del prototipo de cada función utilizada como en el caso del archivo de cabecera STDIO.H del cual se toma la declaración de la función printf() cuando es utilizada), y a su vez los macros especificados como #define son expandidos. Dependiendo del diseño del entorno de compilación, esto suele generar (internamente) un archivo intermedio nuevo con el mismo nombre pero con una nueva extensión (TAREA.I) que consiste en el programa C original pero con los archivos de cabecera ya incluídos y los macros ya expandidos, lo cual es entregado al compilador para que empiece el proceso de compilación, produciéndose otro archivo intermedio nuevo con una nueva extensión (TAREA.OBJ). El último paso consiste en invocar al enlazador (linker) que a su vez llama una librería de módulos para continuar el proceso de compilación, produciéndose un archivo ejecutable con un nombre como TAREA.EXE.

Podemos ilustrar los pasos anteriores con el siguiente diagrama:




El programador puede crear sus propios archivos de cabecera que juzgue necesarios si en algún momento el programador lo juzga conveniente al ir creciendo el tamaño del proyecto en el que está trabajando. A modo de ejemplo, a continuación se muestra un archivo de cabecera que puede ser almacenado por el sistema operativo en el disco duro bajo un nombre como TAREA00.H:

Archivo TAREA00.H:

   #include <stdio.h>

   #define  MACRO1   "Esta es una hilera definida bajo MACRO1\n"
   #define  NUM  250
   #define  IMPRIMIRN  printf("El valor de NUM es %d\n", m)
   #define  MINIMO(a,b)   ((a)<(b))?(a):(b)
   #define  MAXIMO(a,b)   ((a)>(b))?(a):(b)
   #define  e  2.718281

Una vez que se ha hecho lo anterior, se puede guardar el programa principal bajo un nombre como TAREA00.C:

Archivo TAREA00.C:

   #include <tarea00.h>

   main(void) {

     int x, y, m = NUM;

     printf(MACRO1);

     IMPRIMIRN;

   #if  NUM>100
     printf("NUM es mayor que 100\n");
   #else
     printf("NUM es menor o igual que 100\n");
   #endif


   #ifdef  e
     printf("e² = %f\n", e*e);
     printf("El valor definido de e es: %f\n", e);
   #else
     printf("No se ha definido el valor de e\n");
   #endif

   #ifndef  gamma
     printf("No se ha definido el valor de gamma\n");
   #else
     printf("El valor definido de gamma es: %f\n", gamma);
   #endif


   #undef  NUM
   #define  NUM  100

   #if  NUM>100
     printf("NUM es mayor que 100\n");
   #else
     printf("NUM es menor o igual que 100\n");
   #endif

   #if  NUM
     printf("NUM tiene ahora el valor %d\n", NUM);
   #endif

     x = -15, y = 4;
     printf("El minimo es: %d\n", MINIMO(x,y));
     printf("El maximo es: %d\n", MAXIMO(x,y));

   /* Anidacion de directivas */

   #ifdef  e
     #define  VERDADERO  1
     #define  FALSO      0
     printf("Verdadero OR Falso = %d\n", VERDADERO||FALSO);
     printf("Verdadero AND Falso = %d\n", VERDADERO&&FALSO);
     #undef  NUM
     #define  NUM  500
   #endif

     printf("NUM tiene ahora el valor %d\n", NUM);

   #ifndef PI
     #define  PI  3.141592
   #endif

   printf("El valor de PI es: %f\n", PI);

   return 0;

   }


Obsérvese que lo único que se hizo fue “romper” en dos archivos distintos el programa de práctica dado arriba, poniendo los enunciados #define en un archivo junto con el #include del archivo de cabecera STDIO.H en un archivo llamado TAREA00.H, y poniendo el programa principal en otro archivo llamado TAREA00.C. Obsérvese que las directivas al preprocesador C para llevar a cabo compilación condicional fueron puestas en el programa principal, en el archivo TAREA00.C. En realidad, las directivas al preprocesador C se pueden poner en cualquier parte, el único requisito es que tienen que ser colocadas antes del punto en donde serán utilizadas por el programa. El archivo TAREA00.C que guarda al programa principal no incluye al archivo de cabecera STDIO.H puesto que éste ya fue incluído en el archivo de cabecera TAREA00.H que a su vez es invocado dentro del archivo principal con:

#include <tarea00.h>

Hemos subdividido un programa en dos partes distintas, en dos archivos, pero conforme vaya creciendo el programa convirtiéndose en un asunto de gran envergadura requiriendo no de dos o tres sino incluso de varias docenas de archivos distintos, el asunto deja ya de ser la simple compilación de un archivo para convertirse en lo que llamamos un proyecto. Y aunque podemos manejar todo el asunto directamente desde una ventana de líneas de comandos tipo DOS (para lo cual se requiere de utilerías como las que están disponibles en sistemas tipo UNIX), los entornos de programación C ofrecen la facilidad de poder construír proyectos complejos. En rigor de verdad, y aunque en la fase de aprendizaje y familiarización estos entornos de programación den la apariencia de ser juguetes con escasa capacidad para cosas mayores, de hecho han sido usados y siguen siendo usados por programadores profesionales que se las tienen que ver con proyectos enormes que involucran miles de líneas de código, proyectos tales que por su propia naturaleza requieren de una subdivisión del trabajo en donde hay no solo uno sino varios programadores que forman parte de un equipo, cada uno de ellos trabajando en una parte del proyecto sobre las especificaciones que le proporciona el administrador de proyectos elaborando el equivalente en software de “cajas negras” en las que el administrador de proyectos tal vez apenas tenga una idea vaga sobre cómo funcionan en todos sus detalles, y al mismo tiempo con cada programador ignorando en detalle cómo funcionan las otras “cajas negras” creadas por los otros programadores que están a su nivel. Si la idea de que en el proyecto de la elaboración de algo como un programa de diseño gráfico AutoCAD nadie sepa absolutamente todo lo que está sucediendo en todas las partes que forman el programa completo parece preocupante, tómese en cuenta que lo mismo ha ocurrido con el hardware en donde la integración paulatina de cantidades mayores de componentes electrónicos en una pastilla de circuito integrado ha hecho que se pierda casi por completo la noción de lo que está realizando cada transistor individual de los millones de transistores que forman parte del circuito integrado, y en donde la interconexión de estos circuitos integrados equivale también al ensamble de cajas negras en las cuales los que las usan rara vez tienen idea sobre la electrónica que hace posible tales cajas negras de hardware.