Introduzione al Preprocessore in linguaggio C

Finora, nelle lezioni precedenti, abbiamo adoperato delle direttive di precompilazione senza entrare troppo nel dettaglio del loro funzionamento. Abbiamo utilizzato le direttive #define e #include.

Queste direttive in linguaggio C vengono gestite dal Preprocessore. Il preprocessore è un programma che viene eseguito prima del compilatore vero e proprio. Il suo compito è quello di modificare il codice sorgente prima che quest'ultimo venga passato al compilatore per poter, poi, generare il codice eseguibile.

Il preprocessore è uno strumento molto versatile che rende unico e potente il linguaggio C (e così anche il linguaggio C++). Tuttavia il suo utilizzo può essere controproducente se non se ne conosce bene il funzionamento. Lo scopo di questa lezione è proprio quello di capire il ruolo del preprocessore nel processo di compilazione di un programma scritto in C.

Funzionamento del Preprocessore

Tipicamente il comportamento del preprocessore è controllato dalle Direttive di Precompilazione. Queste direttive sono delle istruzioni che vengono interpretate dal preprocessore e che modificano il codice sorgente. Il risultato di questa operazione viene passato successivamente al compilatore che lo trasforma in codice oggetto, ossia istruzioni eseguibili dal processore. Il punto chiave è proprio il fatto che il codice sorgente venga prima modificato o trasformato.

Le direttive di precompilazione iniziano sempre con il carattere # (cancelletto) e terminano quando termina la riga su cui si trovano. Nelle lezioni precedenti abbiamo già incontrato due tipi di direttive: #define e #include.

La direttiva #define permette di definire una cosiddetta macro, termine che sta per macro-istruzione. Le macro non sono l'oggetto di questa lezione tuttavia sono utili per comprendere i meccanismi alla base del preprocessore. Una macro può essere intesa come un nome simbolico che diamo ad un valore o ad un'espressione. Questo nome simbolico può essere utilizzato in tutto il codice sorgente al posto del valore o dell'espressione che rappresenta.

Una semplice macro definita con la direttiva #define è la seguente:

#define PI 3.141592

Con questa macro abbiamo definito un nome simbolico PI e gli abbiamo associato un'espressione (in particolare un valore letterale) 3.141592. Quando prendiamo un file sorgente e lo diamo in pasto al preprocessore, questo effettua due operazioni:

  1. Memorizza, dapprima, il nome della macro insieme al valore o all'espressione che rappresenta;
  2. Successivamente, ogni volta che incontra il nome della macro nel codice sorgente, lo sostituisce con il valore o l'espressione che rappresenta. In gergo tecnico si dice che il preprocessore espande la macro.

La direttiva #include, invece, permette di includere il contenuto di un file sorgente all'interno di un altro file sorgente. Questa direttiva è molto utile per organizzare il codice sorgente in più file. Finora l'abbiamo utilizzata per includere il file stdio.h che contiene le dichiarazioni delle funzioni di input/output standard.

Quando il preprocessore incontra una direttiva #include esegue le seguenti operazioni:

  1. Cerca il file specificato dalla direttiva #include all'interno delle directory di sistema;
  2. Se il file viene trovato, lo apre e ne legge il contenuto;
  3. Sostituisce la direttiva #include con il contenuto del file.

In entrambe i casi, sia che si usi #define sia che si usi #include, il file sorgente di partenza viene modificato a livello di testo. #define sostituisce un'etichetta con un'espressione, #include prende l'intero contenuto di un file e lo inserisce nel codice.

Per chiarire il ruolo che il preprocessore ha nella compilazione, osserviamo la figura seguente:

Preprocessore e Processo di Compilazione
Figura 1: Preprocessore e Processo di Compilazione

In ingresso al preprocessore viene passato il file sorgente.c che può contenere delle direttive di precompilazione. In generale un file sorgente in linguaggio C ha sempre una o più direttive al proprio interno.

Il preprocessore scorre il file sorgente e, ogni volta che incontra una direttiva di precompilazione, la interpreta e la esegue. La cosa importante da notare è che quando una direttiva viene eseguita, il preprocessore modifica il file sorgente e la elimina. Infatti, il risultato finale del preprocessore è un altro file sorgente modificato e, soprattutto, privo di direttive.

Questo file sorgente modificato viene dato in pasto direttamente al compilatore vero e proprio che lo analizza e genera in uscita il codice oggetto eseguibile.

Tutto questo processo è quasi sempre nascosto. Nella pratica quotidiana non dobbiamo invocare direttamente il preprocessore ma, semplicemente, invocare il compilatore che si occuperà di invocare il preprocessore e di passargli il file sorgente. Ad esempio, il compilatore gcc quando invocato su un file sorgente invoca direttamente il suo preprocessore che si chiama cpp (C PreProcessor).

Per chiarire meglio il tutto proviamo ad esaminare un programma di esempio. Supponiamo di voler compilare il programma seguente che prende in ingresso un angolo espresso in gradi e lo trasforma in radianti:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>

#define FATTORE_CONVERSIONE 3.141592 / 180.0

/* Programma per la conversione di un angolo in gradi in radianti */

int main(void) {
    float angolo, radianti;

    /* Lettura dell'angolo in gradi */
    printf("Inserisci un angolo in gradi: ");
    scanf("%f", &angolo);

    /* Conversione in radianti */
    radianti = angolo * FATTORE_CONVERSIONE;

    /* Stampa del risultato */
    printf("L'angolo in radianti è: %f\n", radianti);

    return 0;
}

Questo programma contiene due direttive di precompilazione:

  1. Alla riga 1 abbiamo la direttiva #include che include il file stdio.h che contiene le dichiarazioni delle funzioni di input/output standard;
  2. Alla riga 3 abbiamo la direttiva #define che definisce la macro FATTORE_CONVERSIONE. Questa macro viene utilizzata alla riga 15 per convertire l'angolo da gradi in radianti e rappresenta l'espressione 3.141592 / 180.0.

Adesso proviamo a compilare il programma invocando separatamente precompilatore e compilatore. Sotto Linux, possiamo usare il precompilatore cpp invocandolo direttamente sul file sorgente e salvando il risultato in un file temporaneo:

$ cpp sorgente.c > sorgente.i

Se analizziamo il contenuto del file sorgente.i vediamo che il preprocessore ha sostituito la direttiva #include con il contenuto del file stdio.h e ha espanso la macro FATTORE_CONVERSIONE con l'espressione 3.141592 / 180.0:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
---- CONTENUTO DI stdio.h OMESSO ----




int main(void) {
    float angolo, radianti;


    printf("Inserisci un angolo in gradi: ");
    scanf("%f", &angolo);


    radianti = angolo * 3.141592 / 180.0;


    printf("L'angolo in radianti è: %f\n", radianti);

    return 0;
}

Nel risultato abbiamo omesso il contenuto di stdio.h per brevità.

La cosa da notare è che il preprocessore ha eliminato le direttive di precompilazione e le ha sostituite con spazi vuoti. Nel file risultante manca la direttiva #include e la direttiva #define. Tuttavia alla riga 14 al posto della macro FATTORE_CONVERSIONE abbiamo l'espressione 3.141592 / 180.0.

Inoltre, il preprocessore ha anche effettuato un'altra operazione nascosta: ha eliminato tutti i commenti dal codice sorgente e li ha sostituiti con spazi vuoti.

Ricapitolando:

Definizione

Preprocessore

Il Preprocessore è un programma o componente che prende in ingresso un file sorgente in linguaggio C e lo modifica prima di passarlo al compilatore. Le modifiche che il preprocessore applica sono controllate dalle direttive di precompilazione.

Quando il preprocessore incontra una direttiva di precompilazione, la interpreta e la esegue. In particolare, quando una direttiva viene eseguita, il preprocessore modifica il file sorgente ed elimina la direttiva stessa sostituendola con uno spazio vuoto.

Inoltre, il preprocessore elimina dal codice sorgente tutti i commenti presenti.

Direttive di Precompilazione

Le direttive di precompilazione sono raggruppabili in tre categorie:

  • Definizione di Macro: le direttive #define e #undef permettono di definire e rimuovere macro;
  • Inclusione di File: la direttiva #include permette di includere il contenuto di un file sorgente all'interno di un altro file sorgente;
  • Compilazione Condizionale: le direttive #if, #ifdef, #ifndef, #elif, #else e #endif permettono di compilare solo alcune parti del codice sorgente in base a condizioni.

Oltre a queste, esistono altre direttive più specializzate e usate meno: #error, #line e #pragma.

Regole Comuni

Sebbene utilizzate per scopi differenti, le direttive di precompilazione devono seguire alcune regole comuni. In particolare:

  • Devono iniziare con il carattere # (cancelletto);

    Ogni direttiva comincia sempre con il carattere # ma non è necessario che la direttiva si trovi all'inizio della riga. Il carattere # deve essere il primo carattere della riga ma può essere preceduto da spazi o tabulazioni, quindi una direttiva può essere indentata.

    Ad esempio:

    #define PI 3.141592
        #define E 2.718281
    

    Questo codice è perfettamente valido. La direttiva #define alla riga 2 è indentata di 4 spazi.

  • Finiscono con il termine della riga corrente a meno che non venga usato il carattere \ (backslash);

    Ogni direttiva termina con il termine della riga su cui si trova. Questo significa che una direttiva non può essere divisa su più righe. Se una direttiva è troppo lunga per stare su una riga, allora deve essere spezzata su più righe utilizzando il carattere \ (backslash) alla fine di ogni riga tranne l'ultima.

    Ad esempio supponiamo di voler definire una macro per il valore di \frac{\pi}{2}. Potremmo scrivere la macro su di una riga soltanto in questo modo:

    #define PI_2 3.141592 / 2.0
    

    Se volessimo spezzare la macro e dividerla su due righe dobbiamo usare in maniera esplicita il carattere \ (backslash) alla fine della prima riga:

    #define PI_2 3.141592 \
                 / 2.0
    

    Se avessimo omesso il carattere \ (backslash) alla fine della prima riga, il compilatore avrebbe generato un errore di compilazione. Infatti, il preprocessore gli avrebbe dato in pasto il codice seguente:

                 / 2.0
    

    In questo codice la prima riga è vuota perché la direttiva è stata rimossa, mentre la seconda riga è rimasta e rappresenta un'espressione incompleta.

  • Possono apparire in qualsiasi punto del programma;

    Le direttive di precompilazione possono apparire in qualsiasi punto del programma. Non è necessario che siano all'inizio del file sorgente. Tuttavia, è buona prassi di programmazione raggruppare le direttive di precompilazione all'inizio del file sorgente.

  • I commenti possono apparire sulla stessa riga della direttiva;

    Le direttive di precompilazione possono essere seguite da commenti. I commenti possono apparire sulla stessa riga della direttiva o su righe successive.

    Ad esempio:

    #define PI_2 3.141592 / 2.0 /* Pi greco diviso 2 */
    

    Questo codice è perfettamente valido.

  • I "pezzi" di una direttiva possono essere separati da un numero arbitrario di spazi e tabulazioni;

    Una direttiva, come ad esempio una #define può essere scritta in questo modo:

    #     define     PI   3.141592
    

    Questo codice è perfettamente valido. I token della direttiva #define sono separati da un numero arbitrario di spazi e tabulazioni.

In Sintesi

Questa lezione rappresenta il punto di partenza nella comprensione del funzionamento del preprocessore. In questa lezione abbiamo visto che:

  • Il preprocessore è un programma che prende in ingresso un file sorgente e lo modifica prima di passarlo al compilatore vero e proprio;
  • Il preprocessore è controllato dalle direttive di precompilazione;
  • Le direttive di precompilazione sono istruzioni che iniziano con il carattere # (cancelletto) e terminano con il termine della riga corrente;
  • Una volta eseguite le direttive, esse vengono eliminate dal file sorgente.

Questi quattro punti sono fondamentali per comprendere al meglio come usare il preprocessore. Molti degli errori più comuni nascono dal fatto che viene dimenticato uno dei punti di sopra.

A questo punto possiamo entrare nel vivo e iniziare a studiare le singole direttive di precompilazione. Nella prossima lezione analizzeremo la direttiva #define che permette di definire delle macro.