Scrittura di File Header in linguaggio C

Vediamo come realizzare un file header in linguaggio C studiando separatamente tre casi:

  1. Condivisione di definizioni di macro e tipi:
  2. Condivisione di prototipi di funzioni;
  3. Condivisione di variabili globali.

Condividere Definizioni di Macro e Definizioni di Tipi

La maggior parte dei programmi complessi scritti in C contengono definizioni di macro e tipi che devono essere condivise tra più file sorgenti, se non addirittura tra tutti i file.

Tali definizioni possono essere inserite in un file header.

Prendiamo un esempio. Abbiamo visto che, fino allo standard C99, in linguaggio C non esiste un tipo booleano. Pertanto, in molti programmi C venivano definite delle macro per rappresentare i valori vero e falso più il tipo BOOL che, sostanzialmente, era un alias per int.

Non era raro trovare definizioni di questo tipo:

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

Queste definizioni possono essere inserite in un file header, ad esempio boolean.h, e incluso in tutti i file sorgenti che necessitano di tali definizioni. In questo modo si evita di dover ripetere la definizione di TRUE, FALSE e BOOL in ogni file sorgente.

Possiamo scrivere il file boolean.h in questo modo:

// boolean.h
#define TRUE 1
#define FALSE 0
typedef int BOOL;

E includerlo nei file sorgenti in questo modo:

#include "boolean.h"

Se, ad esempio, due file sorgenti main.c e funzioni.c necessitano di queste definizioni, possiamo includere il file boolean.h in entrambi i file sorgenti, come mostra la figura:

Condivisione di Macro e Tipi di Dato attraverso File Header
Figura 1: Condivisione di Macro e Tipi di Dato attraverso File Header
Definizione

Definizioni di Macro e Tipi nei File Header

Per condividere la definizione di macro e tipi tra più file sorgenti, è sufficiente inserire tali definizioni in un file header e includerlo nei file sorgenti che necessitano di tali definizioni.

Non è necessaria una sintassi particolare.

Inserire definizioni di macro e tipi nei file header ha una serie di vantaggi:

  • Le definizioni non devono essere ripetute in ogni file sorgente;
  • Il programma diventa più semplice da modificare, in quanto le definizioni sono concentrate in un unico file;

    Se, per esempio, volessimo cambiare il tipo BOOL e renderlo un alias di short anziché int, ci basterebbe cambiare il contenuto del file header boolean.h e tutti i file sorgenti che includono boolean.h verrebbero aggiornati automaticamente.

  • Si evitano inconsistenze tra le definizioni nei vari file sorgente.

Condivisione di Prototipi di Funzioni

Abbiamo già analizzato nella lezione introduttiva sul processo di compilazione che per condividere una funzione tra più file sorgenti è necessario:

  1. Definire la funzione in un file sorgente;
  2. Dichiarare il prototipo della funzione in tutti i file sorgenti che la vogliono adoperare prima del punto in cui la funzione è chiamata.

Riprendiamo l'esempio del calcolo dell'area del cerchio che avevamo visto nella lezione introduttiva.

In quel caso avevamo definito la funzione area_cerchio in un file sorgente circle.c:

/* circle.c */

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

Poi, avevamo realizzato il file sorgente main.c con il codice che richiamava la funzione area_cerchio:

/* main.c */
#include <stdio.h>

float area_cerchio(float raggio);

int main() {
    float raggio;
    float area;

    printf("Inserisci il raggio del cerchio: ");
    scanf("%f", &raggio);

    area = area_cerchio(raggio);

    printf("L'area del cerchio di raggio %.2f è %.2f\n", raggio, area);

    return 0;
}

Per poter far funzionare il tutto abbiamo dovuto scrivere il prototipo della funzione area_cerchio in main.c. In caso contrario, il compilatore avrebbe dato un errore di undefined reference.

Questa soluzione naïve funziona, ma presenta dei problemi:

  1. Se il prototipo della funzione area_cerchio cambia, dobbiamo modificare il prototipo in tutti i file sorgenti che la chiamano;
  2. Se la funzione area_cerchio viene utilizzata in molti file sorgenti, dobbiamo ripetere il prototipo in ognuno di essi.

Tutto ciò diventerebbe un incubo di manutenzione.

Per risolvere questi problemi possiamo utilizzare un file header.

In particolare, la soluzione corretta è:

  1. Creare un file header, circle.h che contiene il prototipo della funzione area_cerchio;
  2. Includere il file header in tutti i file sorgenti che chiamano la funzione area_cerchio.

Inoltre, includiamo circle.h anche nel file sorgente circle.c per far si che il compilatore possa controllare che il prototipo della funzione area_cerchio corrisponda alla definizione.

I nuovi file saranno:

/* circle.h */
float area_cerchio(float raggio);
/* circle.c */
#include "circle.h"

float area_cerchio(float raggio) {
    return 3.14159 * raggio * raggio;
}
/* main.c */
#include <stdio.h>

#include "circle.h"

int main() {
    float raggio;
    float area;

    printf("Inserisci il raggio del cerchio: ");
    scanf("%f", &raggio);

    area = area_cerchio(raggio);

    printf("L'area del cerchio di raggio %.2f è %.2f\n", raggio, area);

    return 0;
}

L'effetto sarà quello riportato nella figura che segue:

Condivisione di Funzioni tramite File Header
Figura 2: Condivisione di Funzioni tramite File Header

Questa soluzione può essere espansa. Ad esempio, supponiamo di voler aggiungere una funzione, perimetro_cerchio, che calcola il perimetro di un cerchio. Possiamo definire la funzione in circle.c e il prototipo in circle.h:

/* circle.h */
float area_cerchio(float raggio);
float perimetro_cerchio(float raggio);
/* circle.c */
#include "circle.h"

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

float perimetro_cerchio(float raggio) {
    return 2 * 3.14159 * raggio;
}

Quindi, ricapitolando il meccanismo:

Definizione

Condivisione di Funzioni tramite File Header

Per condividere una o più funzioni tra più file sorgenti, è sufficiente:

  1. Dichiarare i prototipi delle funzioni in un file header;
  2. Includere il file header nel file sorgente in cui le funzioni sono definite.
  3. Includere il file header nei file sorgenti che chiamano le funzioni.

Condividere Variabili Globali

La condivisione di variabili globali avviene, più o meno, nello stesso modo in cui vengono condivise le funzioni.

Per condividere una funzione, abbiamo visto sopra, è sufficiente inserire la definizione della funzione in un unico file sorgente, mentre la dichiarazione della funzione viene inserita in un file header. Quest'ultimo viene poi incluso nei file sorgenti che chiamano la funzione.

La condivisione di una variabile globale in C avviene nello stesso modo ma con una piccola differenza.

Finora, in questa guida non abbiamo fatto differenza tra dichiarazione e definizione di una variabile.

Per dichiarare, ad esempio, una variabile numero di tipo int, basta scrivere:

int numero;

Con questa riga di codice, non solo stiamo dichiarando una variabile numero di tipo int, ma stiamo anche definendo la variabile, ossia stiamo dicendo al compilatore di riservare spazio in memoria per la variabile numero.

Questi due passaggi possono essere divisi in linguaggio C attraverso l'utilizzo della parola chiave extern.

Aggiungendo extern all'inizio della dichiarazione di una variabile, stiamo dicendo al compilatore che siamo intenzionati a usare la variabile, ma che la definizione della variabile è in un altro file sorgente, ossia il suo spazio di memoria è allocato altrove.

La parola chiave extern funziona con variabili di qualunque tipo. Ad esempio, possiamo applicarla ad un array e, in tal caso, non è necessario specificare la dimensione dell'array:

extern int array[];

Dal momento che, con questa riga di codice, il compilatore non alloca lo spazio necessario per l'array, non è obbligatorio specificarne la dimensione.

Definizione

Parola chiave extern

In linguaggio C, la parola chiave extern posta davanti la dichiarazione di una variabile indica al compilatore che la variabile stessa è definita altrove, quindi il suo spazio in memoria è allocato in un altro punto.

Il punto in cui la variabile è definita può essere anche in un altro file sorgente.

Per condividere, quindi, una variabile globale tra più file sorgenti, per prima cosa definiamo la variabile in un file sorgente:

int numero;

Se la variabile numero deve essere inizializzata con un valore, possiamo farlo sempre nello stesso file:

int numero = 42;

Quando il compilatore compila questo file sorgente, alloca lo spazio in memoria per la variabile numero e la inizializza con il valore 42.

Gli altri file che vogliono adoperare la variabile numero devono dichiararla come extern:

extern int numero;

Dopodiché, ciascun file può accedere a numero sia in lettura che in scrittura. Grazie alla parola chiave extern, il compilatore sa che la variabile numero è definita in un altro punto e non deve allocare spazio in memoria per essa.

Facendo così, però, abbiamo un problema. Dobbiamo assicurarci che la variabile numero sia definita o dichiarata nei vari file sorgenti allo stesso modo.

Nota

Dichiarazioni differenti della stessa variabile

Cosa accade se una variabile viene definita in un file sorgente con un tipo, mentre viene dichiarata in un altro file sorgente con un altro tipo con extern?

In tali casi il comportamento del programma non è definito.

Ad esempio, se in un file sorgente definiamo una variabile numero di tipo int:

int numero;

Ma poi, in un altro file sorgente, dichiariamo la variabile numero come extern ma di tipo float:

extern float numero;

Il compilatore non ha modo di controllare che la dichiarazione rispetti la definizione di numero. Per cui, il comportamento del programma diventa impredicibile.

Per evitare problemi di questo tipo, possiamo utilizzare un file header.

La soluzione a questo problema consiste nel:

  1. Definire la variabile in un file sorgente;

    Ad esempio, supponiamo di definire la variabile numero nel file numero.c in cui la inizializziamo anche:

    /* numero.c */
    int numero = 42;
    
  2. Dichiarare la variabile come extern in un file header;

    Creiamo un file header numero.h in cui dichiariamo la variabile numero come extern:

    /* numero.h */
    extern int numero;
    

    Includiamo questo file anche nel file numero.c per far sì che il compilatore possa controllare che la dichiarazione rispetti la definizione:

    /* numero.c */
    #include "numero.h"
    
    int numero = 42;
    
  3. Includere il file header nei file sorgenti che vogliono adoperare la variabile numero.

    Ad esempio, se vogliamo utilizzare la variabile numero in un file sorgente main.c, dobbiamo includere il file header numero.h:

    /* main.c */
    #include "numero.h"
    
    int main() {
        printf("Il valore di numero è %d\n", numero);
        return 0;
    }
    

Il risultato è mostrato in figura:

Condivisione di Variabili Globali attraverso File Header
Figura 3: Condivisione di Variabili Globali attraverso File Header

Sebbene la condivisione di variabili globali sia possibile, è bene ricordare che l'utilizzo di variabili globali dovrebbe essere limitato al minimo indispensabile. Le variabili globali possono rendere il codice difficile da mantenere e da testare.

Ricapitolando:

Definizione

Condivisione di Variabili Globali tramite File Header

Per condividere una variabile globale tra più file sorgenti, è sufficiente:

  1. Definire la variabile in un file sorgente;
  2. Dichiarare la variabile come extern in un file header;
  3. Includere il file header nel file sorgente in cui la variabile è definita;
  4. Includere il file header nei file sorgenti che vogliono adoperare la variabile.

In Sintesi

In questa lezione abbiamo finalmente visto come scrivere un file header in linguaggio C studiando i tre casi più comuni:

  1. Condivisione di definizioni di macro e tipi: in questo modo non c'è necessità di ripetere le definizioni in ogni file sorgente;
  2. Condivisione di prototipi di funzioni: in questo modo i prototipi delle funzioni sono raccolti in un unico file di intestazione;
  3. Condivisione di variabili globali: in questo modo le variabili globali possono essere condivise tra più file sorgenti ma abbiamo dovuto adoperare la parola chiave extern.

Quello che ancora non abbiamo studiato è la possibilità di annidare o innestare i file header. Questo argomento sarà trattato nella prossima lezione.