Linguaggio C - Il preprocessore C

Il preprocessor e' un programma che viene attivato dal compilatore nella fase precedente alla compilazione, detta di precompilazione.

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'

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"
    

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.
Esempio:
  #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:
o __LINE__   Valore decimale del numero della linea corrente del sorgente.
o __FILE__   Stringa del nome del file in corso di compilazione.
o __DATE__   Stringa della data di compilazione (formato Mmm dd yyyy).
o __TIME__   Stringa dell'ora di compilazione (formato hh:mm:ss).
o __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.

Macro vs. funzioni

Bisogna porre molta attenzione qualora si abbiano macro con argomenti, in quanto la loro assomiglianza con le funzioni potrebbe portare a delle differenze notevoli di funzionamento e difficili da individuare.

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:

#define max(a,b) (a) > (b) ? (a) : (b) #define macro1(a,b,c) (a) ? (b) : (c) ... valmax = max(v1++, v2++); /* espansa in * valmax = (v1++) > (v2++) ? (v1++) : (v2++); */ val = macro1(v1++, v2++, v3++); /* espansa in * val = (v1++) ? (v2++) : (v3++) */

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:

  1. La variabile che ha valore maggiore tra v1 e v2, oppure v2 in caso di uguaglianza, viene valutata due volte con possibilita' di un doppio incremento. L'altra variabile viene comunque incrementata una volta.
  2. L'esecuzione dell'espressione espansa puo' dipendere dall'implementazione del compilatore.
Se max fosse stata una funzione, non si segnalerebbero 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.
Nel caso della versione Unix del compilatore, le directory standard di ricerca degli header potrebbero essere /usr/include, /usr/local/include, ...
Nel 2° caso il nomefile viene ricercato nella directory corrente e poi, se non e' stato trovato, la ricerca continua nelle directory standard e in quelle specificate al momento della compilazione come nel 1° caso.

N.B. - Nel caso che un header venga modificato, e' necessario ricompilare tutti i sorgenti che lo includono.

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
#ifdef identificatore
#ifndef identificatore
#else
#elif espressione_costante_intera
#endif
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.

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
    

Altre direttive del preprocessor

La linea di preprocessor
#
non crea nessun effetto.

Con la linea

#error messaggio di errore
il 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 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-C Indice linguaggio C
At Home Umberto Zappi Home Page