Perché i file header e i file .cpp? [chiuso]


484

Perché C ++ ha file header e file .cpp?



è un paradigma OOP comune, .h è una dichiarazione di classe e cpp è la definizione. Uno non ha bisogno di sapere come viene implementato, dovrebbe conoscere solo l'interfaccia.
Manish Kakati,

Questa è la parte migliore dell'interfaccia di separazione c ++ dall'implementazione. Va sempre bene piuttosto che mantenere tutto il codice nel singolo file, abbiamo un'interfaccia separata. Una certa quantità di codice è sempre presente come la funzione inline che fa parte dei file di intestazione. Sembra buono quando viene visualizzato un file di intestazione visualizza l'elenco delle funzioni dichiarate e le variabili di classe.
Miank,

Ci sono momenti in cui i file di intestazione sono essenziali per la compilazione, non solo una preferenza dell'organizzazione o un modo per distribuire librerie precompilate. Supponi di avere una struttura in cui game.c dipende da ENTRAMBE Physics.c e math.c; physics.c dipende anche da math.c. Se includessi i file .c e ti dimenticassi dei file .h per sempre, avresti dichiarazioni duplicate da math.c e nessuna speranza di compilazione. Questo è ciò che ha più senso per me perché i file di intestazione sono importanti. Spero che aiuti qualcun altro.
Samy Bencherif,

Penso che abbia a che fare con il fatto che nelle estensioni sono ammessi solo caratteri alfanumerici. Non so nemmeno se sia vero, solo indovinando
user12211554

Risposte:


202

Bene, la ragione principale sarebbe quella di separare l'interfaccia dall'implementazione. L'intestazione dichiara "cosa" farà una classe (o qualunque cosa venga implementata), mentre il file cpp definisce "come" eseguirà tali funzionalità.

Ciò riduce le dipendenze in modo che il codice che utilizza l'intestazione non debba necessariamente conoscere tutti i dettagli dell'implementazione e qualsiasi altra classe / intestazione necessaria solo per quello. Ciò ridurrà i tempi di compilazione e anche la quantità di ricompilazione necessaria quando cambia qualcosa nell'implementazione.

Non è perfetto e di solito ricorrere a tecniche come Idiom Pimpl per separare correttamente interfaccia e implementazione, ma è un buon inizio.


178
Non proprio vero L'intestazione contiene ancora una parte importante dell'implementazione. Da quando le variabili di istanza privata facevano parte dell'interfaccia di una classe? Funzioni membro privato? Allora cosa diavolo stanno facendo nell'intestazione pubblicamente visibile? E cade ulteriormente a pezzi con i modelli.
jalf

13
Ecco perché ho detto che non è perfetto, e il linguaggio Pimpl è necessario per una maggiore separazione. I modelli sono una lattina completamente diversa di worm - anche se la parola chiave "export" fosse pienamente supportata nella maggior parte dei compilatori, mi farebbe comunque zucchero sintattico piuttosto che una vera separazione.
Joris Timmermans,

4
Come lo gestiscono altre lingue? per esempio - Java? Non esiste un concetto di file di intestazione in Java.
Lazer,

8
@Lazer: Java è più semplice da analizzare. Il compilatore Java può analizzare un file senza conoscere tutte le classi in altri file e controllare i tipi in un secondo momento. In C ++ molti costrutti sono ambigui senza informazioni sul tipo, quindi il compilatore C ++ necessita di informazioni sui tipi di riferimento per analizzare un file. Ecco perché ha bisogno di intestazioni.
Niki,

15
@nikie: che cosa c'entra la "facilità" di analisi? Se Java avesse una grammatica almeno complessa come C ++, potrebbe ancora usare solo i file java. In entrambi i casi, che dire di C? C è facile da analizzare, ma utilizza sia le intestazioni che i file c.
Thomas Eding,

609

Compilazione C ++

Una compilazione in C ++ viene eseguita in 2 fasi principali:

  1. La prima è la compilazione di file di testo "sorgente" in file "oggetto" binari: il file CPP è il file compilato e viene compilato senza alcuna conoscenza degli altri file CPP (o persino delle librerie), a meno che non venga fornito tramite una dichiarazione non elaborata o inclusione dell'intestazione. Il file CPP viene generalmente compilato in un file "oggetto" .OBJ o .O.

  2. Il secondo è il collegamento di tutti i file "oggetto" e, quindi, la creazione del file binario finale (una libreria o un eseguibile).

Dove si colloca l'HPP in tutto questo processo?

Un povero file CPP solitario ...

La compilazione di ciascun file CPP è indipendente da tutti gli altri file CPP, il che significa che se A.CPP necessita di un simbolo definito in B.CPP, come:

// A.CPP
void doSomething()
{
   doSomethingElse(); // Defined in B.CPP
}

// B.CPP
void doSomethingElse()
{
   // Etc.
}

Non verrà compilato perché A.CPP non ha modo di sapere che esiste "doSomethingElse" ... A meno che non ci sia una dichiarazione in A.CPP, come:

// A.CPP
void doSomethingElse() ; // From B.CPP

void doSomething()
{
   doSomethingElse() ; // Defined in B.CPP
}

Quindi, se hai C.CPP che utilizza lo stesso simbolo, copia / incolla la dichiarazione ...

COPIA / AVVISO PASTA!

Sì, c'è un problema. Le copie / paste sono pericolose e difficili da mantenere. Ciò significa che sarebbe bello se non avessimo il modo di NON copiare / incollare e dichiarare ancora il simbolo ... Come possiamo farlo? Con l'inclusione di alcuni file di testo, comunemente suffissi da .h, .hxx, .h ++ o, il mio preferito per i file C ++, .hpp:

// B.HPP (here, we decided to declare every symbol defined in B.CPP)
void doSomethingElse() ;

// A.CPP
#include "B.HPP"

void doSomething()
{
   doSomethingElse() ; // Defined in B.CPP
}

// B.CPP
#include "B.HPP"

void doSomethingElse()
{
   // Etc.
}

// C.CPP
#include "B.HPP"

void doSomethingAgain()
{
   doSomethingElse() ; // Defined in B.CPP
}

Come includefunziona?

L'inclusione di un file, in sostanza, analizza e quindi copia e incolla il suo contenuto nel file CPP.

Ad esempio, nel codice seguente, con l'intestazione A.HPP:

// A.HPP
void someFunction();
void someOtherFunction();

... la fonte B.CPP:

// B.CPP
#include "A.HPP"

void doSomething()
{
   // Etc.
}

... diventerà dopo l'inclusione:

// B.CPP
void someFunction();
void someOtherFunction();

void doSomething()
{
   // Etc.
}

Una piccola cosa: perché includere B.HPP in B.CPP?

Nel caso attuale, ciò non è necessario e B.HPP ha la doSomethingElsedichiarazione di funzione e B.CPP ha la doSomethingElsedefinizione di funzione (che è, di per sé, una dichiarazione). Ma in un caso più generale, in cui B.HPP viene utilizzato per le dichiarazioni (e il codice inline), potrebbe non esserci una definizione corrispondente (ad esempio enumerazioni, strutture semplici, ecc.), Quindi l'inclusione potrebbe essere necessaria se B.CPP utilizza tali dichiarazioni di B.HPP. Tutto sommato, è "buon gusto" per una fonte includere di default la sua intestazione.

Conclusione

Il file di intestazione è quindi necessario, poiché il compilatore C ++ non è in grado di cercare da soli le dichiarazioni di simboli e, pertanto, è necessario aiutarlo includendo tali dichiarazioni.

Un'ultima parola: dovresti mettere le protezioni delle intestazioni attorno al contenuto dei tuoi file HPP, per essere sicuro che inclusioni multiple non rompano nulla, ma tutto sommato, credo che il motivo principale dell'esistenza dei file HPP sia spiegato sopra.

#ifndef B_HPP_
#define B_HPP_

// The declarations in the B.hpp file

#endif // B_HPP_

o anche più semplice

#pragma once

// The declarations in the B.hpp file

2
@nimcap:: You still have to copy paste the signature from header file to cpp file, don't you?nessuna necessità. Finché il CPP "include" l'HPP, il precompilatore eseguirà automaticamente il copia-incolla del contenuto del file HPP nel file CPP. Ho aggiornato la risposta per chiarirlo.
Paercebal,

7
@Bob: While compiling A.cpp, compiler knows the types of arguments and return value of doSomethingElse from the call itself. No, non lo fa. Conosce solo i tipi forniti dall'utente, che per metà del tempo non si preoccuperanno nemmeno di leggere il valore restituito. Quindi, si verificano conversioni implicite. E poi, quando hai il codice foo(bar):, non puoi nemmeno essere sicuro che foosia una funzione. Quindi il compilatore deve avere accesso alle informazioni nelle intestazioni per decidere se l'origine viene compilata correttamente o no ... Quindi, una volta compilato il codice, il linker collegherà semplicemente le chiamate delle funzioni.
Paercebal,

3
@Bob: [continua] ... Ora, il linker potrebbe fare il lavoro svolto dal compilatore, immagino, che renderebbe quindi possibile la tua opzione. (Immagino che questo sia l'oggetto della proposta "moduli" per il prossimo standard). Seems, they're just a pretty ugly arbitrary design.: Se C ++ fosse stato creato nel 2012, anzi. Ma ricorda che il C ++ fu costruito su C negli anni '80 e, a quel tempo, i vincoli erano piuttosto diversi a quel tempo (IIRC, fu deciso ai fini dell'adozione di mantenere gli stessi linker di quelli di C).
Paercebal,

1
@paercebal Grazie per la spiegazione e le note, Paercebal! Perché non posso essere sicuro, questa foo(bar)è una funzione - se viene ottenuta come puntatore? In effetti, parlando di cattivo design, biasimo C, non C ++. Non mi piacciono davvero alcuni vincoli della C pura, come avere file header o avere funzioni che restituiscono un solo valore, pur prendendo più argomenti sull'input (non sembra naturale che input e output si comportino in modo simile ; perché argomenti multipli, ma uscita singola?) :)
Boris Burkov

1
@ Bobo: Why can't I be sure, that foo(bar) is a functionfoo potrebbe essere un tipo, quindi si chiamerebbe un costruttore di classi. In fact, speaking of bad design, I blame C, not C++: Posso dare la colpa a C per molte cose, ma essere stato progettato negli anni '70 non sarà uno di questi. Ancora una volta, i vincoli di quel tempo ... such as having header files or having functions return one and only one value: le tuple possono aiutare a mitigarlo, oltre a passare argomenti per riferimento. Ora, quale sarebbe la sintassi per recuperare più valori restituiti e varrebbe la pena cambiare la lingua?
paercebal,

93

Perché C, dove ha avuto origine il concetto, ha 30 anni e all'epoca era l'unico modo possibile per collegare codice da più file.

Oggi è un terribile hack che distrugge totalmente i tempi di compilazione in C ++, provoca innumerevoli dipendenze inutili (perché le definizioni di classe in un file di intestazione espongono troppe informazioni sull'implementazione) e così via.


3
Mi chiedo perché i file header (o qualunque cosa fosse effettivamente necessaria per la compilazione / collegamento) non fossero semplicemente "auto-generati"?
Mateen Ulhaq,

54

Perché in C ++, il codice eseguibile finale non contiene alcuna informazione sul simbolo, è un codice macchina più o meno puro.

Pertanto, è necessario un modo per descrivere l'interfaccia di un pezzo di codice, che è separato dal codice stesso. Questa descrizione si trova nel file di intestazione.


16

Perché C ++ li ha ereditati da C. Sfortunatamente.


4
Perché l'eredità di C ++ da C è sfortunata?
Lokesh,

3
@Lokesh A causa del suo bagaglio :(
陳 力

1
Come può essere una risposta?
Shuvo Sarker,

14
@ShuvoSarker perché, come hanno dimostrato migliaia di lingue, non esiste alcuna spiegazione tecnica per C ++ che induca i programmatori a scrivere due volte le firme delle funzioni. La risposta a "perché?" è "storia".
Boris,

15

Perché le persone che hanno progettato il formato della libreria non volevano "sprecare" spazio per informazioni utilizzate raramente come le macro del preprocessore C e le dichiarazioni di funzione.

Dato che hai bisogno di quelle informazioni per dire al tuo compilatore "questa funzione è disponibile in seguito quando il linker sta facendo il suo lavoro", hanno dovuto trovare un secondo file in cui queste informazioni condivise potevano essere archiviate.

La maggior parte delle lingue dopo C / C ++ memorizza queste informazioni nell'output (bytecode Java, ad esempio) o non usano affatto un formato precompilato, vengono sempre distribuite in formato sorgente e compilate al volo (Python, Perl).


Non funzionerebbe, riferimenti ciclici. Ieyou non può costruire a.lib da a.cpp prima di costruire b.lib da b.cpp, ma non è possibile nemmeno costruire b.lib prima di a.lib.
MSalters,

20
Java ha risolto che, Python può farlo, qualsiasi linguaggio moderno può farlo. Ma al momento dell'invenzione di C, la RAM era così costosa e scarsa, non era un'opzione.
Aaron Digulla,

6

È il modo preprocessore di dichiarare le interfacce. Inserisci l'interfaccia (dichiarazioni del metodo) nel file di intestazione e l'implementazione nel cpp. Le applicazioni che usano la tua libreria devono solo conoscere l'interfaccia, a cui possono accedere tramite #include.


4

Spesso vorrai avere una definizione di un'interfaccia senza dover spedire l'intero codice. Ad esempio, se si dispone di una libreria condivisa, si spedirebbe con sé un file di intestazione che definisce tutte le funzioni e i simboli utilizzati nella libreria condivisa. Senza i file di intestazione, dovresti spedire la fonte.

All'interno di un singolo progetto, vengono utilizzati i file di intestazione, IMHO, per almeno due scopi:

  • Chiarezza, cioè mantenendo le interfacce separate dall'implementazione, è più facile leggere il codice
  • Tempo di compilazione. Utilizzando solo l'interfaccia laddove possibile, anziché l'implementazione completa, il tempo di compilazione può essere ridotto perché il compilatore può semplicemente fare un riferimento all'interfaccia invece di dover analizzare il codice effettivo (che, in teoria, dovrebbe solo essere fatto una sola volta).

3
Perché i venditori di biblioteche non potevano semplicemente spedire un file "header" generato? Un file "header" libero da pre-processore dovrebbe fornire prestazioni molto migliori (a meno che l'implementazione non sia stata realmente interrotta).
Tom Hawtin - tackline il

Penso che sia irrilevante se il file di intestazione viene generato o scritto a mano, la domanda non era "perché le persone scrivono i file di intestazione da soli?", Era "perché abbiamo i file di intestazione". Lo stesso vale per le intestazioni prive di preprocessore. Certo, questo sarebbe più veloce.

-5

Rispondendo alla risposta di MadKeithV ,

Ciò riduce le dipendenze in modo che il codice che utilizza l'intestazione non debba necessariamente conoscere tutti i dettagli dell'implementazione e qualsiasi altra classe / intestazione necessaria solo per quello. Ciò ridurrà i tempi di compilazione e anche la quantità di ricompilazione necessaria quando cambia qualcosa nell'implementazione.

Un altro motivo è che un'intestazione fornisce un ID univoco per ogni classe.

Quindi se abbiamo qualcosa del genere

class A {..};
class B : public A {...};

class C {
    include A.cpp;
    include B.cpp;
    .....
};

Avremo errori, quando proveremo a costruire il progetto, poiché A fa parte di B, con le intestazioni eviteremmo questo tipo di mal di testa ...

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.