Ok, cercherò di non essere tecnico e fare un esempio facile. Volevo fare una rappresentazione tramite grafi, ma cominciamo ragionando al contrario per spiegarti il funzionamento di un compilatore C/C++. Hai un albero, e il tuo file .cpp è il tronco. Da questo tronco partono un certo numero di rami, e ogni ramo è un file che viene importato nel .cpp con la direttiva #include. A sua volta, da ognuno di questi rami possono partire altri piccoli rami (sempre associati ai file importati con include).
Il compilatore lavora sul tronco. Nella fase di pre-processing, tutti i rami vengono seguiti e processati (semplificando di molto: immagina che ogni volta che vedi "#include <file.h>" il contenuto di file.h venga copia/incollato al posto di quella linea). Dopo aver finito di processare tutto, il parser vero e proprio entra in azione, il codice viene tradotto, e alla fine ti ritrovi con un file oggetto.
Vediamo se me la cavo ancora con l'ascii art:
Codice:
a1.h a2.h
| |
| |
+-> b.h <-+ d.h
| |
| |
+--> c.h <---+
|
|
file.cpp
In questo esempio, il compilatore parte da file.cpp, e ad un certo punto trova la direttiva #include "c.h". Quindi sostituisce la riga #include "c.h" con il contenuto del file c.h. Il file c.h a sua volta contiene due direttive: #include b.h e #include d.h, che vengono rimpiazzate dal contenuto dei file b.h e c.h rispettivamente. E così via.
Prendiamo il tuo caso particolare:
Codice:
cmath funzione.h cmath funzione.h
| | | |
| | | |
+------+-------+ +------+-------+
| |
| |
funzione.cpp test.cpp
Per ogni "albero", l'ordine di inclusione è da sinistra verso destra, il livello di inclusione è dall'alto verso il basso.
Il compilatore processa funzione.cpp, nella cui implementazione definisci le funzioni dichiarate in funzione.h e sicuramente usi funzioni matematiche presenti nella libreria math. Una volta processato funzione.cpp e prodotto il file oggetto, il compilatore si "scorda" di quello che ha già visto.
Poi passa a processare test.cpp e ancora una volta include cmath e funzione.h, e genera un file oggetto.
Il linker si occupa di mettere insieme questi due file oggetto. In particolare, nell'file test.obj ci sarà uno o più simboli con dei nomi uguali a quelli dichiarati in funzione.h, ma senza l'implementazione. Ci penserà il linker a risolvere il problema aggiungendo il corpo dei simboli che si trova in funzione.obj.
Compilatore e linker operano dunque separatamente.
Se tu rimuovi la direttiva #include <cmath> da funzione.cpp, il compilatore (che idealmente opera su un file .cpp alla volta) troverà dei simboli che non riconosce, ad esempio sinf() o cosf(). Da lì procede in 2 modi: o ti dice "non trovo la dichiarazione di sinf()" e interrompe la compilazione, o gli assegna una dichiarazione implicita (un compilatore c assume che le funzioni non dichiarate restituiscano int). Nel primo caso hai un errore in fase di compilazione, nel secondo in fase di link (perché in realtà sinf() restituisce un float, e dunque il simbolo all'interno di test.obj non è lo stesso simbolo che il compilatore ha inserito in funzione.cpp).
Spero di essere stato chiaro.