Mentre puoi includere .cpp file come hai detto, questa è una cattiva idea.
Come hai detto, le dichiarazioni appartengono ai file di intestazione. Questi non causano problemi quando sono inclusi in più unità di compilazione perché non includono le implementazioni. Includere più volte la definizione di una funzione o di un membro della classe causerà un problema (ma non sempre) perché il linker verrà confuso e genererà un errore.
Ciò che dovrebbe accadere è che ogni file .cpp include definizioni per un sottoinsieme del programma, come una classe, un gruppo di funzioni logicamente organizzato, variabili statiche globali (usare con parsimonia se non del tutto), ecc.
Ogni unità di compilazione ( .cpp file) include quindi qualsiasi dichiarazione di cui ha bisogno per compilare le definizioni che contiene. Tiene traccia delle funzioni e delle classi a cui fa riferimento ma non contiene, quindi il linker può risolverle in seguito quando combina il codice oggetto in un eseguibile o in una libreria.
Esempio
-
Foo.h - > contiene dichiarazione (interfaccia) per la classe Foo.
-
Foo.cpp - > contiene la definizione (implementazione) per la classe Foo.
-
Main.cpp - > contiene il metodo principale, il punto di ingresso del programma. Questo codice crea un'istanza di Foo e lo usa.
Sia Foo.cpp che Main.cpp devono includere Foo.h . Foo.cpp ne ha bisogno perché sta definendo il codice che supporta l'interfaccia di classe, quindi ha bisogno di sapere cos'è l'interfaccia. Main.cpp ne ha bisogno perché sta creando un Foo e invocando il suo comportamento, quindi deve sapere qual è il comportamento, la dimensione di un Foo in memoria e come trovarne le funzioni, ecc. ma non ha bisogno dell'attuazione effettiva ancora.
Il compilatore genererà Foo.o da Foo.cpp che contiene tutto il codice classe Foo in forma compilata. Genera anche Main.o che include il metodo principale ei riferimenti non risolti alla classe Foo.
Ora arriva il linker, che combina i due file oggetto Foo.o e Main.o in un file eseguibile. Vede i riferimenti Foo non risolti in Main.o ma vede che Foo.o contiene i simboli necessari, quindi "collega i punti" per così dire. Una chiamata di funzione in Main.o ora è connessa alla posizione effettiva del codice compilato, quindi in fase di esecuzione, il programma può passare alla posizione corretta.
Se avessi incluso il file Foo.cpp in Main.cpp , ci sarebbero due definizioni della classe Foo. Il linker vedrebbe questo e dirà "Non so quale scegliere, quindi questo è un errore". La fase di compilazione avrebbe avuto successo, ma il collegamento non lo avrebbe fatto. (A meno che tu non compili solo Foo.cpp , ma perché è in un file .cpp separato?)
Infine, l'idea di diversi tipi di file è irrilevante per un compilatore C / C ++. Compila "file di testo" che si spera contengano codice valido per la lingua desiderata. A volte può essere in grado di dire la lingua in base all'estensione del file. Ad esempio, compila un file .c senza opzioni del compilatore e assumerà C, mentre un'estensione .cc o .cpp gli direbbe di assumere C ++. Tuttavia, posso facilmente dire a un compilatore di compilare un file .h o anche .docx come C ++, ed emetterà un oggetto ( .o ) se contiene codice C ++ valido in formato di testo normale. Queste estensioni sono più a beneficio del programmatore. Se vedo Foo.h e Foo.cpp , presumo immediatamente che il primo contenga la dichiarazione della classe e il secondo contenga la definizione.