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,unionedenum - Dichiarazioni
typedef - Macro pensate per essere condivise
- Dichiarazioni
externdelle variabili globali condivise - Gli
#includedi 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
#includedi 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.