Scrittura di File Header in linguaggio C
Vediamo come realizzare un file header in linguaggio C studiando separatamente tre casi:
- Condivisione di definizioni di macro e tipi:
- Condivisione di prototipi di funzioni;
- 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:
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 dishort
anzichéint
, ci basterebbe cambiare il contenuto del file headerboolean.h
e tutti i file sorgenti che includonoboolean.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:
- Definire la funzione in un file sorgente;
- 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:
- Se il prototipo della funzione
area_cerchio
cambia, dobbiamo modificare il prototipo in tutti i file sorgenti che la chiamano; - 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 è:
- Creare un file header,
circle.h
che contiene il prototipo della funzionearea_cerchio
; - 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:
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:
Condivisione di Funzioni tramite File Header
Per condividere una o più funzioni tra più file sorgenti, è sufficiente:
- Dichiarare i prototipi delle funzioni in un file header;
- Includere il file header nel file sorgente in cui le funzioni sono definite.
- 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.
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.
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:
-
Definire la variabile in un file sorgente;
Ad esempio, supponiamo di definire la variabile
numero
nel filenumero.c
in cui la inizializziamo anche:/* numero.c */ int numero = 42;
-
Dichiarare la variabile come
extern
in un file header;Creiamo un file header
numero.h
in cui dichiariamo la variabilenumero
comeextern
:/* 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;
-
Includere il file header nei file sorgenti che vogliono adoperare la variabile
numero
.Ad esempio, se vogliamo utilizzare la variabile
numero
in un file sorgentemain.c
, dobbiamo includere il file headernumero.h
:/* main.c */ #include "numero.h" int main() { printf("Il valore di numero è %d\n", numero); return 0; }
Il risultato è mostrato in figura:
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:
Condivisione di Variabili Globali tramite File Header
Per condividere una variabile globale tra più file sorgenti, è sufficiente:
- Definire la variabile in un file sorgente;
- Dichiarare la variabile come
extern
in un file header; - Includere il file header nel file sorgente in cui la variabile è definita;
- 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:
- Condivisione di definizioni di macro e tipi: in questo modo non c'è necessità di ripetere le definizioni in ogni file sorgente;
- Condivisione di prototipi di funzioni: in questo modo i prototipi delle funzioni sono raccolti in un unico file di intestazione;
- 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.