Definire Macro in Linguaggio C

Nella lezione precedente abbiamo studiato il ruolo del preprocessore nella stesura di un programma in linguaggio C. Abbiamo visto che il preprocessore è un programma che viene eseguito prima della compilazione e che si occupa di sostituire le direttive di precompilazione con il loro contenuto.

Finora, nel corso di questa guida sul linguaggio C, abbiamo adoperato la direttiva di precompilazione #define per creare semplici macro che ci permettessero di definire delle costanti. Queste macro prendono anche il nome di macro semplici in quanto non accettano parametri.

In questa lezione andremo a studiare nel dettaglio la sintassi e il funzionamento delle macro semplici utilizzando la direttiva di precompilazione #define.

Successivamente, nelle prossime lezioni, andremo anche a studiare le macro parametriche.

Macro Semplici

Una macro prende il nome dalla contrazione di macro-blocco o macro-istruzione. In altre parole si tratta di un insieme di istruzioni, funzioni o espressioni che vengono raggruppate ed etichettate con un nome simbolico. Questo nome simbolico viene poi utilizzato nei punti in cui si vuole utilizzare il blocco stesso.

Concettualmente, la macro può assomigliare ad una funzione. Del resto lo scopo di una macro è quello di riutilizzare in maniera più semplice blocchi di codice ripetuti. Tuttavia il meccanismo delle macro è molto diverso da quello delle funzioni. Infatti, mentre le funzioni vengono eseguite durante l'esecuzione del programma, le macro vengono sostituite dal preprocessore prima della compilazione.

Nello specifico, il meccanismo delle macro prende il nome di sostituzione testuale. In gergo tecnico si dice che la macro viene espansa. Vediamolo in dettaglio.

La sintassi per definire una macro semplice è la seguente:

#define nome_macro testo_macro

Dove:

  • nome_macro è il nome simbolico della macro;
  • testo_macro è il testo che verrà sostituito al posto del nome.

La cosa importante da notare è che abbiamo usato il termine testo da sostituire. Infatti il testo o corpo della macro può essere una sequenza di caratteri qualunque. Questo significa che la macro può essere utilizzata per sostituire qualsiasi cosa, non solo istruzioni o espressioni.

In questo, una macro è diversa da una funzione: usare una macro significa sostituire il nome simbolico della stessa con il suo corpo. Proprio per questo le macro hanno delle potenzialità enormi e sono molto adoperate nel linguaggio C.

Finora abbiamo usato le macro nei nostri programmi per creare, sostanzialmente, delle costanti. Nel loro manuale originale, i creatori del linguaggio C, Dennis Ritchie e Brian Kernighan, consigliano di usare le macro per definire quelle che loro chiamano Costanti Manifeste (Manifest Constants). Ossia, usare le macro per quelle costanti che non cambiano mai durante l'esecuzione del programma.

L'esempio tipico di utilizzo è quello di usarle per definire costanti numeriche come \pi o e, oppure costanti di conversione come il numero di metri in un miglio o il numero di secondi in un'ora. Ad esempio:

#define PI 3.14159265358979323846
#define E 2.71828182845904523536
#define METRI_IN_UN_MIGLIO 1609.344
#define SECONDI_IN_UN_ORA 3600

In questo modo, ogni volta che nel nostro programma avremo bisogno di usare una di queste costanti, potremo usare il nome simbolico della macro. Ad esempio:

float area_cerchio(float raggio) {
    return PI * raggio * raggio;
}

Quando il preprocessore esamina questo pezzo di codice, sostituisce il nome simbolico della macro con il suo corpo. Quindi il codice sopra, una volta espansa la macro, diventa:

float area_cerchio(float raggio) {
    return 3.14159265358979323846 * raggio * raggio;
}

Il termine espansione deriva proprio dal fatto che la sostituzione del nome di una macro con il suo corpo aumenta ed espande il codice sorgente originale.

Ma questo è solo un primo possibile utilizzo delle macro. In effetti, dato che il corpo della macro può essere testo qualsiasi, possiamo usarle per sostituzioni più complesse. Ad esempio, riprendendo la funzione di sopra, supponiamo che nel nostro programma abbiamo spesso bisogno di usare il valore dell'area di un cerchio di raggio pari a 2. Piuttosto che invocare sempre la funzione area_cerchio con parametro 2, possiamo definire una macro che sostituisca direttamente l'invocazione della funzione. Ad esempio:

#define AREA_CERCHIO_RAGGIO_2 area_cerchio(2.0)

In questo modo, nel nostro programma, possiamo usare il nome simbolico della macro AREA_CERCHIO_RAGGIO_2. Il preprocessore provvederà a sostituire il nome simbolico con il suo corpo. Ad esempio:

printf("L'area di un cerchio di raggio 2 è: %f\n", AREA_CERCHIO_RAGGIO_2);

diventa:

printf("L'area di un cerchio di raggio 2 è: %f\n", area_cerchio(2.0));

Da notare che la sostituzione avviene a livello di testo. Il preprocessore non sostituisce la macro con il risultato dell'invocazione della funzione. Quindi la funzione area_cerchio viene invocata ogni volta che usiamo il nome simbolico della macro.

Ricapitolando:

Definizione

Macro in linguaggio C

In linguaggio C una macro è un nome simbolico dato ad un blocco di testo che prende il nome di corpo della macro.

Il preprocessore del linguaggio C sostituisce, in gergo tecnico espande, il nome simbolico della macro con il suo corpo ogniqualvolta ne incontra una occorrenza nel codice sorgente.

La sintassi per definire una macro semplice, ossia una macro senza parametri, è la seguente:

#define nome_macro testo_macro

Osservazioni sulle macro

A questo punto è necessario fare due osservazioni.

La prima è che una macro non termina con il punto e virgola ;. Infatti, come abbiamo detto, la macro non è un'istruzione. È un blocco di testo che verrà sostituito ad un nome simbolico. Su questo punto dobbiamo prestare attenzione perché potremmo ottenere risultati inaspettati. Torniamo all'esempio di prima:

#define AREA_CERCHIO_RAGGIO_2 area_cerchio(2.0);

Abbiamo aggiunto un punto e virgola al termine della macro. In alcuni casi questo potrebbe non essere un problema. Ad esempio, se usiamo la macro per assegnare il valore ad una variabile:

float area = AREA_CERCHIO_RAGGIO_2;

In tal caso il preprocessore modificherà il codice in questo modo:

float area = area_cerchio(2.0);;

Il punto e virgola in più non è un problema perché il compilatore lo interpreta come un'istruzione vuota. Tuttavia, se usiamo la macro in un'altra istruzione, potremmo ottenere degli errori di compilazione. Ad esempio:

printf("L'area di un cerchio di raggio 2 è: %f\n", AREA_CERCHIO_RAGGIO_2);

In questo caso il preprocessore modificherà il codice in questo modo:

/* ERRORE */
printf("L'area di un cerchio di raggio 2 è: %f\n", area_cerchio(2.0););

Purtroppo in questo caso il compilatore ci segnalerà un errore di sintassi. Infatti il punto e virgola ; è un carattere non valido in questo contesto.

Nota

La definizione di una macro non termina con il punto e virgola

La definizione di una macro non termina con il punto e virgola ;.

Aggiungere un punto e virgola al termine della definizione di una macro può causare errori di compilazione pertanto va usato con cautela.

Come fare, allora, se vogliamo scrivere una macro su più righe? La soluzione è quella di usare il carattere \ per indicare che la macro continua sulla riga successiva. Ad esempio:

#define PI_DIVISO_2 \
    3.14159265358979323846 / 2

La macro termina alla fine dell'ultima riga in cui non è presente il carattere \. Quindi, nel nostro esempio, la macro termina alla fine della seconda riga. Il carattere \ viene ignorato.

Definizione

Macro su più righe

Solitamente, in linguaggio C, una macro termina alla fine della riga in cui è definita. Tuttavia, se vogliamo definire una macro su più righe, possiamo usare il carattere \ per indicare che la macro continua sulla riga successiva.

La sintassi è la seguente:

#define nome_macro testo_macro \
    testo_macro \
    testo_macro \
    testo_macro_finale

La seconda osservazione da fare è nascosta nella prima. Abbiamo detto, infatti, che il meccanismo alla base delle macro è quello della sostituzione testuale. Pertanto il corpo della macro viene preso così com'è e sostituito al nome simbolico della macro. Questo significa che non viene eseguito alcun controllo sintattico sul corpo della macro. Questo è un punto fondamentale che può portare ad errori difficili da scovare.

Ad esempio, dal punto di vista del preprocessore, una macro del genere è del tutto legale:

#define STRANA_MACRO !*%$

E infatti, il preprocessore non segnalerà nessun errore. Inoltre, fintanto che non viene utilizzata nel nostro codice, nemmeno il compilatore segnalerà errori. Questo perché il preprocessore non ha effettuato nessuna sostituzione. Le cose cambiano non appena proviamo ad usarla. Ad esempio:

int main(void) {
    int a = STRANA_MACRO;
    return 0;
}

Il preprocessore espanderà la macro e il codice diventa:

int main(void) {
    int a = !*%$;
    return 0;
}

A questo punto il compilatore segnalerà un errore di sintassi perché !*%$ non è un'espressione valida. Il problema è che la segnalazione dell'errore viene fatta dal compilatore nei punti in cui la macro è stata usata e non dove essa è stata definita. Motivo per cui, diventa difficile risalire alla causa dell'errore ossia alla definizione stessa della macro.

In altre parole, dato che il precompilatore non effettua nessun controllo sintattico sul corpo della macro, soltanto quando le andiamo ad utilizzare nel codice potremo accorgerci di eventuali errori.

L'assenza di controlli sintattici sul corpo della macro è un aspetto che può creare perplessità. Tuttavia, come vedremo, questo è un aspetto che può essere sfruttato per creare delle macro molto potenti.

Ricapitolando:

Nota

Sostituzione testuale e controllo sintattico

Le macro vengono sostituite dal preprocessore con il loro corpo. Questo meccanismo prende il nome di sostituzione testuale.

Il preprocessore non effettua nessun controllo sintattico sul corpo della macro. Pertanto eventuali errori di sintassi vengono segnalati soltanto quando la macro viene utilizzata nel codice.

Utilizzi delle macro

Adesso che abbiamo maggiore dimestichezza con la definizione di una macro possiamo passare al fulcro della questione: quali sono gli utilizzi delle macro?

Abbiamo già visto che le macro possono essere usate per definire delle costanti, le cosiddette costanti manifeste. Ci domandiamo, quindi, qual è il vantaggio di usarle. Vediamo nel dettaglio:

  • Rendono i programmi più leggibili.

    Questo è un indubbio vantaggio. Quando, ad esempio, usiamo costanti numeriche spesso usare un nome simbolico al posto del valore rende il significato di un'istruzione più chiaro. Ad esempio, confrontiamo le due istruzioni seguenti:

    float area = 3.14159265358979323846 * raggio * raggio;
    
    float area = PI * raggio * raggio;
    

    La seconda istruzione è sicuramente più leggibile della prima ad una prima occhiata. Quindi usare una costante è sicuramente meglio rispetto ad utilizzare un numero magico che sembra essere uscito dal nulla.

  • Rendono i programmi più semplici da modificare.

    Questo vantaggio è una diretta conseguenza del punto precedente. Supponiamo di dover utilizzare una costante numerica in più punti del nostro programma. Ad esempio, supponiamo di dover usare lo stesso coefficiente numerico in più calcoli:

    float x1 = 4.523 * y1;
    /* .... */
    float x2 = 4.523 * y2;
    /* .... */
    float x3 = 4.523 * y3;
    

    Potrebbe nascere l'esigenza di dover modificare il nostro programma e sostituire la costante 4.523 con un'altra. Ad esempio:

    float x1 = 5.123 * y1;
    /* .... */
    float x2 = 5.123 * y2;
    /* .... */
    float x3 = 5.123 * y3;
    

    Il problema è che in questo caso dobbiamo modificare a mano più punti del nostro programma. Questo può essere un problema se il nostro programma è molto grande ed è un procedimento incline ad errori: basta una svista dello sviluppatore per commettere un errore di trascrizione.

    Possiamo risolvere questo problema usando una macro. Ad esempio:

    #define COEFFICIENTE 4.523
    
    float x1 = COEFFICIENTE * y1;
    /* .... */
    float x2 = COEFFICIENTE * y2;
    /* .... */
    float x3 = COEFFICIENTE * y3;
    

    In questo modo, se dobbiamo modificare il valore del coefficiente, basterà modificare il valore della macro. Quindi la modifica avverrà in un unico punto:

    #define COEFFICIENTE 5.123
    
    float x1 = COEFFICIENTE * y1;
    /* .... */
    float x2 = COEFFICIENTE * y2;
    /* .... */
    float x3 = COEFFICIENTE * y3;
    

    Inoltre, utilizzando le macro in questo modo si evitano le inconsistenze. Supponiamo che nel nostro programma, ad esempio, abbiamo bisogno di effettuare calcoli usando \pi. Ora il numero \pi è un numero trascendentale che ha un numero infinito di cifre decimali. Per i nostri calcoli, quindi, dobbiamo limitarci ad un numero finito di cifre troncando il numero \pi.

    Se non usiamo una macro, dobbiamo ripetere il numero \pi in più punti magari commettendo errori di troncamento differenti. Ad esempio:

    float area = 3.14159 * raggio * raggio;
    /* .... */
    float circonferenza = 2 * 3.141592 * raggio;
    

    Nel primo caso abbiamo usato solo 5 cifre decimali mentre nel secondo caso abbiamo usato 6 cifre decimali. La conseguenza è che i nostri calcoli possono essere inconsistenti tra di loro. Si noti che ciò non significa che i calcoli siano errati ma, semplicemente, che non abbiamo la stessa precisione numerica nei due casi.

    Pertanto, è più semplice usare una macro:

    #define PI 3.141592
    
    float area = PI * raggio * raggio;
    /* .... */
    float circonferenza = 2 * PI * raggio;
    

    In questo modo, se dobbiamo modificare il numero di cifre decimali, basterà modificare il valore della macro.

Le macro, tuttavia, possono essere usate per scopi più complessi. In questa lezione ne vedremo alcuni lasciando quelli più avanzati alle prossime lezioni.

Modifica della sintassi del linguaggio C

Uno degli utilizzi più potenti e, per chi è alle prime armi, sconvolgenti delle macro è la possibilità di giocare con la sintassi del linguaggio C. In altre parole, sfruttando il meccanismo di sostituzione testuale è possibile modificare la sintassi stessa del linguaggio C. Ovviamente vi sono dei limiti, ma si possono ottenere risultati eccezionali. Pertanto questa possibilità va usata con cautela.

Le modifiche che possiamo applicare al linguaggio possono essere talmente semplici da essere considerate estetiche oppure talmente complesse da essere considerate semantiche.

Uno dei casi più semplici è quello di sostituire elementi sintattici base del linguaggio. Ad esempio, possiamo usare una macro per sostituire il carattere { con la parola chiave BEGIN e il carattere } con la parola chiave END proprio come accade per il linguaggio Pascal:

#define BEGIN {
#define END }

In questo modo possiamo scrivere il nostro codice in questo modo:

int main(void) BEGIN
    int a = 0;
    int b = 1;
    int c = a + b;
END

Le modifiche possono essere anche più complesse. Ad esempio, abbiamo visto nella lezione sul ciclo for che è possibile creare un loop infinito in questo modo:

for (;;) {
    /* ... */
}

Possiamo usare una macro per rendere più leggibile il codice. Ad esempio:

#define LOOP for (;;)
#define BEGIN {
#define END }

In questo modo possiamo scrivere il nostro codice in questo modo:

LOOP BEGIN
/* ... */
END

E possiamo sbizzarrirci quanto vogliamo. Possiamo, ad esempio, creare una macro per uscire dal loop infinito:

#define EXIT_LOOP break

In questo modo possiamo scrivere il nostro codice in questo modo:

LOOP BEGIN
    /* ... */
    if (condizione) {
        EXIT_LOOP;
    }
END

In poche parole, è quasi come se stessimo creando un nuovo linguaggio di programmazione. Tuttavia, come abbiamo detto, questa possibilità va usata con cautela. Infatti, ciò potrebbe rendere il nostro codice illeggibile per chi non è abituato a questo tipo di sintassi. Inoltre, potremmo causare degli errori difficili da scovare.

Tipi Opachi

Un altro utilizzo, spesso molto abusato, delle macro discende direttamente dalla capacità vista sopra. Si tratta di poter rinominare un tipo di dato. Ossia di poter assegnare un nuovo nome ad un tipo di dato già esistente.

Ad esempio, abbiamo visto nelle lezioni precedenti che in linguaggio C non esiste il tipo booleano che, invece, molti altri linguaggi di programmazione mettono a disposizione. In linguaggio C, infatti, qualunque valore differente da 0 viene considerato come vero.

Tuttavia, possiamo simulare la presenza di un tipo booleano usando un alias per il tipo int. Per far questo possiamo usare una macro. Ad esempio:

#define BOOL  int
#define TRUE  1
#define FALSE 0

In questo caso abbiamo usato tre macro. La prima macro BOOL è un alias per il tipo int. Le altre due macro TRUE e FALSE sono alias per i valori 1 e 0.

A questo punto possiamo usare queste macro nel nostro codice in questo modo:

BOOL condizione = TRUE;

/* ... */

if (a < 5) {
    condizione = FALSE;
}

/* ... */

if (condizione) {
    /* ... */
}

Non è affatto raro trovare programmi in linguaggio C scritti in questo modo. Ad esempio, se analizziamo programmi scritti per il sistema operativo Windows, potremmo trovare codice scritto in questo modo:

DWORD ThreadProc(LPVOID lpParameter) {
    /* ... */
}

In questo caso DWORD è un alias per il tipo unsigned long e LPVOID è un alias per il tipo void *. Sotto Windows, infatti, è fortemente suggerito questo stile di programmazione dove vengono usati degli alias per i tipi di dato.

In generale quando si usa un alias per un tipo di dati si parla di Tipo Opaco (Opaque Type). Vengono usati spesso dai framework e dalle librerie quando si vuole nascondere la struttura interna di un tipo di dato. Questo non per motivi di segretezza ma per motivi di astrazione.

Ad esempio, in linguaggio C, il tipo FILE è un tipo opaco come vedremo nelle lezioni sull'input/output. In realtà si tratta di un alias per un puntatore. Dal punto di vista del programmatore, tuttavia, è più semplice riferirsi ad un FILE piuttosto che ad un puntatore.

Definizione

Tipo Opaco

In linguaggio C un tipo opaco è un tipo di dato la cui vera struttura è nascosta all'utente finale inteso come programmatore che lo utilizza. Spesso l'utilizzo di tipi opachi nasce da esigenze di astrazione del codice.

Per definire un tipo opaco si usa una macro che definisce un alias per un tipo di dato già esistente:

#define nome_tipo_opaco tipo_dato_esistente

Stile di scrittura delle macro

Abbiamo visto che le macro possono essere usate per scopi molto diversi. Tuttavia, come abbiamo detto, è necessario usare le macro con cautela. Infatti, se usate in modo errato, possono portare a risultati inaspettati e a errori difficili da scovare.

Per questo motivo, anche se non è necessario, molti programmatori preferiscono nominare le macro in modo da evidenziare il fatto che si tratta proprio di una macro e non del nome di una funzione o variabile. Il modo più utilizzato e diffuso è quello di assegnare alle macro nomi completamente in maiuscolo.

Nei nostri esempi abbiamo sempre usato questo stile di scrittura:

#define PI 3.14159265358979323846
#define BOOL int
#define TRUE 1
#define FALSE 0

In questo modo ogniqualvolta vediamo un nome scritto in maiuscolo sappiamo all'istante che si tratta di una macro.

In realtà il linguaggio C non impone nessuna regola sulla scrittura delle macro. Possiamo usare nomi scritti in minuscolo o in maiuscolo o anche una combinazione dei due. Restiamo dell'idea, comunque, che sia una buona norma usare questo stile.

Consiglio

Nomi delle macro

È una buona norma usare nomi scritti in maiuscolo per le macro. In questo modo è più semplice riconoscerle all'interno del codice.

#define NOME_MACRO testo_macro

In Conclusione

Dopo aver introdotto lo scopo e il funzionamento del preprocessore, in questa lezione siamo entrati nel vivo del suo utilizzo studiando le macro del linguaggio C.

Le macro sono in sostanza composte da un nome, o etichetta, ed un corpo che può essere una porzione di testo qualunque. Le macro differiscono rispetto ad una funzione in quanto il loro meccanismo di funzionamento è quello della sostituzione testuale. Ossia, il preprocessore sostituisce il nome della macro con il suo corpo. Una macro non viene invocata ma viene espansa.

Detto questo abbiamo visto in questa lezione gli utilizzi principali delle macro semplici. Una macro semplice è una macro che non accetta parametri. In questa lezione abbiamo visto che le possiamo usare per:

  • Definire costanti;
  • Raggruppare blocchi di codice ripetuti;
  • Modificare la sintassi del linguaggio C;
  • Creare alias per tipi di dato: i cosiddetti tipi opachi.

Questi impieghi già ci consentono di scrivere programmi efficienti e leggibili migliorando la nostra produttività.

Una macro, tuttavia, può anche accettare in ingresso dei parametri, proprio come una funzione. In tal caso si parla di macro parametriche; esse forniscono una potenza e una flessibilità al programmatore che non ha eguali. Studieremo le macro parametriche nella prossima lezione.