Il preprocessor legge un sorgente C e produce in output un altro sorgente C, dopo avere espanso in linea le macro, incluso i file e valutato le compilazioni condizionali o eseguito altre direttive.
Una direttiva inizia sempre con il carattere pound
'#' eventualmente preceduto e/o seguito da
spazi. I token seguenti definiscono la direttiva ed il suo comportamento.
Una direttiva al preprocessor puo' comparire in qualsiasi punto del sorgente
in compilazione ed il suo effetto permane fino alla fine del file.
Per ulteriori approffondimenti sul preprocessore C, il consiglio e' di fare riferimento all'appendice A.12 del 'Linguaggio C'
Le macro possono essere definite anche in forma paramentrica; in tal caso
la sostituzione dei parametri formali con quelli attuali avviene in modo
testuale durante la fase di espansione della macro.
Nel definire una macro risulta di fondamentale importanza scrivere i
parametri formali fra parentesi tonde, poiche' anche nel caso che gli
argomenti attuali fossero delle espressioni, l'espansione risulta essere
ancora corretta.
Nei commenti dell'esempio sopra riportato sono state espanse le macro come
verrebbero compilate, con a fianco il risultato atteso.
Una macro deve essere definita su una unica riga del file sorgente.
Se un parametro formale e' preceduto dal carattere pound #, il suo
valore attuale e' espanso testualmente come stringa.
E' anche possibile annullare la definizione di una macro con la direttiva
#undef. Di solito #undef e' impiegata per assicurarsi
che una funzione sia definita come tale, piuttosto che come macro. Un altro
possibile impiego di #undef e' per la gestione della
compilazione condizionale.
Tipicamente sono predefinite 5 macro:Espansione in linea delle macro
Definire una macro significa associare una stringa ad un
identificatore.
Ogni volta che il preprocessore C incontra l'identificatore cosi' definito,
esegue la sua sostituzione in linea, con la stringa ad esso associata.
La definizione delle macro avviene per mezzo della direttiva
#define
Esempio:
#define MAX 100
#define STRING_ERR "Rilevato errore !\n"
Esempio:
#define SWAP(tipo,x,y) {tipo t; t=(x); (x)=(y); (y)=t;}
...
SWAP(int,a,b); /* espansione in {int t; t=(a); (a)=(b); (b)=t;} */
SWAP(double,f,g); /* espansione in {double t; t=(f); (f)=(g); (g)=t;} */
...
Inoltre se l'espansione della macro e' un'espressione, risulta conveniente
definire la macro fra parentesi.
Esempio:
#define CERCHIO1(r) r*r*3.14 /* area del cerchio dato il raggio */
#define CERCHIO2(r) (r)*(r)*3.14 /* area del cerchio dato il raggio */
#define CERCHIO3(r) ((r)*(r)*3.14) /* area del cerchio dato il raggio */
...
double raggio;
double ris;
...
ris = CERCHIO1(raggio+2); /* ris = raggio+2*raggio+2*3.14 ERROR! */
ris = CERCHIO2(raggio+2); /* ris = (raggio+2)*(raggio+2)*3.14 OK! */
ris = CERCHIO3(raggio+2); /* ris = ((raggio+2)*(raggio+2)*3.14) OK! */
...
ris = 100/CERCHIO2(raggio+2); /* ris = 100/(raggio+2)*(raggio+2)*3.14 ERROR! */
ris = 100/CERCHIO3(raggio+2); /* ris = 100/((raggio+2)*(raggio+2)*3.14) OK! */
L'errore non e' riscontrabile in fase di pre-compilazione o di compilazione,
ma l'errore e' dovuto al risultato fornito.
Per convincersi di cio' e' sufficiente dare una sbirciata alle
precedenze fra operatori.
Tuttavia facendo ricorso al carattere di escape \ si possono scrivere
macro su piu' linee.
Esempio:
#define SWAP(tipo,x,y) \
{ \
tipo t; \
t=(x); \
(x)=(y); \
(y)=t; \
}
Esempio:
#define DEBUG_OUT(expr) fprintf(stderr, #expr " = %g\n", (float)(expr))
...
DEBUG_OUT(x*y+z); /* espansione:
* fprintf(stderr, "x*y+z" " = %g\n", (float)(x*y+z))
*/
Tramite l'impiego di ## e' possibile concatenare gli argomenti reali
durante l'espansione di una macro.
Esempio:
#define MACRO(x,n) x = x##n
...
int val;
int val1, val2;
...
MACRO(val,1); /* espansione: val = val1 */
Esempio:
#undef DEBUG
__LINE__ | Valore decimale del numero della linea corrente del sorgente. | ||
__FILE__ | Stringa del nome del file in corso di compilazione. | ||
__DATE__ | Stringa della data di compilazione (formato Mmm dd yyyy). | ||
__TIME__ | Stringa dell'ora di compilazione (formato hh:mm:ss). | ||
__STDC__ | Contiene il valore 1 se il compilatore e' conforme allo standard ANSI. |
Di solito il compilatore accetta nella linea di comando delle opzioni per
definire e cancellare delle macro analogamente a quanto viene eseguito
con #define e #undef.
Tipicamente tali opzioni sono -Dmacro o -Dmacro=def per definire
una macro e -Umacro per eliminare la definizione.
Le opzioni -D e -U vengono eseguite prima di cominciare
l'attivita' di preprocessing sul sorgente.
L'aspetto piu' consistente della differenza fra macro e funzioni e' legato
al fatto che,
mentre per le chiamate a funzioni gli argomenti vengono valutati assieme
ai loro effetti collaterali una e una sola volta prima che il
controllo venga passato alla funzione chiamata, nel caso della macro si
ha una sostituzione testuale in linea con la conseguente valutazione
degli argomenti dipendente direttamente dalla implementazione della macro.
Un esempio chiarira' le idee:
Nel caso della macro max(v1++,v2++) risulta subito evidente che v1 e v2 risultano entrambi presenti 2 volte ciascuno nella forma incrementale postfissa. Ne conseguono 2 anomalie:
Analogamente la macro1(v1++, v2++, v3++) viene espansa come gia'
esemplificato.
La variabile v1 viene sempre incrementata 1 volta. In seguito, in funzione
del valore di v1, viene incrementata la variabile v2 oppure v3.
Durante la sostituzione di una macro, essa viene espansa in linea prima
della compilazione, permettendo cosi' una maggior velocita' di esecuzione
rispetto alla versione con funzioni.
Durante la chiamata di una funzione si ha un cambio di contesto, come per esempio una diversa visibilita' di variabili e funzioni; questo non puo' accadere con le macro, visto che rimane in esecuzione sempre la stessa funzione.
Inclusione di file (#include)
Le definizioni ricorrenti delle macro, le dichiarazioni
dei prototype di funzione e delle
variabili esterne, di solito vengono
scritte, una volta per tutte, in files tradizionalmente chiamati
header ed aventi normalmente estensione .h.
Il preprocessore C, tramite la direttiva #include, puo' ricercare il file indicato in alcune directory standard o definite al momento della compilazione ed espanderlo testualmente in sostituzione della direttiva.
Considerato che nel C ogni funzione, variabile, macro deve essere definita o dichiarata prima del suo utilizzo, risulta evidente che ha senso includere gli header file all'inizio, cioe' nella testata (e da qui deriva il nome di header), del file sorgente.
La direttiva #include puo' essere impiegata in due forme:
#include <nomefile> #include "nomefile"Nel 1° caso il nomefile viene ricercato in un insieme di directory standard definite dall'implementazione ed in altre che sono specificate al momento della compilazione.
N.B. - Nel caso che un header venga modificato, e' necessario ricompilare tutti i sorgenti che lo includono.
In particolare #if testa
l'espressione_costante_intera e se risulta diversa
da zero, la condizione e' considerata verificata positivamente.
L'espressione_costante_intera non puo' comprendere
costanti di tipo enumerativo, operatori di cast e sizeof.
Con la linea
E' possibile modificare il valore delle macro predefinite __LINE__ e
__FILE__ tramite una delle due forme della direttiva
#line:
La direttiva
Compilazione condizionale
Il preprocessore C al verificarsi di alcune condizioni puo' includere o
escludere parti del codice sorgente alla compilazione.
Le direttive che indicano al preprocessore la compilazione condizionata
sono riportate di seguito:
#if espressione_costante_intera
dove #if, #ifdef,
#ifndef testano la condizione. Se risulta
verificata, viene incluso per la compilazione il codice dalla riga successiva
alla direttiva, fino ad incontrare una delle direttive
#else, #elif o
#end.
#ifdef identificatore
#ifndef identificatore
#else
#elif espressione_costante_intera
#endif
#ifdef considera superata la condizione se e'
definito identificatore.
#ifdef identificatore
equivale a
#if defined (identificatore)
o #if defined identificatore
#ifndef considera superata la condizione se non e'
definito identificatore.
#ifndef identificatore
equivale a
#if !defined (identificatore)
o #if !defined identificatore
La parte di codice successiva a #else viene
passata al compilatore nel caso cha la #if,
#ifdef, #ifndef non
sia stata soddisfatta.
La direttiva #elif equivale ad
#else #if tranne il fatto di non aumentare di un
livello di annidamento l'intera #if.
La direttiva #endif chiude la
#if, #ifdef,
#ifndef del corrispondente livello di annidamento.
Esempio:
#ifdef DEBUG
fprintf(stderr, "Linea di Debug %d\n", (int)__LINE__);
#endif
Altre direttive del preprocessor
La linea di preprocessor
#
non crea nessun effetto.
#error
messaggio di errore
il preprocessore termina ed arresta la compilazione con l'errore indicato
nella direttiva.
#line linea
"nomefile"
#line linea
#pragma
sequenza di token
e' fortemente dipendente dalla implementazione del compilatore, in quanto
la sequenza di token comunica con il compilatore stesso per fargli
eseguire particolari operazioni.
Indice linguaggio C
Umberto Zappi Home Page