Menu

File header in C: .h e .c, include guard e programmi su più file

Come dividere un programma C su più file: cosa va in un .h, cosa va in un .c, le include guard che impediscono la doppia inclusione, compilare più file insieme e condividere variabili globali con extern.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

Un singolo file .c va bene finché non arriva a 2.000 righe. Dividere un programma su più file permette di compilare ogni parte separatamente, riusarla in altri programmi e leggerla da sola, ma il C non ha un sistema di import. Quello che ha è il preprocessore che incolla testo, più un linker che alla fine unisce i pezzi.

Un file header (.h) è il contratto condiviso tra quei pezzi: dice a ogni file sorgente cosa esiste altrove, senza contenerne l'implementazione.

Dichiarazioni e definizioni

Tutto il meccanismo si regge su una sola distinzione.

Una dichiarazione dice questa cosa esiste da qualche parte e ha questa forma. Non genera codice e può comparire un numero qualsiasi di volte:

int add(int a, int b);        /* dichiarazione di funzione (prototipo) */
extern int error_count;       /* dichiarazione di variabile */
struct Point { int x, y; };   /* definizione di tipo: ripetibile in ogni file */

Una definizione crea la cosa. Deve comparire esattamente una volta in tutto il programma:

int add(int a, int b) { return a + b; }   /* definizione di funzione */
int error_count = 0;                      /* definizione di variabile */

Gli header contengono dichiarazioni. I file sorgente contengono definizioni. Se inverti le cose il linker si lamenta con "multiple definition of ...", l'unico messaggio d'errore che significa quasi sempre che una definizione è finita in un header.

Un programma su due file

Ecco la divisione utile più piccola. Un header che dichiara due funzioni:

/* math_utils.h */
#ifndef MATH_UTILS_H
#define MATH_UTILS_H

int add(int a, int b);
int max_of(int a, int b);

#endif

Il file sorgente che le implementa (nota che include il proprio header):

/* math_utils.c */
#include "math_utils.h"

int add(int a, int b) {
    return a + b;
}

int max_of(int a, int b) {
    return (a > b) ? a : b;
}

E il programma che le usa:

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

int main(void) {
    printf("add(3, 4)    = %d\n", add(3, 4));
    printf("max_of(3, 4) = %d\n", max_of(3, 4));
    return 0;
}

Compila insieme entrambi i file sorgente:

gcc main.c math_utils.c -o app
./app

Due dettagli da notare. Primo, math_utils.c include il proprio header, e non è ridondante. Fa controllare al compilatore che ogni definizione corrisponda alla sua dichiarazione, quindi se cambi il prototipo nell'header e dimentichi il file .c, ricevi subito un errore invece di una discrepanza in fase di linking.

Secondo, math_utils.h non compare sulla riga di comando di gcc. Gli header non vengono mai compilati; vengono incollati nei file .c da #include. Passare un .h al compilatore produce un file di header precompilato inutile e nessun codice da collegare.

Lo stesso programma compresso in un solo file, così puoi eseguirlo qui:

Le dichiarazioni prima di main sono quello che l'header fornisce nella versione reale, ed è esattamente per questo che i prototipi di funzione e gli header sono la stessa idea a due scale diverse.

Include guard

#include incolla testo, e un testo incollato due volte è un testo duplicato. Per un prototipo di funzione non fa danni, per una struct è fatale:

/* shapes.h SENZA guard */
struct Point { int x, y; };

Se main.c include sia shapes.h sia canvas.h, e anche canvas.h include shapes.h, il compilatore vede struct Point definita due volte nella stessa unità di traduzione e si ferma con "redefinition of 'struct Point'". In un progetto reale queste catene diventano abbastanza profonde da non poterle seguire a mano.

La soluzione è un'include guard: una macro che registra "questo header è già stato incollato".

/* shapes.h */
#ifndef SHAPES_H
#define SHAPES_H

struct Point { int x, y; };
struct Point origin_point(void);

#endif /* SHAPES_H */

La prima inclusione trova SHAPES_H non definita, quindi il contenuto viene tenuto, e intanto definisce SHAPES_H. Ogni inclusione successiva nello stesso file la trova definita e salta dritta a #endif. Il nome della macro deve essere unico in tutto il progetto; la convenzione abituale è FILENAME_H, ricavato dal percorso.

L'alternativa su una riga è supportata da tutti i compilatori più diffusi:

/* shapes.h */
#pragma once

struct Point { int x, y; };

#pragma once non può subire collisioni di nomi e non può essere rotta da un errore di battitura in #endif. L'unico svantaggio è che non fa parte dello standard C, quindi un progetto che deve compilare su compilatori insoliti dovrebbe preferire la forma #ifndef. In ogni caso, ogni header ne ha una, senza eccezioni, compresi gli header che pensi nessun altro includerà.

Cosa va in un header

Metti in un .h:

  • Prototipi di funzione
  • Definizioni di struct, union ed enum
  • Dichiarazioni typedef
  • Macro pensate per essere condivise
  • Dichiarazioni extern delle variabili globali condivise
  • Gli #include di cui l'header stesso ha bisogno per essere autosufficiente

Tieni fuori da un .h:

  • I corpi delle funzioni (a meno che non siano volutamente static inline)
  • Le definizioni di variabili: int counter; in un header definisce una variabile separata in ogni file che lo include, oppure causa un errore di linking, a seconda del compilatore
  • Gli #include di header di cui l'header stesso non ha bisogno: scaricano quella dipendenza su tutti quelli che lo usano

Vale la pena fare di "autosufficiente" una regola: un header deve compilare quando viene incluso per primo, prima di qualsiasi altra cosa. Se shapes.h usa size_t, include lui stesso <stddef.h> invece di sperare che l'abbia fatto il file che lo include.

Un header completo e ben scritto:

/* inventory.h */
#ifndef INVENTORY_H
#define INVENTORY_H

#include <stddef.h>   /* per size_t, usato qui sotto */

#define MAX_NAME 64

typedef struct {
    char   name[MAX_NAME];
    int    quantity;
    double price;
} Item;

/* condivisa in tutto il programma, definita una volta in inventory.c */
extern int item_count;

void   inventory_add(const Item *item);
double inventory_total(void);
size_t inventory_size(void);

#endif /* INVENTORY_H */

Condividere una variabile globale con extern

Una variabile globale deve essere definita in esattamente un file .c e dichiarata in tutti gli altri. È extern che crea la dichiarazione:

/* inventory.h: dichiarazione, nessuna memoria */
extern int item_count;
/* inventory.c: l'unica definizione */
#include "inventory.h"
int item_count = 0;
/* main.c: la usa, tramite l'header */
#include <stdio.h>
#include "inventory.h"

int main(void) {
    printf("%d articoli\n", item_count);
    return 0;
}

Togli extern dall'header e ogni file che lo include definisce il proprio item_count, il che nel migliore dei casi è un errore di linking "multiple definition" e nel peggiore due contatori indipendenti.

L'esigenza opposta è altrettanto comune: una variabile o una funzione di supporto che deve restare privata a un solo file .c. static a livello di file fa proprio questo: dà al nome un collegamento interno, invisibile al linker e quindi a tutti gli altri file:

/* inventory.c */
static Item storage[256];                  /* privato a questo file */
static int  find_slot(const char *name);   /* funzione di supporto privata */

Due file possono avere ciascuno uno static int counter; senza collisioni. È la versione C di un membro privato, ed è la scelta predefinita da cui partire: nell'header va solo ciò di cui gli altri file hanno davvero bisogno.

Compilare programmi più grandi

Elencare ogni file funziona ma è lento, perché ogni file viene ricompilato ogni volta:

gcc main.c inventory.c report.c -o app

La forma che scala compila ogni sorgente in un file oggetto e poi li collega:

gcc -c main.c        # produce main.o
gcc -c inventory.c   # produce inventory.o
gcc -c report.c      # produce report.o
gcc main.o inventory.o report.o -o app

Ora modificare report.c richiede solo gcc -c report.c e un nuovo linking. È esattamente la contabilità che automatizza make:

app: main.o inventory.o report.o
	gcc main.o inventory.o report.o -o app

%.o: %.c
	gcc -Wall -Wextra -c $< -o $@

Se i tuoi header stanno in una sottocartella, -Iinclude la aggiunge al percorso di ricerca delle parentesi angolari.

Gli errori e cosa significano

I due tipi di fallimento sono facili da distinguere una volta che sai quale fase li ha prodotti.

"undefined reference to 'add'": un errore del linker. La dichiarazione è stata trovata, la definizione no. Hai dimenticato di elencare il file .c sulla riga di comando, oppure la funzione è static, oppure il nome è scritto male in uno dei due posti.

"multiple definition of 'item_count'": anche questo è un errore del linker, l'immagine speculare del precedente: una definizione è finita in un header, o in due file sorgente. Spostala in un solo .c e lascia una dichiarazione extern nell'header.

"redefinition of 'struct Item'": un errore del compilatore, che significa che un header è stato incollato due volte nello stesso file. Aggiungi l'include guard.

"implicit declaration of function 'add'": un avviso del compilatore (un errore in C99 e nelle modalità successive) che significa che il prototipo non è mai stato visto. Hai dimenticato l'#include, oppure l'header non lo dichiara.

Quando un programma si estende su più file, la cosa successiva che di solito vuoi è variare ciò che viene compilato in base alla piattaforma o al tipo di build, cioè la compilazione condizionale.

Domande frequenti

Cosa va in un file .h e cosa in un file .c?

L'header contiene le dichiarazioni: prototipi di funzione, definizioni di struct e typedef, enum, macro e dichiarazioni extern delle variabili globali condivise. Il file .c contiene le definizioni: i corpi delle funzioni e le variabili vere e proprie. La regola pratica: un header dice cosa esiste, un file sorgente dice cosa fa.

Cos'è un'include guard e perché serve?

#include incolla testo, quindi includere un header due volte ne incolla due volte il contenuto, ridefinendo ogni struct e typedef e facendo fallire la compilazione. Un'include guard racchiude l'header tra #ifndef MYHEADER_H / #define MYHEADER_H / #endif, così la seconda inclusione trova la macro già definita e salta il contenuto.

Come si compila un programma C con più file?

Elenca ogni file .c sulla riga di comando: gcc main.c math_utils.c -o app. Non metterci mai un file .h: gli header vengono incollati da #include, non compilati da soli. Per progetti più grandi, compila in file oggetto (gcc -c main.c) e collegali, che è proprio ciò che automatizza un Makefile.

Meglio #pragma once o le include guard con #ifndef?

Funzionano entrambe. #pragma once è una sola riga, non può avere collisioni di nomi e tutti i compilatori più diffusi la supportano, ma non fa parte dello standard C. La forma #ifndef/#define/#endif è standard e funziona ovunque. Scegline una e usala in modo coerente in tutto il progetto; per la massima portabilità scegli la forma #ifndef.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA