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

17

Perché dobbiamo includere sia i file .h che .cpp , mentre possiamo farlo funzionare con solo un file .cpp e quindi includerlo.

Ad esempio, creando un file.h contenente dichiarazioni, quindi creando un file.cpp contenente definizioni e includendo entrambi su main.cpp .

Oppure, creando un file.cpp contenente dichiarazioni / definizioni (nessun prototipo) Inserendolo in main.cpp .

Entrambi hanno funzionato per me, ma non possono fare la differenza poiché non ho uno sfondo sul processo di compilazione e collegamento.

    
posta reaffer 23.08.2014 - 18:47
fonte

7 risposte

25

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.

    
risposta data 23.08.2014 - 20:55
fonte
8

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

Probabilmente dovrai sempre includere il file di intestazione file.h se contiene (come dettato da convenzioni e abitudini ):

  • definizioni macro
  • definizioni dei tipi (ad esempio typedef , struct , class etc, ...)
  • definizioni di static inline funzioni
  • dichiarazioni di funzioni esterne.

Si noti che è una questione di convenzioni (e di praticità) inserirli in un file di intestazione.

Ovviamente, la tua implementazione file.cpp ha bisogno di tutto quanto sopra, quindi all'inizio 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 (ovvero unità di traduzione). Ma non lo vuoi (tranne forse se il tuo codice C o C ++ viene generato automaticamente, quindi potresti fare il programma generatore facendo quella copia e incolla, imitando il ruolo del preprocessore).

Il punto è che il preprocessore sta facendo solo operazioni testuali . Potresti (in linea di principio) evitarlo interamente da copia & incolla o sostituiscilo con un altro "preprocessore" o generatore di codice C (come gpp o m4 ).

Un altro problema è che i recenti standard C (o C ++) definiscono diverse intestazioni standard. La maggior parte delle implementazioni implementa realmente queste intestazioni standard come (file di implementazione specifici) file , ma credo che sarebbe possibile per un'implementazione conforme implementare standard inclusi (come #include <stdio.h> per C, o #include <vector> per C ++) con alcuni trucchi magici (es. Usando qualche database o qualche informazione dentro il compilatore).

Se utilizzi i compilatori GCC (ad esempio gcc o g++ ) puoi utilizzare il -H flag per essere informato su ogni inclusione e il -C -E contrassegna per ottenere il modulo preelaborato. Naturalmente ci sono molti altri flag del compilatore che influenzano la pre-elaborazione (ad esempio -I /some/dir/ per aggiungere /some/dir/ per la ricerca di file inclusi e -D per predefinire alcune macro di preprocessore, ecc. Ecc.).

    
risposta data 23.08.2014 - 18:55
fonte
2

A causa del modello di build a più unità del 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 questo nasce l'idioma dell'intestazione C ++. È convenzione per un motivo.

puoi scaricare l'intero programma in una singola unità di traduzione, ma questo introduce problemi con il riutilizzo del codice, il test dell'unità e la gestione delle dipendenze tra moduli. È anche solo un gran casino.

    
risposta data 24.08.2014 - 00:47
fonte
0

La risposta selezionata da Perché abbiamo bisogno di 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 fornisce una soluzione a due problemi di sviluppo dell'applicazione:

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

C / C ++ è in grado di scalare da piccoli programmi a programmi multi-milioni di file multi-milli e di grandi dimensioni. 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 l'autore di un'interfaccia di funzioni e classi.

Quando si utilizza una funzione, è necessario conoscere l'interfaccia della funzione, i parametri da utilizzare, le funzioni restituite e la necessità di sapere cosa fa la funzione. Questo è facilmente documentato nel file di intestazione senza mai guardare l'implementazione. Quando hai letto l'implementazione di printf ? Acquista lo usiamo ogni giorno.

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

Per lo sviluppo su larga scala, la compilazione e il collegamento può 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 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, l'uso di tecniche di sviluppo software su larga scala è una buona abitudine. Ma dobbiamo anche ricordare perché usiamo queste abitudini.

    
risposta data 24.08.2014 - 22:42
fonte
0

Potresti anche chiedere 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 inline all'interno di un'intestazione
  • tutto all'interno di un'unità di compilazione e nessuna intestazione.

Il momento in cui è ragionevole essere completamente in linea in un'intestazione è quando la classe è in realtà una struttura di dati con pochi 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 all'interno di un'intestazione è quando potresti usare la classe in più progetti e nello specifico vuoi evitare di collegarti nelle librerie.

Un tempo in cui potresti includere un'intera classe all'interno di una unità di compilazione e non esporre affatto l'intestazione è:

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

  • Un'implementazione di una classe base astratta creata da una sorta 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 attraverso 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 qualcuno inserisca l'intestazione.

So cosa potresti pensare ... Se è solo per la manutenibilità, includendo il file cpp (o un'intestazione totalmente inline) puoi facilmente modificare un file per "trovare" il codice e ricostruirlo.

Tuttavia la "manutenibilità" non è solo avere il codice in ordine. È una questione di impatti del cambiamento. È generalmente noto che se si mantiene invariato un header e si cambia solo un file di implementazione (.cpp) , non sarà necessario ricostruire l'altra fonte perché non dovrebbero esserci effetti collaterali.

Questo rende più "sicuro" apportare tali cambiamenti senza preoccuparsi dell'effetto "knock-on" e questo è ciò che realmente si intende per "manutenibilità".

    
risposta data 26.08.2014 - 11:20
fonte
-1

L'esempio di pupazzo di neve ha bisogno di una piccola estensione per mostrarti esattamente perché sono necessari i file .h

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

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

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

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

Bar.cpp - > usa anche la variabile 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 sarebbe stata definita più di una volta.

    
risposta data 24.08.2014 - 00:23
fonte
-2

Se scrivi l'intero codice nello stesso file, allora renderà il tuo codice brutto.
In secondo luogo, non potrai condividere la tua lezione scritta con gli altri. Secondo l'ingegneria del software, è necessario scrivere il codice cliente separatamente. Il cliente non dovrebbe sapere come funziona il tuo programma. Hanno solo bisogno di output Se scrivi l'intero programma nello stesso file, questo danneggerà la sicurezza del tuo programma.

    
risposta data 26.08.2014 - 18:40
fonte

Leggi altre domande sui tag