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.

Definizione

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.

Nota

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 o C:\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.

Consiglio

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.

Definizione

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:

  1. file1.c viene compilato in file1.o;

    $ gcc -c file1.c -o file1.o
    
  2. file2.c viene compilato in file2.o;

    $ gcc -c file2.c -o file2.o
    
  3. file1.o e file2.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:

  1. Il primo è che il percorso /path/to/directory viene aggiunto alla lista dei percorsi di ricerca dei file header di sistema;
  2. 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
Definizione

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:

  1. Specificando il percorso assoluto di header.h:

    #include "/home/user/progetto/include/header.h"
    
  2. 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"
Definizione

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"
Nota

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 e esempio_linux.h per Linux;
  • esempio_windows.c e esempio_windows.h per Windows;
  • esempio_macos.c e esempio_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.

Definizione

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.