Introduzione ai File Header in Linguaggio C
I File Header o File di Intestazione sono file del linguaggio C il cui scopo è quello di condividere informazioni tra più file sorgenti.
In questa lezione introduttiva approfondiremo il loro scopo, come funzionano e come includerli nei file sorgenti.
A partire dalla prossima lezione, invece, vedremo come scrivere un file header.
La motivazione dei file header
Quando dividiamo un programma in file sorgenti multipli, come abbiamo visto nella lezione precedente, sorgono una serie di problemi fondamentali:
- Come può un file sorgente richiamare una funzione definita in un altro file sorgente?
- Come può una funzione accedere ad una variabile esterna definita in un altro file sorgente?
- Come si può condividere la stessa definizione di una macro o di una struttura dati tra più file sorgenti?
Al primo problema, nella lezione precedente, abbiamo trovato una soluzione non ottimale che consisteva nel copiare il prototipo di una funzione da un file sorgente all'altro. Questa soluzione, tuttavia, è inefficiente e non scalabile.
Per risolvere questi problemi, il linguaggio C fornisce un secondo tipo di file sorgente, chiamato file header o file di intestazione.
A differenza di un file sorgente normale, che termina con l'estensione .c
, un file header termina con l'estensione .h
.
Lo scopo di un file header non è di contenere codice, che, di norma, è contenuto in un file sorgente .c
, ma di contenere tutte quelle informazioni che possono essere condivise tra più file sorgenti, tra cui:
- Prototipi di funzioni
- Dichiarazioni di variabili esterne
- Definizioni di macro
- Definizioni di strutture dati
In questo modo, possiamo raccogliere queste informazioni in un unico file. Tale file può essere acceduto da più file sorgenti attraverso un meccanismo che prende il nome di inclusione e che, parzialmente, abbiamo già affrontato quando abbiamo studiato il funzionamento del precompilatore.
File Header
Un File Header o file di intestazione in linguaggio C è un file sorgente che termina con l'estensione .h
e che contiene tutte quelle informazioni che possono essere condivise tra più file sorgenti, tra cui prototipi di funzioni, dichiarazioni di variabili esterne, definizioni di macro e definizioni di strutture dati.
Il meccanismo con cui un file header può essere letto da un file sorgente è chiamato inclusione.
Contenuto di un File Header
Normalmente i file header vengono adoperati per contenere esclusivamente dichiarazioni. Quindi per contenere i prototipi di funzione, le definizioni di strutture dati, le dichiarazioni di variabili esterne e le definizioni di macro.
I file header non dovrebbero contenere codice effettivo, come ad esempio la definizione di funzioni.
Esistono, tuttavia, delle eccezioni a questa regola che vedremo in seguito.
Direttiva di precompilazione #include
Alla base del meccanismo di inclusione dei file header c'è la direttiva di precompilazione #include
.
Abbiamo già accennato a questa direttiva in passato, quando abbiamo parlato delle direttive di precompilazione in generale. Adesso è venuto il momento di approfondirla.
La direttiva #include
si presenta in due forme. La prima, che è quella che abbiamo adoperato sinora, include un file di intestazione del sistema o della libreria standard del C, come ad esempio <stdio.h>
. Questa forma racchiude il nome del file header tra parentesi angolari:
#include <nome_file.h>
La seconda forma, invece, include un file di intestazione definito dall'utente. Questa forma racchiude il nome del file header tra virgolette:
#include "nome_file.h"
La differenza tra le due forme è abbastanza sottile. All'atto pratico sono equivalenti, ciò che cambia è dove il compilatore cerca il file specificato. Le regole sono le seguenti:
-
#include <nome_file.h>
:Il compilatore cerca il file header nei percorsi di sistema, come ad esempio
/usr/include
su Unix oC:\Program Files\Microsoft Visual Studio\VC\include
su Windows. Questo perché è in tali percorsi che si trovano i file header del sistema e della libreria standard del C. -
#include "nome_file.h"
:Il compilatore cerca prima il file header nella stessa directory del file sorgente che contiene la direttiva
#include
. Se non lo trova, allora cerca il file nei percorsi di sistema.
Ottenere il percorso di sistema in cui gcc
cerca i file header
Un modo per ottenere la lista delle directory in cui il compilatore gcc
cerca i file header su Linux è quello di adoperare il seguente comando su shell:
$ echo | gcc -E -Wp,-v -
Adoperando questo comando otteniamo un output molto simile al seguente:
#include "..." search starts here:
#include <...> search starts here:
/usr/lib/gcc/x86_64-pc-linux-gnu/14.2.1/include
/usr/local/include
/usr/lib/gcc/x86_64-pc-linux-gnu/14.2.1/include-fixed
/usr/include
End of search list.
Dall'output di questo esempio si può notare che, di default, gcc
cerca i file header in quattro directory principali:
/usr/lib/gcc/x86_64-pc-linux-gnu/14.2.1/include
/usr/local/include
/usr/lib/gcc/x86_64-pc-linux-gnu/14.2.1/include-fixed
/usr/include
Ovviamente, il risultato può cambiare a seconda della versione di gcc
e della distribuzione Linux.
L'ordine delle directory è importante, perché gcc
cerca i file header in queste directory in sequenza. Nel senso che, se, ad esempio, esistessero due file header con lo stesso nome in due directory diverse, gcc
utilizzerebbe il primo che trova.
File Header e Processo di Compilazione
Nella lezione precedente abbiamo approfondito il processo di compilazione di programmi scritti in linguaggio C.
In particolare, abbiamo visto che ogni file sorgente con estensione .c
deve essere compilato in un file oggetto con estensione .o
. Questi file oggetto vengono poi collegati insieme dal linker per formare l'eseguibile finale.
Quello che ci domandiamo adesso è dove si collocano i file header in questo meccanismo?
La risposta è: i file header non devono essere compilati.
In altre parole, quando compiliamo un programma composto da vari file sorgente .c
e vari file header .h
solo i file sorgente .c
devono essere compilati.
La motivazione sta proprio nel meccanismo di inclusione. Infatti, dal momento che i file header vengono inclusi nei file sorgenti, tutto il loro contenuto viene copiato nel file sorgente prima della compilazione. Quindi, il compilatore ha già a disposizione tutte le informazioni contenute nei file header.
I File Header non devono essere Compilati
I file header in linguaggio C non devono essere compilati. Questo perché il loro contenuto viene copiato nei file sorgenti prima della compilazione attraverso la direttiva #include
.
Ad esempio, se un programma è composto da due file sorgenti file1.c
e file2.c
e da due file header header1.h
e header2.h
, il processo di compilazione è il seguente:
-
file1.c
viene compilato infile1.o
;$ gcc -c file1.c -o file1.o
-
file2.c
viene compilato infile2.o
;$ gcc -c file2.c -o file2.o
-
file1.o
efile2.o
vengono collegati insieme dal linker per formare l'eseguibile finale;$ gcc file1.o file2.o -o eseguibile
Modificare il percorso di ricerca dei file header
Con la direttiva #include
il compilatore cerca i file di intestazione nei percorsi di sistema oppure nella stessa directory del file sorgente che contiene la direttiva #include
a seconda della forma utilizzata.
Tuttavia è possibile modificare il percorso di ricerca dei file header in due modi.
Il primo modo consiste nello specificare il percorso della directory dove i file header vanno cercati da parte del compilatore.
Nel caso del compilatore gcc
, possiamo specificare il percorso di ricerca dei file header con l'opzione -I
seguita dal percorso della directory. Ad esempio:
$ gcc -I/path/to/directory file.c
Da notare che, in questo modo si ottengono due risultati:
- Il primo è che il percorso
/path/to/directory
viene aggiunto alla lista dei percorsi di ricerca dei file header di sistema; - Il secondo è che i file header contenuti in
/path/to/directory
possono essere inclusi nei file sorgenti con la direttiva#include <nome_file.h>
, quindi con le parentesi angolari. Questo perché diventano a tutti gli effetti file header di sistema.
Possiamo aggiungere più di un percorso di ricerca dei file header specificando più volte l'opzione -I
:
$ gcc -I/path/to/directory1 -I/path/to/directory2 file.c
Aggiunta di un Percorso di Ricerca dei File Header
Tutti i compilatori C permettono di aggiungere altri percorsi di ricerca dei file header diversi da quelli di sistema.
Quando si usa questo meccanismo, i file header contenuti in questi percorsi diventano a tutti gli effetti file header di sistema e possono essere inclusi nei file sorgenti con la direttiva #include <nome_file.h>
.
La sintassi per il compilatore gcc
è la seguente:
$ gcc -I/path/to/directory file.c
Il secondo modo consiste nello specificare direttamente il percorso del file header da includere. Questo percorso può essere sia assoluto che relativo.
Prendiamo un esempio. Supponiamo di avere la seguente struttura di directory:
/home/user/progetto
|
├── include
│ └── header.h
└── src
└── main.c
In questo caso abbiamo due file:
main.c
il cui percorso completo è/home/user/progetto/src/main.c
;header.h
il cui percorso completo è/home/user/progetto/include/header.h
.
Dobbiamo includere header.h
in main.c
. Possiamo farlo in due modi:
-
Specificando il percorso assoluto di
header.h
:#include "/home/user/progetto/include/header.h"
-
Specificando il percorso relativo di
header.h
:#include "../include/header.h"
Infatti, la direttiva #include
permette di specificare sia percorsi assoluti che relativi.
Inoltre, i percorsi racchiusi tra doppi apici, "
, non vengono trattati come stringhe. Per cui, ad esempio, sotto Windows possiamo scrivere:
// Inclusione assoluta sotto Windows
#include "C:\path\to\file.h"
Senza dover preoccuparci dei caratteri di escape.
Inoltre, sempre sotto Windows, la maggior parte dei compilatori permette di adoperare, come separatore di directory, sia il backslash \
che lo slash /
. Quindi, possiamo scrivere:
// Inclusione assoluta sotto Windows
#include "C:/path/to/file.h"
Inclusione di File Header con Percorso Assoluto o Relativo
La direttiva #include
permette di specificare sia percorsi assoluti che relativi per includere un file header.
La sintassi per includere un file header con percorso assoluto è la seguente:
#include "/percorso/assoluto/nome_file.h"
La sintassi per includere un file header con percorso relativo è la seguente:
#include "../percorso/relativo/nome_file.h"
Evitare i percorsi assoluti nei file sorgenti
Sebbene sia consentito, è sempre meglio evitare di adoperare percorsi assoluti nelle direttive #include
all'interno dei file sorgenti.
I percorsi assoluti rendono il codice meno portabile, perché il codice sorgente diventa dipendente dalla struttura delle cartelle del sistema in cui è stato scritto.
Quello che conviene, invece, è adoperare percorsi relativi, in modo che il codice sorgente possa essere spostato da un sistema all'altro senza dover modificare le direttive #include
.
Terza forma della direttiva include
Sopra abbiamo visto che esistono due forme della direttiva #include
: una con le parentesi angolari, per i file header di sistema, ed una con i doppi apici, per i file header definiti dall'utente.
In realtà, esiste una terza forma per questa direttiva utile in alcuni casi. Questa forma prende il nome di include
con macro.
La sintassi di questa forma è la seguente:
#include macro1 macro2 ... macroN
In questo caso, le macro macro1
, macro2
, ..., macroN
sono macro qualunque del preprocessore che vengono espanse e sostituite. Il vincolo è che, dopo l'espansione, il risultato deve essere una delle due forme standard della direttiva #include
.
Il vantaggio di adoperare questa terza forma è che possiamo evitare di codificare direttamente il nome dei file header nei file sorgenti ma definirli con apposite macro. Questo torna utile, ad esempio, per la compilazione condizionale di un programma.
Ad esempio, supponiamo di avere una funzione esempio
che deve essere implementata in maniera diversa a seconda del sistema operativo. Possiamo implementare le tre versioni di esempio
in tre file differenti con annessi file header diversi:
esempio_linux.c
eesempio_linux.h
per Linux;esempio_windows.c
eesempio_windows.h
per Windows;esempio_macos.c
eesempio_macos.h
per macOS.
A questo punto, nel file sorgente che deve usare esempio
possiamo scrivere il codice che include il file header corretto a seconda del sistema operativo:
#ifdef __linux__
#define FILE_HEADER "esempio_linux.h"
#elif _WIN32
#define FILE_HEADER "esempio_windows.h"
#elif __APPLE__
#define FILE_HEADER "esempio_macos.h"
#endif
#include FILE_HEADER
In questo modo, a seconda del sistema operativo, verrà incluso il file header corretto.
Inclusione di File Header con Macro
La direttiva #include
permette di includere un file header utilizzando delle macro del preprocessore. L'importante è che l'espansione delle macro porti ad una delle due forme standard della direttiva #include
.
La sintassi per includere un file header con macro è la seguente:
#include macro1 macro2 ... macroN
Conclusioni
In questa lezione abbiamo introdotto i file header o file di intestazione in linguaggio C.
Questi file sono utili per condividere informazioni tra più file sorgenti, come prototipi di funzioni, dichiarazioni di variabili esterne, definizioni di macro e definizioni di strutture dati.
Abbiamo visto che i file header terminano con l'estensione .h
e che possono essere inclusi nei file sorgenti con la direttiva di precompilazione #include
.
La direttiva #include
si presenta in due forme: una con le parentesi angolari, per i file header di sistema, e una con i doppi apici, per i file header definiti dall'utente.
Infine, abbiamo visto che esiste una terza forma della direttiva #include
che permette di includere file header utilizzando delle macro del preprocessore.
Tuttavia, non abbiamo ancora visto come scrivere un file header. Nelle prossime lezioni, vedremo come fare per:
- Condividere prototipi di funzioni tra più file sorgenti;
- Condividere dichiarazioni di variabili esterne tra più file sorgenti;
- Condividere definizioni di macro tra più file sorgenti.