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'
#define MAX 100 #define STRING_ERR "Rilevato errore !\n"
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.
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;} */ ...
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.
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! */
Nei commenti dell'esempio sopra riportato sono state espanse le macro come
verrebbero compilate, con a fianco il risultato atteso.
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.
Una macro deve essere definita su una unica riga del file sorgente.
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; \ }
Se un parametro formale e' preceduto dal carattere pound #, il suo
valore attuale e' espanso testualmente come stringa.
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.
#define MACRO(x,n) x = x##n ... int val; int val1, val2; ... MACRO(val,1); /* espansione: val = val1 */
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.
Esempio:
#undef DEBUG
Tipicamente sono predefinite 5 macro:
![]() |
__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.
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.
#if espressione_costante_interadove #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
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.
#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
#non crea nessun effetto.
Con la linea
#error messaggio di erroreil preprocessore termina ed arresta la compilazione con l'errore indicato nella direttiva.
E' possibile modificare il valore delle macro predefinite __LINE__ e __FILE__ tramite una delle due forme della direttiva #line:
#line linea "nomefile"
#line linea
La direttiva
#pragma sequenza di tokene' fortemente dipendente dalla implementazione del compilatore, in quanto la sequenza di token comunica con il compilatore stesso per fargli eseguire particolari operazioni.