Perché C ++ necessita di un file di intestazione separato?


138

Non ho mai veramente capito perché C ++ abbia bisogno di un file header separato con le stesse funzioni del file .cpp. Rende molto difficile la creazione di classi e il loro refactoring e aggiunge file non necessari al progetto. E poi c'è il problema di dover includere i file header, ma di controllare esplicitamente se è già stato incluso.

Il C ++ è stato ratificato nel 1998, quindi perché è stato progettato in questo modo? Quali sono i vantaggi di avere un file header separato?


Domanda di follow-up:

In che modo il compilatore trova il file .cpp con il codice al suo interno, quando tutto ciò che includo è il file .h? Presuppone che il file .cpp abbia lo stesso nome del file .h o cerca effettivamente tutti i file nella struttura di directory?


2
Se vuoi modificare un singolo file, controlla solo lzz (www.lazycplusplus.com).
Richard Corden,

Risposte:


105

Sembra che tu stia chiedendo di separare le definizioni dalle dichiarazioni, sebbene ci siano altri usi per i file di intestazione.

La risposta è che C ++ non "ha bisogno" di questo. Se si contrassegna tutto in linea (che è comunque automatico per le funzioni membro definite in una definizione di classe), non è necessaria la separazione. Puoi semplicemente definire tutto nei file di intestazione.

I motivi che potresti voler separare sono:

  1. Per migliorare i tempi di costruzione.
  2. Per collegarsi al codice senza avere l'origine per le definizioni.
  3. Per evitare di contrassegnare tutto "in linea".

Se la tua domanda più generale è "perché C ++ non è identico a Java?", Allora devo chiederti "perché stai scrivendo C ++ anziché Java?" ;-P

Più seriamente, tuttavia, la ragione è che il compilatore C ++ non può semplicemente entrare in un'altra unità di traduzione e capire come usare i suoi simboli, nel modo in cui javac può e fa. Il file di intestazione è necessario per dichiarare al compilatore cosa può aspettarsi di essere disponibile al momento del collegamento.

Quindi #includeè una semplice sostituzione testuale. Se si definisce tutto nei file di intestazione, il preprocessore finisce per creare un'enorme copia e incolla di ogni file di origine nel progetto e inserirlo nel compilatore. Il fatto che lo standard C ++ sia stato ratificato nel 1998 non ha nulla a che fare con questo, è il fatto che l'ambiente di compilazione per C ++ si basa così strettamente su quello di C.

Conversione dei miei commenti per rispondere alla domanda di follow-up:

In che modo il compilatore trova il file .cpp con il codice al suo interno

Non, almeno non nel momento in cui compila il codice che ha utilizzato il file di intestazione. Le funzioni a cui stai collegando non hanno nemmeno bisogno di essere state ancora scritte, non importa che il compilatore sappia in quale .cppfile si troveranno. Tutto ciò che il codice chiamante deve sapere al momento della compilazione è espresso nella dichiarazione di funzione. Al momento del collegamento fornirai un elenco di .ofile o librerie statiche o dinamiche e l'intestazione in effetti promette che le definizioni delle funzioni saranno presenti da qualche parte.


3
Per aggiungere a "I motivi che potresti voler separare sono:" E penso che la funzione più importante dei file di intestazione sia: Separare il design della struttura del codice dall'implementazione, Perché: A. Quando entri in strutture davvero complicate che coinvolgono molti oggetti esso è molto più facile setacciare i file di intestazione e ricordare come funzionano insieme, integrati dai commenti dell'intestazione. B. Una persona non si è occupata di definire tutta la struttura degli oggetti e qualcun altro si sta occupando dell'implementazione che mantiene le cose organizzate. Nel complesso penso che renda più leggibile il codice complesso.
Andres Canella,

In un modo più semplice posso pensare all'utilità della separazione dei file header vs cpp è quella di separare Interface vs. Implements che aiuta veramente per progetti medio / grandi.
Krishna Oza,

Volevo che fosse incluso in (2), "link contro codice senza avere le definizioni". OK, quindi potresti avere ancora accesso al repository git con le definizioni in, ma il punto è che puoi compilare il tuo codice separatamente dalle implementazioni delle sue dipendenze e quindi collegare. Se letteralmente tutto ciò che volevi era separare l'interfaccia / implementazione in diversi file, senza preoccuparti della costruzione separata, allora potresti farlo semplicemente avendo foo_interface.h includendo foo_implementation.h.
Steve Jessop,

4
@AndresCanella No, non lo è. Rende la lettura e il mantenimento del codice non proprio un incubo. Per comprendere appieno cosa fa qualcosa nel codice è necessario passare attraverso 2n file anziché n file. Questa non è una notazione Big-Oh, 2n fa molta differenza rispetto a solo n.
Błażej Michalik,

1
Secondo, è una bugia che aiuta le intestazioni. controlla la fonte minix per esempio, è così difficile seguire dove inizia dove passa il controllo, dove le cose sono dichiarate / definite .. se fosse costruito attraverso moduli dinamici separati, sarebbe digeribile dando un senso a una cosa e poi saltando a un modulo di dipendenza. invece, è necessario seguire le intestazioni e rende l'inferno la lettura di qualsiasi codice scritto in questo modo. al contrario, nodejs chiarisce da dove viene senza ifdefs e puoi facilmente identificare da dove proviene.
Dmitry

91

C ++ fa così perché C lo ha fatto in quel modo, quindi la vera domanda è perché C l'ha fatto in quel modo? Wikipedia parla un po 'di questo.

I linguaggi compilati più recenti (come Java, C #) non usano dichiarazioni a termine; gli identificatori vengono riconosciuti automaticamente dai file di origine e letti direttamente dai simboli della libreria dinamica. Ciò significa che i file di intestazione non sono necessari.


13
+1 Colpisce l'unghia sulla testa. Questo in realtà non richiede una spiegazione dettagliata.
MSalters,

6
Non mi ha colpito alla testa :( Devo ancora cercare il motivo per cui C ++ deve utilizzare le dichiarazioni in avanti e perché non è in grado di riconoscere gli identificatori dai file sorgente e leggere direttamente dai simboli della libreria dinamica, e perché C ++ lo ha fatto solo perché C ha fatto così: p
Alexander Taylor

3
E sei un programmatore migliore per averlo fatto @AlexanderTaylor :)
Donald Byrd

66

Alcune persone considerano i file di intestazione un vantaggio:

  • Si afferma che abilita / applica / consente la separazione dell'interfaccia e l'implementazione, ma di solito non è così. I file di intestazione sono pieni di dettagli di implementazione (ad esempio, le variabili membro di una classe devono essere specificate nell'intestazione, anche se non fanno parte dell'interfaccia pubblica) e le funzioni possono, e spesso sono, definite in linea nella dichiarazione della classe nell'intestazione, distruggendo di nuovo questa separazione.
  • Talvolta si dice che migliora il tempo di compilazione perché ogni unità di traduzione può essere elaborata in modo indipendente. Eppure il C ++ è probabilmente il linguaggio più lento esistente quando si tratta di tempi di compilazione. Una parte del motivo sono le molte molte inclusioni ripetute della stessa intestazione. Un gran numero di intestazioni sono incluse da più unità di traduzione, richiedendo che vengano analizzate più volte.

In definitiva, il sistema di intestazione è un artefatto degli anni '70 quando fu progettato C. Allora, i computer avevano pochissima memoria e mantenere l'intero modulo in memoria non era un'opzione. Un compilatore doveva iniziare a leggere il file in alto, quindi procedere in modo lineare attraverso il codice sorgente. Il meccanismo dell'intestazione consente questo. Il compilatore non deve considerare altre unità di traduzione, deve solo leggere il codice dall'alto verso il basso.

E C ++ ha mantenuto questo sistema per compatibilità con le versioni precedenti.

Oggi non ha senso. È inefficiente, soggetto a errori e eccessivamente complicato. Ci sono modi molto migliori per separare l'interfaccia e l'implementazione, se quello era l'obiettivo.

Tuttavia, una delle proposte per C ++ 0x era quella di aggiungere un sistema di moduli adeguato, che consentisse la compilazione di codice simile a .NET o Java, in moduli più grandi, tutto in una volta e senza intestazioni. Questa proposta non ha fatto il taglio in C ++ 0x, ma credo che sia ancora nella categoria "ci piacerebbe farlo più tardi". Forse in un TR2 o simile.


QUESTA è la migliore risposta sulla pagina. Grazie!
Chuck Le Butt,

29

Per la mia comprensione (limitata - normalmente non sono uno sviluppatore C), questo è radicato in C. Ricorda che C non sa cosa siano le classi o gli spazi dei nomi, è solo un lungo programma. Inoltre, le funzioni devono essere dichiarate prima di utilizzarle.

Ad esempio, quanto segue dovrebbe dare un errore del compilatore:

void SomeFunction() {
    SomeOtherFunction();
}

void SomeOtherFunction() {
    printf("What?");
}

L'errore dovrebbe essere che "SomeOtherFunction non è dichiarato" perché lo si chiama prima della sua dichiarazione. Un modo per risolvere questo problema è spostando SomeOtherFunction sopra SomeFunction. Un altro approccio è quello di dichiarare prima la firma delle funzioni:

void SomeOtherFunction();

void SomeFunction() {
    SomeOtherFunction();
}

void SomeOtherFunction() {
    printf("What?");
}

Questo fa sapere al compilatore: guarda da qualche parte nel codice, c'è una funzione chiamata SomeOtherFunction che restituisce void e non accetta alcun parametro. Quindi, se trovi codice che tenta di chiamare SomeOtherFunction, non farti prendere dal panico e vai a cercarlo.

Ora, immagina di avere SomeFunction e SomeOtherFunction in due diversi file .c. Devi quindi #includere "SomeOther.c" in Some.c. Ora, aggiungi alcune funzioni "private" a SomeOther.c. Poiché C non conosce funzioni private, tale funzione sarebbe disponibile anche in Some.c.

È qui che arrivano i file .h: specificano tutte le funzioni (e variabili) che si desidera "esportare" da un file .c a cui è possibile accedere in altri file .c. In questo modo, ottieni qualcosa come un ambito pubblico / privato. Inoltre, puoi dare questo file .h ad altre persone senza dover condividere il tuo codice sorgente - i file .h funzionano anche con i file .lib compilati.

Quindi il motivo principale è davvero per comodità, per la protezione del codice sorgente e per avere un po 'di disaccoppiamento tra le parti dell'applicazione.

Quello era C però. Il C ++ ha introdotto Classi e modificatori privati ​​/ pubblici, quindi sebbene si possa ancora chiedere se sono necessari, C ++ AFAIK richiede comunque la dichiarazione delle funzioni prima di utilizzarle. Inoltre, molti sviluppatori C ++ sono o erano anche devoti C e hanno assunto i loro concetti e abitudini in C ++ - perché cambiare ciò che non è rotto?


5
Perché il compilatore non può scorrere il codice e trovare tutte le definizioni delle funzioni? Sembra qualcosa che sarebbe abbastanza facile da programmare nel compilatore.
Marius,

3
Se avete la fonte, che spesso non hanno. Il C ++ compilato è effettivamente un codice macchina con informazioni sufficienti per caricare e collegare il codice. Quindi, si punta la CPU sul punto di ingresso e si lascia correre. Ciò è fondamentalmente diverso da Java o C #, in cui il codice viene compilato in un bytecode intermedio contenente metadati sul suo contenuto.
DevSolar,

Immagino che nel 1972 avrebbe potuto essere un'operazione piuttosto costosa per il compilatore.
Michael Stum

Esattamente quello che ha detto Michael Stum. Quando è stato definito questo comportamento, una scansione lineare attraverso una singola unità di traduzione era l'unica cosa che poteva essere praticamente implementata.
jalf

3
Sì, la compilazione su un bitter a 16 bit con un serraggio di massa del nastro non è banale.
MSalters,

11

Primo vantaggio: se non si dispone di file di intestazione, è necessario includere file di origine in altri file di origine. Ciò causerebbe la compilazione dei file inclusi quando il file incluso viene modificato.

Secondo vantaggio: consente di condividere le interfacce senza condividere il codice tra diverse unità (diversi sviluppatori, team, aziende ecc.)


1
Stai insinuando che, ad esempio in C # "dovresti includere i file sorgente in altri file sorgente"? Perché ovviamente no. Per il secondo vantaggio, penso che dipenda troppo dalla lingua: non userete i file .h ad es. Delphi
Vlagged

Devi comunque ricompilare l'intero progetto, quindi il primo vantaggio conta davvero?
Marius,

ok, ma non credo che una funzionalità linguistica. È più pratico affrontare la dichiarazione C prima della definizione di "problema". È come se qualcuno di famoso avesse detto "non è un bug, è una caratteristica" :)
neuro

@Marius: Sì, conta davvero. Il collegamento dell'intero progetto è diverso dalla compilazione e collegamento dell'intero progetto. Man mano che il numero di file nel progetto aumenta, la loro compilazione diventa davvero fastidiosa. @Vlagged: hai ragione, ma non ho confrontato c ++ con un'altra lingua. Ho confrontato usando solo i file sorgente e usando i file sorgente e di intestazione.
erelender

C # non include i file di origine in altri, ma è comunque necessario fare riferimento ai moduli - e questo fa sì che il compilatore recuperi i file di origine (o rifletta nel file binario) per analizzare i simboli utilizzati dal codice.
gbjbaanb,

5

La necessità di file di intestazione deriva dalle limitazioni che il compilatore ha per conoscere le informazioni sul tipo di funzioni e / o variabili in altri moduli. Il programma o la libreria compilati non include le informazioni sul tipo richieste dal compilatore per associarsi a qualsiasi oggetto definito in altre unità di compilazione.

Al fine di compensare questa limitazione, C e C ++ consentono dichiarazioni e queste dichiarazioni possono essere incluse nei moduli che le usano con l'aiuto della direttiva #include del preprocessore.

Lingue come Java o C # d'altra parte includono le informazioni necessarie per l'associazione nell'output del compilatore (file di classe o assembly). Pertanto, non è più necessario mantenere dichiarazioni autonome che devono essere incluse dai client di un modulo.

Il motivo per cui le informazioni di associazione non vengono incluse nell'output del compilatore è semplice: non è necessario in fase di esecuzione (qualsiasi controllo del tipo si verifica in fase di compilazione). Sprecherebbe solo spazio. Ricorda che C / C ++ proviene da un'epoca in cui le dimensioni di un eseguibile o di una libreria erano abbastanza importanti.


Sono d'accordo con te. Ho avuto un'idea simile qui: stackoverflow.com/questions/3702132/...
smwikipedia

4

C ++ è stato progettato per aggiungere moderne funzionalità del linguaggio di programmazione all'infrastruttura C, senza modificare inutilmente nulla di C che non riguardasse specificamente il linguaggio stesso.

Sì, a questo punto (10 anni dopo il primo standard C ++ e 20 anni dopo che ha iniziato a crescere seriamente nell'uso) è facile chiedersi perché non ha un sistema di moduli adeguato. Ovviamente qualsiasi nuovo linguaggio progettato oggi non funzionerebbe come C ++. Ma non è questo il punto del C ++.

Il punto del C ++ è di essere evolutivo, una continua continuazione della pratica esistente, solo aggiungendo nuove capacità senza (troppo spesso) rompere le cose che funzionano adeguatamente per la sua comunità di utenti.

Ciò significa che rende alcune cose più difficili (specialmente per le persone che iniziano un nuovo progetto) e alcune cose più facili (specialmente per coloro che mantengono il codice esistente) rispetto ad altre lingue.

Quindi piuttosto che aspettarsi che C ++ si trasformi in C # (che sarebbe inutile dato che abbiamo già C #), perché non scegliere semplicemente lo strumento giusto per il lavoro? Io stesso, mi sforzo di scrivere blocchi significativi di nuove funzionalità in un linguaggio moderno (mi capita di usare C #), e ho una grande quantità di C ++ esistente che sto conservando in C ++ perché non ci sarebbe alcun valore reale nel riscriverlo tutti. Si integrano comunque molto bene, quindi è in gran parte indolore.


Come si integrano C # e C ++? Tramite COM?
Peter Mortensen,

1
Esistono tre modi principali, il "migliore" dipende dal codice esistente. Ho usato tutti e tre. Quello che uso di più è COM perché il mio codice esistente era già stato progettato attorno ad esso, quindi è praticamente senza soluzione di continuità, funziona molto bene per me. In alcuni posti strani uso C ++ / CLI che offre un'integrazione incredibilmente fluida per qualsiasi situazione in cui non hai già interfacce COM (e potresti preferire l'utilizzo di interfacce COM esistenti anche se ce l'hai). Infine c'è p / invoke che sostanzialmente ti consente di chiamare qualsiasi funzione simile a C esposta da una DLL, quindi ti consente di chiamare direttamente qualsiasi API Win32 da C #.
Daniel Earwicker,

4

Bene, il C ++ è stato ratificato nel 1998, ma era stato utilizzato per molto più tempo di quello, e la ratifica stava principalmente stabilendo l'uso corrente piuttosto che imporre la struttura. E poiché C ++ era basato su C e C ha file di intestazione, anche C ++ li ha.

Il motivo principale dei file di intestazione è abilitare la compilazione separata dei file e ridurre al minimo le dipendenze.

Supponiamo di avere foo.cpp e di voler utilizzare il codice dai file bar.h / bar.cpp.

Posso #includere "bar.h" in foo.cpp, quindi programmare e compilare foo.cpp anche se bar.cpp non esiste. Il file di intestazione funge da promessa per il compilatore che le classi / funzioni in bar.h esisteranno in fase di esecuzione e ha già tutto ciò che deve sapere.

Naturalmente, se le funzioni in bar.h non hanno corpi quando provo a collegare il mio programma, allora non si collegherà e visualizzerò un errore.

Un effetto collaterale è che puoi fornire agli utenti un file di intestazione senza rivelare il tuo codice sorgente.

Un altro è che se modifichi l'implementazione del tuo codice nel file * .cpp, ma non cambi affatto l'intestazione, devi solo compilare il file * .cpp invece di tutto ciò che lo utilizza. Naturalmente, se metti molta implementazione nel file di intestazione, questo diventa meno utile.


3

Non ha bisogno di un file di intestazione separato con le stesse funzioni di main. Ne ha bisogno solo se si sviluppa un'applicazione utilizzando più file di codice e se si utilizza una funzione non precedentemente dichiarata.

È davvero un problema di portata.


1

Il C ++ è stato ratificato nel 1998, quindi perché è stato progettato in questo modo? Quali sono i vantaggi di avere un file header separato?

In realtà i file di intestazione diventano molto utili quando si esaminano i programmi per la prima volta, il controllo dei file di intestazione (utilizzando solo un editor di testo) offre una panoramica dell'architettura del programma, a differenza di altre lingue in cui è necessario utilizzare strumenti sofisticati per visualizzare le classi e le loro funzioni membro.


1

Penso che la vera (historical) motivo dietro file di intestazione come stava facendo più facile per gli sviluppatori del compilatore ... ma poi, i file di intestazione ne frega vantaggi.
Controlla questo post precedente per ulteriori discussioni ...


1

Bene, puoi sviluppare perfettamente C ++ senza file header. In effetti alcune librerie che usano intensamente i template non usano il paradigma dei file header / code (vedi boost). Ma in C / C ++ non puoi usare qualcosa che non è dichiarato. Un modo pratico per gestirlo è usare i file header. Inoltre, ottieni il vantaggio di condividere l'interfaccia senza condividere codice / implementazione. E penso che non sia stato immaginato dai creatori di C: quando usi file di intestazione condivisi devi usare il famoso:

#ifndef MY_HEADER_SWEET_GUARDIAN
#define MY_HEADER_SWEET_GUARDIAN

// [...]
// my header
// [...]

#endif // MY_HEADER_SWEET_GUARDIAN

questa non è in realtà una caratteristica della lingua ma un modo pratico per gestire l'inclusione multipla.

Quindi, penso che quando è stato creato il C, i problemi con la dichiarazione in avanti sono stati sottovalutati e ora quando si utilizza un linguaggio di alto livello come il C ++, dobbiamo affrontare questo tipo di cose.

Un altro onere per noi poveri utenti C ++ ...


1

Se si desidera che il compilatore scopra automaticamente i simboli definiti in altri file, è necessario forzare il programmatore a posizionare quei file in posizioni predefinite (come la struttura dei pacchetti Java determina la struttura delle cartelle del progetto). Preferisco i file di intestazione. Inoltre avresti bisogno di fonti di librerie che usi o di un modo uniforme per mettere le informazioni necessarie al compilatore in binari.

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.