DIRECTIVAS DE ANSI C


¿De qué va esto del preprocesador?


     El preprocesador que incluye todo compilador de C es una potente y flexible herramienta que usada adecuadamente nos obsequia con un montón de facilidades. Una vez que se prueba no puede dejar de usarse. Todo programador de C ha usado alguna vez las facilidades mínimas, que son la inclusión de ficheros y la definición de constantes, pero esto es sólo la punta del iceberg. Hay mucho más que descubrir.

     Conceptualmente el preprocesado es un primer paso separado durante la compilación. Es una manera de incluir instrucciones dirigidas al compilador dentro del código fuente, mediante el uso de directivas. El estándar ANSI C contempla 12. Cualquier compilador debe implementar estas 12, pero puede añadir más. Desde aquí, no sugerimos el uso de características no estándar, ya que ataremos nuestro código a un compilador específico. Siempre es mejor realizar código portable. Pero esto, claro está, sólo depende de lo que uno desee hacer.

     En este artículo, daremos un paseo por las directivas éstandar y plantearemos unas cuantas formas útiles de usarlas, rematando la faena con un ejemplo real sacado del código fuente del sistema operativo NetBSD.





Pero... ¿cómo se usa?



     El preprocesador se basa en el uso de un juego de directivas. Son fácilmente reconocibles por su formato escritas con una almohadilla: #directiva.

 1. #include

     Esta directiva obliga al compilador a incluir en el fichero fuente el contenido del fichero indicado por la directiva. Hay dos maneras de hacer esto:


 #include "nombre_de_fichero"
 #include <nombre_de_fichero>



     Si se especifica la ruta explícita de un fichero, el compilador sólo buscará el fichero en ese directorio.Si por el contrario sólo indicamos el nombre del archivo o una ruta parcial, el comportamiento dependerá de si hemos usado comillas o ángulos. En el caso de las comillas, primero se busca en el directorio actual, si no se encuentra el fichero, se busca en los directorios indicados en la linea de comandos y si aún así falla la búsqueda, se iría a los directorios estándar. Si hemos usado los ángulos, primero se busca en los directorios especificados por la opción -I de la linea de comandos (p.ej cc fichero.c -Idirectorio_de_includes) y si falla se busca en los directorios estándar.

     Es posible incluir ficheros en cualquier parte del código, pero no es habitual. Suele usarse únicamente para inclusión de ficheros de cabecera al comienzo del fichero fuente. Es importante recordar que se permite el anidamiento de inclusiones, es decir, que un fichero incluído, contenga a su vez directivas #include.


 #include 

 main(void) {

     #include "mi_fichero"
 }



 2. #define

     Se usa para definir un identificador y una cadena que sustituirá al identificador cada vez que este aparezca en el código. La forma general de la directiva es:


 #define nombre_de_macro cadena



     La cadena, cuya presencia no es obligatoria, comienza un espacio después del identificador nombre_de_macro y acaba con un salto de linea. No es necesario el uso de ; para indicar el fin de la cadena. De hecho, si incluimos un ; estamos indicándole al compilador que forma parte de cadena. Hay dos usos básicos de #define, definición de constantes y definición de macros.


 #define DEFINICION_SIN_VALOR
 #define FALSE 0
 #define TRUE FALSE+1
 #define MSG_TRUE "True\n"
 #define MSG_FALSE "False\n"

 int var = FALSE;

 if (var == TRUE)
     printf(MSG_TRUE);
 else
     printf (MSG_FALSE);



     En el ejemplo anterior hemos creado cuatro constantes, y vemos como pueden ser usadas en nuestro código fuente. Ahora bien, hay que tener en cuenta que no se realizan sustituciones cuando nombre_de_macro está dentro de una cadena como en printf("TRUE"); y que en cadena podemos usar definiciones previas y usar expresiones que sean validas en ANSI C, como es una suma, etc.

     La definición de macros es sin duda el uso más potente de #define:


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

     printf("máximo es %d", max(10,20));

 #define pot(x) x*x

     printf("resultado = %d", pot(2+2));



     En el primer caso hemos definido una macro para calcular el máximo de dos cosas comparables. Esta es una de las grandes ventajas de una macro, no es necesario declarar explícitamente el tipo de los parámetros, es responsabilidad del programador dar los correctos. max(a,b) es igual de válido para a y b enteros y flotantes, por ejemplo.

     El segundo ejemplo, la macro potencia, contiene un error a propósito, para ilustrar la necesidad de parentizar correctamente. Tal y como está, el resultado del código anterior es printf("resultado = %d",2+2*2+2) lo que imprimiría por pantalla 8, lo que no es 42. La macro de abajo si imprimiría correctamente el resultado.


 #define pot(x) (x)*(x)



     Un último detalle, al que hay que prestar bastante atención y que provoca bastantes fallos. Cuando la cadena es muy larga, puede partirse en varias lineas usando el carácter de barra invertida \. Pero OJO,NO DEBEN DEJARSE ESPACIOS EN BLANCO DESPUÉS DE \es necesario terminar la linea con intro, para que el preprocesador sepa que la macro continúa en la línea siguiente.


 #define macro(x) opera(x);    \
                  imprime(x);



 3. #undef

     Sirve para eliminar una definición previa.


 #undef nombre_de_macro



     Toda definición es válida hasta que se encuentra su #undef correspondiente. Aunque no parece muy útil, de vez en cuando se desea restringir el alcance de una definición.


 #define PI 3.1416
     /* Aqui código que usa PI */
 #undef PI
     /* Aqui código que no usa PI */



 4. Directivas de compilación condicional

     Son seis #if,#else,#elif,#endif,#ifdef y #endif. La sintaxis es la que sigue:


 #if expresión_1
     sentencias_1
 #elif expresión_2
     sentencias_2
 #else
     sentencias_3
 #endif

 #ifdef nombre_de_macro
     sentencias
 #endif



     Si se cumple la condición, todas las sentencias de ese bloque condición se incluyen en el código a la hora de compilar. En el primer caso, usando #if, podemos observar que tiene la misma estructura que el if-else de ANSI C, y, de hecho, se rige por los mismos principios: se pueden obviar las ramas #elif y #else. La expresión que se usa, se evalúa en tiempo de compilación por tanto debe contener constantes e identificadores definidos previamente.#ifdef se usa para preguntar si se ha definido algo, o su contrapartida #ifndef si no se ha definido. Pero veamos un ejemplo donde se use todo esto:


 #include 

 #define A 100
 #define GATO

 main(void) {

     #if A>50
         printf("Compilando con A > 50);
     #else
         printf("Compilando con A <= 50);
     #endif

     #ifdef GATO
         printf("Se ha definido GATO");
     #endif
 }




 5. #error

     Se usa para depuración, su sintaxis es:


 #error mensaje_de_error



     Mensaje de error no va entre comillas. Cuando el compilador llega a esta línea se detiene la compilación y se imprime el siguiente mensaje "Fatal: Archivo Línea Error directiva:mensaje_de_error". No es habitual, ya que rara vez es necesario detener una compilación.

 6. Macros predefinidas


 __LINE__ número de línea que se está compilando actualmente
 __FILE__ nombre del fichero que se está compilando actualmente
 __DATE__ fecha en que se pasó de código fuente a objeto en formato mes/día/año
 __TIME__ tiempo transcurrido entre comienzo de compilación y paso por la macro
 __STDC__ contiene la constante decimal 1, indicando que la implementación del compilador se ajusta al estándar ANSI C, de no ser así, contiene otro número



     Tienen interés para informar. Por ejemplo, si en un error de ejecución quisiésemos saber en que línea de código fuente y fichero está la instrucción que lo provoca, deberíamos incluir un mensaje de error del tipo:


 printf("Error en %s linea %d",__FILE__,__LINE__);



 7. #line

     Se usa para modificar el contenido de __LINE__ y __FILE__.


 #line número nombre_de_archivo



     Número puede ser cualquier entero positivo. nombre_de_archivo es opcional y es cualquier identificador de archivo válido. Realmente su uso es muy raro, salvo para depuración y aplicaciones especiales.

 8. #pragma

     La hemos dejado para el final, no porque sea la menos importante, sino porque se desmarca un poco del resto de directivas en cuanto a su funcionamiento. Es una directiva definida por el estándar como dependiente de la implementación. Se usa para pasarle instrucciones al compilador. Estas intrucciones dependen de dicho compilador, y el estándar no define ninguna. Su uso ata el código a un compilador y debe tenerse cuidado con ellas si se desea realizar código portable.

     Un ejemplo de directiva de este estilo, puede ser...


 #pragma inline



     ... que es una directiva que definen los compiladores TURBO C para indicar que se va a usar ensamblador en línea dentro del código fuente C.





Vale, eso es la teoría, pero a la hora de la verdad...



     Lo cierto, es que lo escrito arriba no dista de lo que pone en cualquier libro. Es todo lo que hay que saber, pero es insuficiente para ver hasta qué punto puede ser útil el preprocesador. Por este motivo, expondremos ahora una serie de ejemplillos que son útiles y se usan en la vida real.


 Problemas de inclusión

     Cuando estamos añadiendo ficheros de cabecera estándar, muchas veces duplicamos las inclusiones, y el compilador no protesta, aunque "teóricamente redefinamos" cosas. Sin embargo, cuando incluímos nuestros ficheros, sí que hay problemas. Redefinición de tipos, variables ya definidas, etc. ¿Cuál es el sistema que usa el compilador con los .h estándar? ¿Cuál es el increíble secreto de los programadores profesionales para evitar esto?...


 /* Este es un fichero fichero.h */
 #ifndef _FICHERO_H_
 #define _FICHERO_H_

     /* Aqui el código */

 #endif



     La magia es fácil. El truco consiste en hacer una definición cuando se incluye el fichero. Esto, combinado con el #ifndef nos asegura que sólo se incluirá una vez el código de cabecera. Una vez que se aprende, esta estructura se convierte en un gesto natural a la hora de programar un fichero de cabecera.

 Compilación condicional

     Muchas veces, interesa tener varias versiones de un mismo código en un solo juego de ficheros fuente. A la hora de hacer un código, por ejemplo, es útil insertar código de depuración, pero que no es necesario una vez que el programa llegua al usuario final. Solución: compilación condicional.

     Un ejemplo de código de depuración puede ser:


 #include 
 #include "mis_funciones.h"

 int main(void) {

     int x = 10 ,y = 20, z ;

     z = mi_suma(x,y);

     #ifdef DEBUG
         printf("DEBUG: mi_suma(x = %d,y = %d) = z = %d",x,y,z);
     #endif

     return 0;
 }



     Debemos tener en cuenta que muchos compiladores aceptan la definición de macros a través de la línea de comandos, lo cual nos evita tener que escribir un #define DEBUG dentro del mismo código. Podemos pasarlo en la línea de comandos, lo cual va muy bien con ficheros de herramientas de compilación como Make. En gcc (que viene con todas las distribuciones Linux) se usa la opción -D.


 gcc miejemplo.c -DDEBUG



     También es interesante compilar condicionalmente cuando se quiere realizar código portable pero debemos usar alguna característica de un compilador en especial...


 #ifdef COMPILADOR_1
     #pragma directiva_1_1
     #pragma directiva_1_2
     #pragma directiva_1_3
 #ifdef COMPILADOR_2
     #pragma directiva2_1
     #pragma directiva2_2
 #endif



 Macros

     Las macros son muy útiles para realizar tareas comunes, que se repiten por todo el código. Si las resumimos adecuadamente en una macro, obtenemos varias ventajas. Reducimos los errores, ya que no estamos duplicando código a mano propenso a fallos de escritura. Aumentamos la legibilidad, ya que reducimos estructuras complejas a un nombre descriptivo. Hacemos fácil el cambio y la modificación de dicha estructura, al encontrarse en un solo punto del código.

     Un ejemplo, sería una macro para reservar memoria de cualquier tipo.


 #define RESERVAMEM(TIPO,VAR,TAM)                                    \
         if(( VAR = (TIPO *) malloc( TAM * sizeof (TIPO)))==NULL)    \
             { printf("Error al reservar memoria\n");                \
               exit(0); }


 #define RESERVAMEM2(TIPO,VAR,TAM1,TAM2)        \
         do {                                   \
             int i;                             \
             RESERVAMEM(TIPO *,VAR,TAM1);       \
             for (i=0;i
                 RESERVAMEM(TIPO,VAR[i],TAM2);  \
         } while(0);

     /* Reservamos memoria para un array de 100 enteros
        apuntado por a y para una matriz de 10x10 flotantes
        apuntada por b */

     int *a;
     float **b;

     RESERVAMEM(int,a,100);
     RESERVAMEM(float,b,10,10);



     Aqui,además, podemos ver como es posible usar macros dentro de macros. Lo que resulta francamente útil. Hay varias cosas que debemos tener en cuenta respecto a la sintaxis. Los espacios en blanco no son malos. En la macro anterior, hemos usado float* y float** sin escribirlo realmente y partiendo de float gracias a que el compilador salta los caracteres blancos. Por otro lado están los puntos y coma, siempre es mejor tener ;; que el compilador interpreta como una sentencia vacía que tener un código erróneo. Pero lo más importante es el uso de la condición nula. Si queremos crear macros realmente complejas y que funcionen correctamente la mejor opción es crear un bucle do-while que solo itere una vez. Esto nos permitirá tener un subbloque de código con lo que podremos crear variables locales como el iterador i del ejemplo.


 Alias y pseudofunciones

     En ciertas aplicaciones es interesante tener una función genérica con múltiples parámetros y permitir que el usuario emplee esa función de modo simplificado. Este es el modo de funcionamiento de la librería curses de UNIX/Linux. Librería que se usa para controlar la salida gráfica por terminal.


 ...
 #define move(y,x) mvwin(stdscr,y,x)
 ...



     El fichero de cabecera de esta librería, por ejemplo, define funciones que se aplican a la ventana principal stdscr pero que son aplicaciones de funciones más genéricas que se aplican a cualquier ventana (como se ve en el ejemplo con las funciones de mover el cursor). Otras ocultan el hecho de que se pasan por referencia,etc.


 La madre de todas las macros: "el ejemplo" en NetBSD

     El último ejemplo, está sacado del código fuente del sistema operativo NetBSD. Es parte de un fichero que define varios tipos de colas usando única y exclusivamente macros. Es usado principalmente en un componente fundamental del sistema: Buffer Caché

     Aunque, a simple vista, parecería razonable crear un módulo con funciones que hiciesen las operaciones típicas de las colas, debemos pensar que esto es código del sistema operativo y queremos máxima velocidad. Cada llamada a una función implica crear un nuevo frame en la pila del sistema, salvar los registros del micro, ejecutar y recuperar los registros de la pila anterior. Por tanto, llamar a una función implica una sobrecarga en tiempo. En una aplicación crítica, esto no es deseable, y es mejor replicar código. Aumentamos el tamaño de los binarios pero ganamos velocidad.

No hay comentarios:

Publicar un comentario