Direttive per il preprocessore

Da Hacknowledge.

Ogni compilatore traduce le istruzioni di un file sorgente in linguaggio macchina. Il programmatore generalmente non è consapevole del lavoro del compilatore: si fornisce delle istruzioni di un linguaggio di alto livello per evitare le complessità gestionali del linguaggio macchina. Ma, comunque, è importante poter comunicare con il compilatore. Il C fa uso del preprocessore per estendere la sua potenza e la sua notazione, consentendo al programmatore un'interazione con il compilatore. L'identificatore delle righe che riguardano le direttive ad esso è #, che nel C ANSI può essere anche preceduto da spazi mentre nel C Tradizionale deve trovarsi all'inizio della riga. Le direttive non fanno comunque parte della grammatica del linguaggio, ampliano solo l'ambiente di programmazione. Per lo standard ANSI, le direttive sono le seguenti:

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

Indice

[modifica] La direttiva #include

Solitamente anche nei programmi più banali si usa la direttiva #include per, appunto, includere nel sorgente file esterni o librerie.

Per includere una libreria si usano le parentesti angolari < e >, mentre per includere un file esterno o magari nella stessa cartella del programma si usano i doppi apici ".

Un esempio di inclusione di una libreria e un file che si trova nella cartella superiore di dove si trova il sorgente in cui la includiamo:

#include <stdio.h>
#include "../file1.h"

In questo caso il preprocessore quando incontrerà queste righe le sostituirà con il contenuto del file richiamato.

In Unix solitamente i file d'intestazione specificati nelle parentesi angolari si trovano nel percorso /usr/include/.

Nei file inclusi possono naturalmente anche esserci altre direttive al preprocessore che verranno poi a loro volta "lavorate".

[modifica] La direttiva #define

La direttiva #define si usa, appunto, per definire qualcosa ad esempio:

#define scrivi printf

In questo caso la definizione è scrivi che va a sostituire la parola printf quindi nel corso del programma al posto di:

printf("Ciao preprocessore!");

Si potrà scrivere

scrivi("Ciao preprocessore!");

Comunque il define può anche definire numeri, simboli o altro. Vari esempi di define:

#define EQ  ==
 #define OK  printf("OK\n");
 #define DEBUG 1

Nell'esempio sopra visualizzato si può notare il cosidetto "ZUCCHERO SINTATTICO" dato che ogni tanto a un programmatore in C può scappare di mettere un solo = nelle uguaglianze così con la definizione EQ == si potrà scrivere così:

if ( a EQ b ) ...

Evitando errori di sintassi.

Un'altra cosa da notare è la definizione DEBUG molto utile nelle fasi di test di un programma che si può usare nel controllo del flusso tramite sempre direttive al preprocessore che vedremo adesso.

[modifica] Controllo del flusso

Con le direttive al preprocessore si può esegure anche un flusso del controllo ( if, else ) utilizzando le direttive #if, #else, #elif, #endif e #ifdef.

Iniziamo a spiegarli dai primi cioè #if, #else, #elif e #endif che corrispondo al controllo del flusso normalmente utilizzato: if, else, else if mentre l'ultimo #endif è "originale" del preprocessore.

Esempio:

#include <stdio.h>
  #define A 2
  #define B 4
 
  int main(void)
  {
 
    #if A == 2 || B == 2
      printf("A o B sono uguali a 2\n");
    #elif A == 4 && B == 4
      printf("A e B sono uguali a 4\n");
    #elif A != B
      printf("A è diversa da B\n");
    #else
      printf("A e B sono di un valore non definito\n");
    #endif
 
    return 0;
  }

Si possono notare le seguenti cose:

  • Le variabili su cui eseguire controlli devono essere definite tramite #define
  • Anche nel controllo del flusso tramite direttive al preprocessore si possono eseguire controlli con || ( OR ), && ( AND ) e != ( NOT ).
  • La direttiva #endif "dice" al preprocessore che il controllo del flusso è finito.

Per eseguire un debug con questo sistema si potrebbe inserire qualcosa tipo:

#if DEBUG 1
    printf("x = %d\n", x);
    printf("y = %s\n", y);
    ...
  #endif

Ma ora vedremo con la direttiva #ifdef cosa si può fare, in pratica "ifdef" sta per "se definito" quindi si può tramite essa controllare se una variabile è stata definita o meno e con l'aggiunta delle direttive #undef e #ifndef vedremo cosa si può fare con l'esempio seguente:

#include <stdio.h>
  #define  NUMERO 4
 
  int main(void)
  {
   #ifndef NUMERO
    #define NUMBER 4
   #ifdef NUMBER
    #undef NUMBER
    #define NUMERO 4
   #endif
    return 0;
   }

Innanzitutto chiariamo cosa vuol dire ifndef e undef, la prima equivale a "se non è definito" ( if not define ) mentre la seconda equivale a "togli la definizione" ( undefine ).

Nell'esempio sopra definiamo NUMERO dopodichè all'interno del corpo main iniziamo col verificare se non è definito numero, se ciò è vero definiamo NUMBER, se invece è definito NUMBER togliamo la definizione di NUMBER e definiamo NUMERO. Dopodichè si esce dal programma.

La direttiva #undef diciamo che è inutile nei piccoli programmi, ma risulta utilissima nei programmi di grandi dimensioni composti magari da molti file e da molte persone che ci lavorano e senza andare in giro o sfogliare tra i file se una cosa è stata definita o meno questa semplice direttiva ci facilita la vita.

L'uso di #ifdef è utilissimo nel caso in cui si vogliano usare dei file header. Infatti, un file header potrebbe essere incluso in due diversi file sorgenti che si vanno a compilare insieme, e questo potrebbe generare ambiguità ed errori in fase di compilazione (funzioni o dati che risulterebbero dichiarati due volte). Per evitare questo problema si usano proprio le direttive al preprocessore. Nell'header che andremo a creare avremo una cosa del genere:

  • Se la variabile _NOMEHEADER_H non è definita
  • Definisci la variabile
  • Dichiara tutto il contenuto dell'header
  • Altrimenti, termina la dichiarazione dell'header

In codice:

#ifndef  _MIOHEADER_H
#define  _MIOHEADER_H
 
// Qui metto tutte le mie funzioni e i miei dati
 
#endif

In pratica, una volta definita la macro _MIOHEADER_H il file header non verrà più incluso in nessun altro file, risolvendo quindi gli eventuali problemi di header definiti due o più volte.

[modifica] Macro con parametri

Con la direttiva #define si possono definire macro con parametri. Il "come" lo vedremo nell'esempio seguente:

#define PD(x) ((x) * (x))

PD sta per "Potenza di Due" in pratica se nel corso del programma si esegue qualcosa tipo:

a = PD(3);

il preprocessore la espandera in questo modo:

a = ((3) * (3));

Un'altro esempio per concludere aggiungerei:

#define minore(x, y) (((x) < (y)) ? (x) : (y))

Questa definizione una volta richiamata rilascia il valore più piccolo tra i due numeri dati. Naturalmente l'utilizzo epsanso di parentesi consente di essere sicuri che una volta espansa la stringa tutte le operazioni vengano eseguite nel modo desiderato.

[modifica] Macro predefinite

Nel C esistono 5 tipi di macro già definite sempre disponibili che non possono essere ridefinite dal programmatore. Si possono vedere nello schema seguente:

/* MACRO    ||  COSA CONTIENE */
   __DATE__   /* Una stringa che contiene la data corrente */
   __FILE__   /* Una stringa che contiene il nome del file */
   __TIME__   /* Una stringa che contiene l'ora corrente */
   __LINE__   /* Un intero che raprresenta il numero di riga corrente */
   __STDC__   /* Un intero diverso da 0 se l'implementazione segue lo standard ANSI C */

[modifica] Operatori # e ##

Questo tipo di operatori sono disponibili solo nel C ANSI. L'operatore unario # trasforma un parametro formale di una definizione di macro in una stringa ad esempio:

#define nomi(a, b) printf("Ciao " #a " e " #b "! Benvenuti!\n");

da richiamare nel corpo main con:

nomi(HdS619, BlackLight);

Una volta espanso dal preprocessore questa linea diventerà:

printf("Ciao " "HdS619"  " e " "BlackLight" "! Benvenuti!\n");

Ora invece vediamo l'operatore binario ## che serve a concatenare token. Ad esempio:

#include <stdio.h>
  #define X(y) x ## y
  X(3) = X(4) = X(12) = ...

verrà espanso in:

x3 = x4 = x12 = ...

In pratica si può pensare che "colleghi" i due parametri x e y.

[modifica] Direttive #error e #warning

Le direttive #error e #warning servono rispettivamente per dare errori nella compilazione oppure avvisi.
Solitamente queste due direttive vengono usate insieme a quelle che controllano il "flusso di compilazione" ( #else, #if, #undef, ecc... ).
La loro sintassi è la seguente:

 #error   Messaggio di errore
 #warning Messaggio di avvertimento
Strumenti personali