Proprietà Generali delle Macro in Linguaggio C

Finora abbiamo studiato le macro semplici, le macro parametriche e gli operatori per le macro in linguaggio C.

Adesso che sappiamo come definire e utilizzare le macro, vediamo alcune proprietà generali che valgono per tutte le macro in linguaggio C.

In questa lezione vedremo come le macro possono invocare altre macro, le eccezioni all'espansione di macro, il comportamento delle macro rispetto allo scope e la direttiva #undef.

Proprietà delle Macro

Abbiamo studiato, nelle lezioni precedenti, come definire macro semplici e macro parametriche in linguaggio C. Ne abbiamo visto la sintassi e l'utilizzo. Adesso ci concentriamo su alcune proprietà che valgono in generale per entrambe i casi.

Invocazioni innestate di macro

Una delle caratteristiche più potenti delle macro in linguaggio C è la possibilità di invocare altre macro all'interno del corpo di una macro. Questo permette di creare macro complesse e di comporre funzionalità diverse in un'unica macro.

Prendiamo un esempio. Definiamo, con una macro semplice, la costante \pi:

#define PI 3.14159265359

Possiamo riutilizzare la macro PI all'interno di altre macro. Possiamo, ad esempio, creare una macro parametrica per il calcolo dell'area di un cerchio ed un'altra per calcolarne il periometro:

#define AREA_CERCHIO(r) (PI * r * r)
#define PERIMETRO_CERCHIO(r) (2 * PI * r)

In entrambe le macro parametriche abbiamo invocato, nel corpo stesso, la macro PI.

Per comprendere come lavora il preprocessore in questi casi, supponiamo di dargli in pasto uno stralcio di codice del genere:

double raggio = 2.0;

double area = AREA_CERCHIO(raggio);
double perimetro = PERIMETRO_CERCHIO(raggio);

Quando il preprocessore analizza questo codice, per prima cosa sostituisce, al posto delle macro, il corpo delle macro stesse. In questo caso, il preprocessore sostituirà AREA_CERCHIO(raggio) con PI * raggio * raggio e PERIMETRO_CERCHIO(raggio) con 2 * PI * raggio:

double raggio = 2.0;

double area = PI * raggio * raggio;
double perimetro = 2 * PI * raggio;

A questo punto, il preprocessore analizza una seconda volta il codice e sostituisce, al posto della macro PI, il valore numerico 3.14159265359:

double raggio = 2.0;

double area = 3.14159265359 * raggio * raggio;
double perimetro = 2 * 3.14159265359 * raggio;

Questo procedimento viene ripetuto fintanto che il preprocessore non incontra più alcuna macro da sostituire.

Ricapitolando:

Definizione

Invocazioni innestate di macro

In linguaggio C, il corpo di una macro può contenere invocazioni ad altre macro.

Esiste, tuttavia, una piccola eccezione a questa regola. Ossia, una macro non può invocare se stessa all'interno del proprio corpo.

Chiariamo con un esempio. Supponiamo di voler creare una macro per il calcolo del fattoriale di un numero intero. Proviamo ad implementarla, anche se non è ammesso, in maniera ricorsiva. Definiamo la macro FACT:

/* CODICE ERRATO */
#define FACT(n) ((n) == 0 ? 1 : (n) * FACT(n - 1))

A differenza di una funzione, una macro non viene invocata ma espansa, il suo corpo, cioè, viene sostituito al posto della macro stessa. Provando a compilare il codice che segue:

int fattoriale_di_3 = FACT(3);

quello che accade è che la sostituzione procederebbe all'infinito. Infatti, il preprocessore sostituirebbe FACT(3) con:

int fattoriale_di_3 = ((3) == 0 ? 1 : (3) * FACT(3 - 1));

Poi, successivamente, sostituirebbe FACT(3 - 1) con:

int fattoriale_di_3 = ((3) == 0 ? 1 : (3) * ((3 - 1) == 0 ? 1 : (3 - 1) * FACT(3 - 1 - 1)));

Di nuovo, sostituirebbe FACT(3 - 1 - 1) con:

int fattoriale_di_3 = ((3) == 0 ? 1 : (3) * ((3 - 1) == 0 ? 1 : (3 - 1) * (((3 - 1 - 1) == 0 ? 1 : (3 - 1 - 1) * FACT(3 - 1 - 1 - 1))));

E così via, all'infinito. Questo comportamento è un errore e il preprocessore, per evitare di infilarsi in un loop infinito, non permette a una macro di invocare se stessa.

In particolare, secondo lo standard del C, se il preprocessore incontra nel corpo di una macro il nome della macro stessa, non la sostituirà mai e la lascia intatta. Per cui, il codice di sopra semplicemente diventa:

int fattoriale_di_3 = ((3) == 0 ? 1 : (3) * FACT(3 - 1));

L'invocazione FACT(3 - 1) non verrà sostituita e, ovviamente, il programma non verrà compilato in quanto non esiste una funzione che si chiama FACT.

Ricapitolando:

Nota

Una Macro non può invocare se stessa

Il linguaggio C non consente la definizione di macro ricorsive, ossia macro che invocano se stesse.

Nel caso in cui il preprocessore incontra una macro che invoca se stessa nel proprio corpo, l'invocazione interna non verrà sostituita e la macro verrà lasciata intatta.

Eccezioni all'espansione di macro

Abbiamo visto che il preprocessore sostituisce le macro con il loro corpo, attraverso il processo chiamato espansione.

Esistono delle eccezioni a tale meccanismo. Per chiarire meglio, consideriamo l'esempio che segue:

#define DIMENSIONE 1024

int DIMENSIONE_BUFFER;

printf("Inserire il valore di DIMENSIONE_BUFFER\n");
scanf("%d", &DIMENSIONE_BUFFER);

if (DIMENSIONE_BUFFER > DIMENSIONE) {
    printf("ERRORE: Superata DIMENSIONE massima");
}

In questo esempio abbiamo definito una macro semplice, DIMENSIONE, che contiene la dimensione massima che possiamo assegnare ad un buffer.

Quando il preprocessore esamina il codice di sopra, accade che:

  • quando incontra l'identificatore DIMENSIONE_BUFFER non sostituirà DIMENSIONE con 1024, in quanto DIMENSIONE fa parte dell'identificatore DIMENSIONE_BUFFER stesso;

    In altre parole, il preprocessore non sostituisce il nome di una macro se essa fa parte di un identificatore. Quindi la riga:

    int DIMENSIONE_BUFFER;
    

    rimarrà invariata.

  • quando incontra l'identificatore DIMENSIONE all'interno della condizione if, invece, sostituirà DIMENSIONE con 1024:

    if (DIMENSIONE_BUFFER > 1024) {
        printf("ERRORE: Superata DIMENSIONE massima");
    }
    
  • Inoltre, quando incontra il token DIMENSIONE all'interno della stringa "ERRORE: Superata DIMENSIONE massima", non sostituirà DIMENSIONE con 1024, in quanto DIMENSIONE fa parte di una stringa letterale.

    In altre parole, il preprocessore non sostituisce il nome di una macro se essa fa parte di una stringa letterale.

Ricapitolando:

Definizione

Eccezioni all'Espansione di Macro

Il preprocessore non sostituisce il nome di una macro se essa:

  • fa parte del nome di un identificatore;
  • è contenuto in una stringa letterale.

Dobbiamo, però, fare una piccola osservazione. Abbiamo detto che il nome di una macro non viene sostituita se esso fa parte di un identificatore. Tuttavia, se il nome di una macro è uguale al nome intero di un identificatore, e quindi non ne è una parte, allora la sostituzione avviene normalmente.

Ad esempio:

#define NOME prova

int NOME = 5;
int MIO_NOME = 6;

In questo caso, il preprocessore sostituirà NOME con prova nella prima riga, ma non nella seconda, in quanto il nome della macro NOME è parte ma non è il tutto dell'identificatore MIO_NOME. Per cui il codice di sopra diventa:

int prova = 5;
int MIO_NOME = 6;

Macro e Scope

La definizione di una macro non segue le normali regole di visibilità che valgono, invece, per le variabili.

Prima di tutto, la definizione di una macro è sempre globale. Vediamo un esempio:

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

double area_cerchio(double raggio) {
    #define PI 3.14159265359
    return PI * raggio * raggio;
}

int main() {
    double raggio;
    printf("Inserisci il raggio del cerchio: \n");
    scanf("%lf", &raggio);
    double area = area_cerchio(raggio);
    printf("L'area del cerchio si calcola come: %lf * %lf * %lf\n",
            PI, raggio, raggio);
    printf("L'area del cerchio vale = %lf\n", area);
    return 0;
}

In questo esempio abbiamo creato una funzione area_cerchio che calcola, come dice il nome, l'area di un cerchio. All'interno della funzione abbiamo definito la macro PI che rappresenta il valore di \pi.

A differenza di una variabile locale, la macro PI è globale e visibile in tutto il file sorgente. Quindi, possiamo utilizzarla anche all'interno della funzione main. Infatti, nelle righe 13 e 14, abbiamo utilizzato la macro PI per stampare il valore di \pi e l'area del cerchio.

Inoltre, una macro è visibile solo a partire dal punto in cui è definita fino alla fine del file sorgente. Se proviamo a modificare il codice sorgente di sopra in questo modo:

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

void stampa_area_cerchio(double raggio, double area) {
    printf("L'area del cerchio si calcola come: %lf * %lf * %lf\n",
            PI, raggio, raggio);
    printf("L'area del cerchio vale = %lf\n", area);
}

double area_cerchio(double raggio) {
    #define PI 3.14159265359
    return PI * raggio * raggio;
}

int main() {
    double raggio;
    printf("Inserisci il raggio del cerchio: \n");
    scanf("%lf", &raggio);
    double area = area_cerchio(raggio);
    stampa_area_cerchio(raggio, area);
    return 0;
}

Otteniamo un errore di compilazione. Infatti, abbiamo usato la macro PI all'interno della funzione stampa_area_cerchio, ma la macro PI è definita dopo la funzione stessa. Quindi, il preprocessore non è in grado di sostituire PI con il valore numerico di \pi e il programma non verrà compilato.

Provando a compilare con gcc otteniamo un errore del genere:

test_macro.c: In function ‘stampa_area_cerchio’:
test_macro.c:5:13: error: ‘PI’ undeclared (first use in this function)
    5 |             PI, raggio, raggio);
      |             ^~

Infine, la definizione di una macro ha effetto soltanto nel file in cui è definita. Questo comportamento lo studieremo nel dettaglio nel momento in cui andremo a parlare di programmi composti da più file sorgenti. Per il momento, ricordiamo che una macro è globale e visibile in tutto il file sorgente in cui è definita.

Ricapitolando:

Definizione

Macro e Scope

La definizione di una macro ha effetto, ossia è visibile e utilizzabile, secondo le seguenti regole:

  1. Una macro è sempre globale per il file sorgente in cui è definita;

    Macro definite all'interno di blocchi di codice o funzioni sono globalmente visibili all'interno del file stesso;

  2. La visibilità di una macro è valida dal punto in cui è definita fino alla fine del file sorgente;

    Una macro non può essere utilizzata in un punto precedente alla sua definizione.

Direttiva #undef

Normalmente, il preprocessore non consente di ridefinire una macro con lo stesso nome.

Ad esempio, il codice che segue non è valido:

#define PI 3.14159265359

#define AREA(r) PI * r * r

printf("Area di un cerchio di raggio 2: %lf\n", AREA(2));

/* ERRORE: Ridefinizione della macro AREA */
#define AREA(r) r * r

printf("Area di un quadrato di lato 2: %lf\n", AREA(2));

Nell'esempio di sopra, abbiamo definito, dapprima, la macro AREA per calcolare l'area di un cerchio. Successivamente, abbiamo tentato di ridefinire la macro AREA per calcolare l'area di un quadrato.

Questo comportamento non è permesso e il preprocessore restituirà un errore di compilazione.

Si può ridefinire una macro soltanto se la nuova definizione è identica alla precedente a meno di spazi. Ad esempio, il codice di sotto è corretto:

#define PI 3.14159265359

#define AREA(r) PI * r * r

printf("Area di un cerchio di raggio 2: %lf\n", AREA(2));

/* OK: Ridefinizione della macro AREA */
#define AREA(r) PI*r*r

Ovviamente, in tal caso, non possiamo utilizzare AREA per calcolare l'area di un quadrato.

Esiste, tuttavia, una direttiva del preprocessore, che ancora non abbiamo studiato, che permette di disattivare la definizione di una macro. Questa direttiva è #undef.

Utilizzando la direttiva #undef, possiamo disattivare la definizione di una macro e, successivamente, ridefinirla con un nuovo corpo. Tornando all'esempio di prima, possiamo scrivere il codice in questo modo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#define PI 3.14159265359

#define AREA(r) PI * r * r

printf("Area di un cerchio di raggio 2: %lf\n", AREA(2));

/* Disattiviamo la macro AREA */
#undef AREA

/* Ora possiamo ridefinire la macro AREA */
#define AREA(r) r * r

printf("Area di un quadrato di lato 2: %lf\n", AREA(2));

In questo modo, disattiviamo la macro AREA con la direttiva #undef e possiamo ridefinirla con un nuovo corpo.

Ricapitolando:

Definizione

Direttiva #undef

La direttiva #undef permette di disattivare la definizione di una macro in linguaggio C.

La sintassi è:

#undef NOME_MACRO

Una macro, quindi, sarà valida e visibile dal punto in cui è stata definita fino alla direttiva #undef oppure fino alla fine del file sorgente.

In Sintesi

In questa lezione abbiamo visto alcune proprietà generali delle macro in linguaggio C:

  • Abbiamo visto che una macro può invocare altre macro all'interno del proprio corpo, ma non può invocare se stessa;
  • Abbiamo studiato alcune eccezioni all'espansione di macro, ossia casi in cui il preprocessore non sostituisce il nome di una macro; in particolare, se il nome di una macro fa parte di un identificatore o di una stringa letterale;
  • Abbiamo analizzato il comportamento delle macro rispetto allo scope e abbiamo visto che una macro è globale e visibile in tutto il file sorgente in cui è definita;
  • Infine, abbiamo studiato la direttiva #undef che permette di disattivare la definizione di una macro.

Nella prossima lezione, analizzeremo alcuni dettagli sulla sintassi del linguaggio e C e le macro che nelle precedenti lezioni abbiamo solo accennato.