Direttive al Preprocessore
Cos'é il preprocessore ?
In C++ (come in C), prima che il compilatore inizi a lavorare, viene attivato un programma, detto preprocessore, che ricerca nel file sorgente speciali istruzioni, chiamate direttive.
Una direttiva inizia sempre con il carattere # (a colonna 1) e occupa una sola riga (non ha un terminatore, in quanto finisce alla fine della riga; riconosce però i commenti, introdotti da // o da /*, e la continuazione alla riga successiva, definita da \).
Il preprocessore crea una copia del file sorgente (da far leggere al compilatore) e, ogni volta che incontra una direttiva, la esegue sostituendola con il risultato dell'operazione. Pertanto il preprocessore, eseguendo le direttive, non produce codice binario, ma codice sorgente per il compilatore.
Ogni file sorgente, dopo la trasformazione operata dal preprocessore, prende il nome di translation unit. Ogni translation unit viene poi compilata separatamente, con la creazione del corrispondente file oggetto, in codice binario. Spetta al linker, infine, collegare tutti i files oggetto, generando un unico programma eseguibile.
Nel linguaggio esistono molte direttive (alcune delle quali dipendono dal sistema operativo). In questo corso tratteremo soltanto delle seguenti: #include , #define , #undef e direttive condizionali.
Direttiva #include
Ci é già noto il significato della direttiva #include:
#include <filename> oppure #include "filename"
che determina l'inserimento, nel punto in cui si trova la direttiva, dell'intero contenuto del file con nome filename.
Se si usano le parentesi angolari, si intende che filename vada cercato nella directory di default del linguaggio; se invece si usano le virgolette, il file si trova nella directory del programma.
La direttiva #include viene usata quasi esclusivamente per inserire gli header-files (.h) ed é particolarmente utile quando in uno stesso programma ci sono più implementation-files che includono lo stesso header-file.
Direttiva #define di una costante
Quando il preprocessore incontra la seguente direttiva:
#define identificatore valore
dove, identificatore è un nome simbolico (che segue le regole generali di specifica di tutti gli altri identificatori) e valore é un'espressione qualsiasi, delimitata a sinistra da blanks o tabs e a destra da blanks, tabs o new-line (i blanks e tabs interni fanno parte di valore), sostituisce identificatore con valore in tutto il file (da quel punto in poi).
Es. #define bla frase qualsiasi anche con "virgolette"
sostituisce (da quel punto in poi) in tutto il file la parola bla con la frase: frase qualsiasi anche con "virgolette" (la "stranezza" dell'esempio riportato ha lo scopo di dimostrare che la sostituzione é assolutamente fedele e cieca, qualunque sia il contenuto dell'espressione che viene sostituita all'identificatore; il compito di "segnalare gli errori" viene lasciato al compilatore!)
In generale la direttiva #define serve per assegnare un nome a una costante (che viene detta "costante predefinita").
Es. #define ID_START 3457
da questo punto in poi, ogni volta che il programma deve usare il numero 3457, si può specificare in sua vece ID_START
Esistono principalmente due vantaggi nell'uso di #define:
se il programmatore decide di cambiare valore a una costante, é sufficiente che lo faccia in un solo punto del programma;
molto spesso i nomi sono più significativi e mnemonici dei numeri (oppure più brevi delle stringhe, se rappresentano costanti stringa) e perciò l'uso delle costanti predefinite permette una maggiore leggibilità del codice e una maggiore efficienza nella programmazione.
In pratica la direttiva #define produce gli stessi risultati dello specificatore di tipo const; al posto della direttiva dell'esempio precedente si sarebbe potuto scrivere la dichiarazione:const int ID_START = 3457;
Confronto fra la direttiva #define e lo specificatore const
Vantaggi nell'uso di const:
il tipo della costante é dichiarato; un eventuale errore di dichiarazione viene segnalato immediatamente;
la costante é riconosciuta, e quindi analizzabile, nelle operazioni di debug.
Vantaggi nell'uso di #define:
una costante predefinita a volte è più comoda e immediata (è una questione sostanzialmente "estetica"!) e può essere usata anche per altri scopi (per esempio per sostituire o mascherare nomi).
Direttiva #define di una macro
Quando il preprocessore incontra la seguente direttiva:
#define identificatore(argomenti) espressione
riconosce una macro, che distingue dalla definizione di una costante per la presenza della parentesi tonda subito dopo identificatore (senza blanks in mezzo).
Una macro é molto simile a una funzione. Il suo uso é chiarito dal seguente esempio:
#define Max(a,b) a > b ? a : b
tutte le volte che il preprocessore trova nel programma una chiamata della macro, per esempio Max(x,y), la espande, sostituendola con: x > y ? x : yCome nel caso di definizione di una costante, anche per una macro la sostituzione avviene in modo assolutamente fedele: a parte i nomi degli argomenti, che sono ricopiati dalla chiamata e non dalla definizione, tutti gli altri simboli usati nella definizione sono riprodotti senza alcuna modifica (per esempio il punto e virgola di fine istruzione viene messo solo se compare anche nella definizione).
Nella chiamata di una macro si possono mettere, al posto degli argomenti, anche delle espressioni (come nelle chiamate di funzioni); sarà compito, come al solito, del compilatore controllare che l'espressione risultante sia accettabile. Riprendendo l'esempio precedente, la seguente chiamata:
Max(x+1,y) espansa in x+1 > y ? x+1 : y
sarà accettata dal compilatore, in istruzioni del tipo :
c = Max(x+1,y);
ma rigettata in istruzioni come:
Max(x+1,y) = c;
in quanto, in questo caso, gli operandi di un operatore condizionale devono essere l-values.In altri casi, la sostituzione "cieca" può causare errori che lo stesso compilatore non é in grado di riconoscere.
Es. #define quadrato(x) x*x
la chiamata: quadrato(2+3) viene espansa in 2+3*2+3 con risultato, evidentemente, errato.
Per evitare tale errore si sarebbe dovuto scrivere: #define quadrato(x) (x)*(x)Agli effetti pratici (purché si usino le dovute attenzioni!), la definizione di una macro produce gli stessi risultati dello specificatore inline di una funzione.
Confronto fra la direttiva #define e lo specificatore inline
Vantaggi nell'uso di inline:
il tipo della funzione é dichiarato e controllato ;
la funzione é riconosciuta, e quindi analizzabile, nelle operazioni di debug;
l'espansione di una funzione inline é fatta non in modo "cieco", ma in modo "intelligente" (vantaggio decisivo!).
Vantaggi nell'uso di #define:
Una macro e più immediata e più semplice da scrivere di una funzione.
Le macro, usatissime in C, sono raramente utilizzate in C++, se non per funzioni molto brevi e adoperate a livello locale (cioè nello stesso modulo in cui sono definite). Un uso più frequente delle macro si ha quando non corrispondono a funzioni ma a espressioni "parametrizzate" molto lunghe che compaiono più volte nel programma.
Direttive condizionali
Il preprocessore dispone di un suo mini-linguaggio di controllo, che consiste nelle seguenti direttive condizionali:
#if espressione1 oppure #if defined(identificatore1) oppure ... #if !defined(identificatore1) ...... blocco di direttive e/o istruzioni ........
#elif espressione2 oppure #elif defined(identificatore2) oppure ... #elif !defined(identificatore2) ...... blocco di direttive e/o istruzioni ........
#else ...... blocco di direttive e/o istruzioni ........
#endif
dove: espressione é un espressione logica che può contenere solo identificatori di costanti predefinite o costanti literals, ma non variabili e neppure variabili dichiarate const, che il preprocessore non riconosce
defined(identificatore) restituisce vero se identificatore é definito (cioè se é stata eseguita la direttiva: #define identificatore); al posto di #if defined(identificatore) si può usare la forma: #ifdef identificatore
!defined(identificatore) restituisce vero se identificatore non é definito; al posto di #if !defined(identificatore) si può usare la forma: #ifndef identificatore
#elif sta per else if ed é opzionale (possono esserci più blocchi consecutivi, ciascuno introdotto da un #elif)
#else é opzionale (se esiste, deve introdurre l'ultimo blocco prima di #endif)
#endif (obbligatorio) termina la sequenza iniziata con un #if
non é necessario racchiudere i blocchi fra parentesi graffe, perché ogni blocco é terminato da #elif, o da #else, o da #endif
Il preprocessore identifica il blocco (se esiste) che corrisponde alla prima condizione risultata vera, oppure il blocco relativo alla direttiva #else (se esiste) nel caso che tutte le condizioni precedenti siano risultate false. Tale blocco può contenere sia istruzioni di programma che altre direttive, comprese direttive condizionali (possono esistere più blocchi #if "innestati"): il preprocessore esegue le direttive e presenta al compilatore le istruzioni che si trovano nel blocco selezionato, scartando sia direttive che istruzioni contenute negli altri blocchi della sequenza #if ... #endif.
Direttiva #undef
La direttiva:
#undef identificatore
indica al preprocessore di disattivare l'identificatore specificato, cioè rimuovere la corrispondenza fra l'identificatore e una costante, precedentemente stabilita con la direttiva:
#define identificatore costante
Nelle istruzioni successive alla direttiva #undef, lo stesso nome potrà essere adibito ad altri usi.
Es. #ifdef EOF #undef EOF #endif char EOF[] = "Ente Opere Filantropiche";