Perché dobbiamo includere il .h mentre tutto funziona quando si include solo il file .cpp?


18

Perché abbiamo bisogno di includere sia l' .he .cppfile, mentre siamo in grado di farlo funzionare esclusivamente includendo il .cppfile di?

Ad esempio: creazione di file.hdichiarazioni di contenimento, quindi creazione di file.cppdefinizioni di contenimento e includendo entrambi in main.cpp.

In alternativa: creare una file.cppdichiarazione / definizioni contenenti (senza prototipi) includendola in main.cpp.

Entrambi lavorano per me. Non riesco a vedere la differenza. Forse alcune informazioni dettagliate sul processo di compilazione e collegamento possono essere di aiuto.


Condividere la tua ricerca aiuta tutti . Raccontaci cosa hai provato e perché non ha soddisfatto le tue esigenze. Ciò dimostra che hai impiegato del tempo per cercare di aiutarti, ci salva dal ribadire risposte ovvie e soprattutto ti aiuta a ottenere una risposta più specifica e pertinente. Vedi anche Come chiedere
moscerino il

Consiglio vivamente di guardare le domande correlate qui a lato. programmers.stackexchange.com/questions/56215/… e programmers.stackexchange.com/questions/115368/… sono buone letture

Perché questo è sui programmatori e non su SO?
Corse di leggerezza con Monica

@LightnessRacesInOrbit perché è una questione di stile di codifica e non una domanda "come fare".
CashCow,

@CashCow: Sembra che tu abbia frainteso SO, che non è un sito "come fare". Sembra anche che tu abbia frainteso questa domanda, che non è una questione di stile di codifica.
Corse di leggerezza con Monica il

Risposte:


29

Mentre puoi includere .cppfile come hai già detto, questa è una cattiva idea.

Come hai detto, le dichiarazioni appartengono ai file di intestazione. Questi non causano problemi se inclusi in più unità di compilazione perché non includono implementazioni. Includere una definizione di una funzione o di un membro della classe più volte normalmente causerà un problema (ma non sempre) perché il linker verrà confuso e genererà un errore.

Ciò che dovrebbe accadere è che ogni .cppfile includa 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 ( .cppfile) include quindi tutte le dichiarazioni necessarie per compilare le definizioni in essa contenute. Tiene traccia delle funzioni e delle classi a cui fa riferimento ma che non contiene, quindi il linker può risolverli in un secondo momento quando combina il codice oggetto in un eseguibile o in una libreria.

Esempio

  • Foo.h -> contiene la dichiarazione (interfaccia) per la classe Foo.
  • Foo.cpp -> contiene la definizione (implementazione) per la classe Foo.
  • Main.cpp-> contiene il metodo principale, punto di ingresso del programma. Questo codice crea un'istanza di Foo e la utilizza.

Entrambi Foo.cppe Main.cppdevono essere inclusi Foo.h. Foo.cppne ha bisogno perché sta definendo il codice che supporta l'interfaccia di classe, quindi ha bisogno di sapere quale sia l'interfaccia. Main.cppne ha bisogno perché sta creando un Foo e invocando il suo comportamento, quindi deve sapere qual è quel comportamento, la dimensione di un Foo in memoria e come trovare le sue funzioni, ecc. ma non ha ancora bisogno dell'attuazione effettiva.

Il compilatore genererà Foo.oda Foo.cppcui contiene tutto il codice della classe Foo in forma compilata. Genera anche Main.oche include il metodo principale e riferimenti irrisolti alla classe Foo.

Ora arriva il linker, che combina i due file oggetto Foo.oe Main.oin un file eseguibile. Vede i riferimenti irrisolti di Foo Main.oma vede che Foo.ocontiene i simboli necessari, quindi "collega i punti" per così dire. Una chiamata di funzione 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 Foo.cppfile Main.cpp, ci sarebbero due definizioni della classe Foo. Il linker lo vedrebbe e dire "Non so quale scegliere, quindi questo è un errore". La fase di compilazione avrebbe esito positivo, ma il collegamento no. (A meno che non si compili Foo.cppma perché è in un .cppfile separato ?)

Infine, l'idea di diversi tipi di file è irrilevante per un compilatore C / C ++. Compila "file di testo" che si spera contengano un codice valido per la lingua desiderata. A volte potrebbe essere in grado di dire la lingua in base all'estensione del file. Ad esempio, compila un .cfile senza opzioni di compilatore e assumerà C, mentre un'estensione .cco .cppgli direbbe di assumere C ++. Tuttavia, posso facilmente dire a un compilatore di compilare un file .ho anche .docxcome C ++ ed emetterà un .ofile object ( ) se contiene un codice C ++ valido in formato testo normale. Queste estensioni sono più a beneficio del programmatore. Se vedo Foo.he Foo.cpp, presumo immediatamente che il primo contenga la dichiarazione della classe e il secondo contenga la definizione.


Non capisco l'argomento che stai facendo qui. Se includi semplicemente Foo.cppin Main.cppnon è necessario includere il .hfile, hai un file in meno, vinci comunque a dividere il codice in file separati per leggibilità e il tuo comando di compilazione è più semplice. Mentre capisco l'importanza dei file di intestazione, non penso che sia così
Benjamin Gruenbaum,

2
Per un programma banale, basta mettere tutto in un unico file. Non includere nemmeno le intestazioni del progetto (solo le intestazioni della libreria). Includere i file di origine è una cattiva abitudine che causerà problemi a tutti i programmi tranne quelli più piccoli una volta che si hanno più unità di compilazione e le si compila separatamente.

2
If you had included the Foo.cpp file in Main.cpp, there would be two definitions of class Foo.Frase più importante relativa alla domanda.
marczellm,

@Snowman Giusto, che è quello su cui penso dovresti concentrarti - più unità di compilazione. Attaccare il codice con / senza file di intestazione per suddividere il codice in pezzi più piccoli è utile e ortogonale allo scopo dei file di intestazione. Se tutto ciò che vogliamo fare è dividerlo in file, i file di intestazione non ci comprano nulla - una volta che abbiamo bisogno di collegamenti dinamici, collegamenti condizionati e più build anticipate sono improvvisamente molto utili.
Benjamin Gruenbaum,

Esistono molti modi per organizzare l'origine per un compilatore / linker C / C ++. Il richiedente non era sicuro del processo, quindi ho spiegato le migliori pratiche su come organizzare il codice e su come funziona il processo. Puoi dividere i capelli sul fatto che è possibile organizzare il codice in modo diverso, ma il fatto rimane, ho spiegato come il 99% dei progetti là fuori lo fanno, che è la migliore pratica per un motivo. Funziona bene e mantiene la tua sanità mentale.

9

Maggiori informazioni sul ruolo del preprocessore C e C ++ , che è concettualmente la prima "fase" del compilatore C o C ++ (storicamente era un programma separato /lib/cpp; ora, per motivi di prestazioni, è integrato all'interno del compilatore cc1 o cc1plus). Leggi in particolare la documentazione del cpppreprocessore GNU . Quindi, in pratica, il compilatore prima concettualmente elabora la tua unità di compilazione (o unità di traduzione ) e quindi lavora sul modulo preelaborato.

Probabilmente dovrai sempre includere il file header file.hse contiene (come dettato da convenzioni e abitudini ):

  • definizioni macro
  • definizioni dei tipi (ad es typedef. struct, classecc., ecc.)
  • definizioni di static inlinefunzioni
  • dichiarazioni di funzioni esterne.

Si noti che è una questione di convenzioni (e convenienza) metterle in un file di intestazione.

Naturalmente, l'implementazione file.cpprichiede tutto quanto sopra, quindi all'inizio lo vuole #include "file.h" .

Questa è una convenzione (ma molto comune). È possibile evitare i file di intestazione e copiare e incollare il loro contenuto in file di implementazione (ad esempio unità di traduzione). Ma non lo vuoi (tranne forse se il tuo codice C o C ++ viene generato automaticamente; quindi potresti fare in modo che il programma generatore esegua quel copia e incolla, imitando il ruolo del preprocessore).

Il punto è che il preprocessore sta eseguendo solo operazioni testuali . Potresti (in linea di principio) evitarlo del tutto copiando e incollando o sostituendolo con un altro "preprocessore" o generatore di codice C (come gpp o m4 ).

Un ulteriore problema è che i recenti standard C (o C ++) definiscono diverse intestazioni standard. La maggior parte delle implementazioni davvero implementano questi standard di intestazioni come (implementazione specifici) file , ma credo che sarebbe stato possibile per un conforme attuazione per implementare standard prevede (come #include <stdio.h>per C, o #include <vector>per C ++) con alcuni trucchi di magia (ad esempio utilizzando alcuni database o un po ' informazioni all'interno del compilatore).

Se si utilizzano compilatori GCC (ad esempio gcco g++) è possibile utilizzare il -Hflag per essere informato su ogni inclusione e i -C -Eflag per ottenere il modulo preelaborato. Naturalmente ci sono molti altri flag del compilatore che influenzano la preelaborazione (ad esempio -I /some/dir/per aggiungere /some/dir/per la ricerca di file inclusi e -Dper predefinire alcune macro del preprocessore, ecc. Ecc.).

NB. Le versioni future di C ++ (forse C ++ 20 , forse anche più tardi) potrebbero avere moduli C ++ .


1
Se i collegamenti marcissero, sarebbe comunque una buona risposta?
Dan Pichelman,

4
Ho modificato la mia risposta per migliorarla e scelgo con cura i collegamenti - a Wikipedia o alla documentazione GNU - che credo rimarranno pertinenti per lungo tempo.
Basile Starynkevitch,

3

A causa del modello di build a più unità di C ++, hai bisogno di un modo per avere il codice che appare nel tuo programma una sola volta (definizioni) e hai bisogno di un modo per avere il codice che appare in ogni unità di traduzione del tuo programma (dichiarazioni).

Da qui nasce il linguaggio dell'intestazione C ++. È una convenzione per un motivo.

È possibile scaricare l'intero programma in un'unica unità di traduzione, ma ciò introduce problemi con il riutilizzo del codice, il test dell'unità e la gestione delle dipendenze tra i moduli. È anche solo un gran casino.


0

La risposta selezionata da Perché dobbiamo scrivere un file di intestazione? è una spiegazione ragionevole, ma volevo aggiungere ulteriori dettagli.

Sembra che il razionale per i file di intestazione tende a perdersi nell'insegnamento e nella discussione di C / C ++.

Il file delle intestazioni offre una soluzione a due problemi di sviluppo dell'applicazione:

  1. Separazione di interfaccia e implementazione
  2. Tempi di compilazione / collegamento migliorati per programmi di grandi dimensioni

Il C / C ++ può scalare da piccoli programmi a file multi-milione di righe e migliaia di programmi. Lo sviluppo di applicazioni può scalare da team di uno sviluppatore a centinaia di sviluppatori.

Puoi indossare diversi cappelli come sviluppatore. In particolare, puoi essere l'utente di un'interfaccia per funzioni e classi oppure puoi essere autore di un'interfaccia di funzioni e classi.

Quando si utilizza una funzione, è necessario conoscere l'interfaccia delle funzioni, quali parametri utilizzare, quali funzioni restituiscono e è necessario sapere cosa fa la funzione. Questo è facilmente documentabile nel file di intestazione senza mai guardare all'implementazione. Quando hai letto l'implementazione di printf? Acquista lo usiamo ogni giorno.

Quando sei lo sviluppatore di un'interfaccia, il cappello cambia l'altra direzione. Il file di intestazione fornisce la dichiarazione dell'interfaccia pubblica. Il file di intestazione definisce le esigenze di un'altra implementazione per utilizzare questa interfaccia. Le informazioni interne e private a questa nuova interfaccia non sono (e non dovrebbero) essere dichiarate nel file di intestazione. Il file di intestazione pubblico dovrebbe essere tutto ciò che serve a chiunque per utilizzare il modulo.

Per lo sviluppo su larga scala, la compilazione e il collegamento possono richiedere molto tempo. Da molti minuti a molte ore (anche a molti giorni!). Dividere il software in interfacce (intestazioni) e implementazioni (fonti) fornisce un metodo per compilare solo i file che devono essere compilati piuttosto che ricostruire tutto.

Inoltre, i file di intestazione consentono a uno sviluppatore di fornire una libreria (già compilata) e un file di intestazione. Altri utenti della libreria potrebbero non vedere mai l'implementazione corretta, ma possono comunque utilizzare la libreria con il file di intestazione. Lo fai ogni giorno con la libreria standard C / C ++.

Anche se stai sviluppando una piccola applicazione, utilizzare tecniche di sviluppo software su larga scala è una buona abitudine. Ma dobbiamo anche ricordare perché usiamo queste abitudini.


0

Potresti anche chiederti perché non mettere tutto il tuo codice in un unico file.

La risposta più semplice è la manutenzione del codice.

Ci sono momenti in cui è ragionevole creare una classe:

  • tutto racchiuso in un'intestazione
  • tutto all'interno di un'unità di compilazione e nessuna intestazione.

Il momento in cui è ragionevole integrarsi completamente in un'intestazione è quando la classe è in realtà una struttura di dati con alcuni getter e setter di base e forse un costruttore che accetta valori per inizializzare i suoi membri.

(I modelli, che devono essere tutti in linea, sono un problema leggermente diverso).

L'altra volta per creare una classe tutta all'interno di un'intestazione è quando è possibile utilizzare la classe in più progetti e in particolare si desidera evitare il collegamento nelle librerie.

Un momento in cui potresti includere un'intera classe all'interno di un'unità di compilazione e non esporre affatto la sua intestazione è:

  • Una classe "impl" che viene utilizzata solo dalla classe che implementa. È un dettaglio di implementazione di quella classe e non viene utilizzato esternamente.

  • Un'implementazione di una classe base astratta creata da un tipo di metodo factory che restituisce un puntatore / riferimento / puntatore intelligente) alla classe base. Il metodo factory sarebbe esposto dalla classe stessa non lo sarebbe. (Inoltre, se la classe ha un'istanza che si registra su una tabella tramite un'istanza statica, non ha nemmeno bisogno di essere esposta tramite una factory).

  • Una classe di tipo "functor".

In altre parole, dove non vuoi che nessuno includa l'intestazione.

So cosa potresti pensare ... Se è solo per manutenibilità, includendo il file cpp (o un'intestazione totalmente incorporata) sei in grado di modificare facilmente un file per "trovare" il codice e ricostruirlo.

Tuttavia, la "manutenibilità" non significa solo che il codice sia ordinato. È una questione di impatti del cambiamento. È generalmente noto che se si mantiene invariata un'intestazione e si modifica un (.cpp)file di implementazione , non sarà necessario ricostruire l'altra fonte perché non dovrebbero esserci effetti collaterali.

Ciò rende "più sicuro" apportare tali cambiamenti senza preoccuparsi dell'effetto a catena e questo è ciò che realmente si intende per "manutenibilità".


-1

L'esempio di Snowman richiede una piccola estensione per mostrare esattamente perché sono necessari i file .h.

Aggiungi un'altra barra di classe al gioco che dipende anche dalla classe Foo.

Foo.h -> contiene una dichiarazione per la classe Foo

Foo.cpp -> contiene la definizione (impementazione) della classe Foo

Main.cpp -> utilizza una variabile del tipo Foo.

Bar.cpp -> usa anche variabili di tipo Foo.

Ora tutti i file cpp devono includere Foo.h Sarebbe un errore includere Foo.cpp in più di un altro file cpp. Il linker fallirebbe perché la classe Foo verrebbe definita più di una volta.


1
Neanche quello. Ciò potrebbe essere risolto esattamente nello stesso modo in cui viene risolto nei .hfile - con include guards ed è esattamente lo stesso problema che hai con i .hfile comunque. Non è per questo che .hservono i file.
Benjamin Gruenbaum,

Non sono sicuro di come utilizzeresti le protezioni perché funzionano solo per file (come fa il preprocessore). In questo caso, poiché Main.cpp e Bar.cpp sono file diversi, Foo.cpp verrebbe incluso solo una volta in entrambi (protetto o no, non fa alcuna differenza). Main.cpp e Bar.cpp sarebbero entrambi compilati. Ma l'errore di collegamento si verifica ancora a causa della doppia definizione della classe Foo. Tuttavia c'è anche un'eredità che non ho nemmeno menzionato. Dichiarazione per moduli esterni (DLL) ecc.
SkaarjFace

No, si tratterebbe di un'unità di compilazione, nessun collegamento avverrebbe affatto poiché includi il .cppfile.
Benjamin Gruenbaum,

Non ho detto che non verrà compilato. Ma non collegherà sia main.o che bar.o nello stesso eseguibile. Senza collegare a che cosa serve la compilazione.
SkaarjFace,

Uhh ... far funzionare il codice? Puoi ancora collegarlo, ma semplicemente non collegare i file tra loro - piuttosto sarebbero la stessa unità di compilazione.
Benjamin Gruenbaum,

-2

Se scrivi l'intero codice nello stesso file, il tuo codice sarà brutto.
In secondo luogo, non sarai in grado di condividere la tua lezione scritta con altri. Secondo l'ingegneria del software è necessario scrivere il codice client separatamente. Il client non dovrebbe sapere come funziona il tuo programma. Hanno solo bisogno di output Se si scrive l'intero programma nello stesso file, si perderà la sicurezza del programma.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.