Perché usare #ifndef CLASS_H e #define CLASS_H nel file .h ma non in .cpp?


137

Ho sempre visto persone scrivere

class.h

#ifndef CLASS_H
#define CLASS_H

//blah blah blah

#endif

La domanda è: perché non lo fanno anche per il file .cpp che contiene definizioni per le funzioni di classe?

Diciamo che ho main.cppe main.cppinclude class.h. Il class.hfile non fa includenulla, quindi come fa a main.cppsapere cosa c'è nel file class.cpp?


5
"importazione" non è probabilmente la parola che vuoi usare qui. Includere.
Kate Gregory,

5
In C ++, non esiste una correlazione 1 a 1 tra file e classi. Puoi mettere tutte le classi in un file che vuoi (o persino dividere una classe su più file, anche se raramente è utile). Pertanto la macro dovrebbe essere FILE_H, non CLASS_H.
sabato

Risposte:


304

Innanzitutto, per rispondere alla tua prima richiesta:

Quando vedi questo nel file .h :

#ifndef FILE_H
#define FILE_H

/* ... Declarations etc here ... */

#endif

Questa è una tecnica di preprocessore per impedire che un file di intestazione venga incluso più volte, il che può essere problematico per vari motivi. Durante la compilazione del progetto, ogni file .cpp (di solito) viene compilato. In termini semplici, questo significa che il compilatore prenderà il tuo file .cpp , aprirà tutti i file #includedda esso, li concatenerà tutti in un unico file di testo, quindi eseguirà l'analisi della sintassi e infine lo convertirà in un codice intermedio, ottimizzerà / eseguirà altri attività e infine generare l'output dell'assembly per l'architettura di destinazione. Per questo motivo, se un file è #includedpiù volte sotto un .cppfile, il compilatore aggiungerà il contenuto del suo file due volte, quindi se ci sono definizioni all'interno di quel file, otterrai un errore del compilatore che ti dice che hai ridefinito una variabile. Quando il file viene elaborato dal passaggio del preprocessore nel processo di compilazione, la prima volta che viene raggiunto il suo contenuto, le prime due righe controlleranno se FILE_Hè stato definito per il preprocessore. In caso contrario, definirà FILE_He continuerà a elaborare il codice tra esso e la #endifdirettiva. Alla successiva visualizzazione del contenuto del file da parte del preprocessore, il segno di spunta FILE_Hsarà falso, quindi scansionerà immediatamente verso il basso #endife continuerà dopo di esso. Questo impedisce errori di ridefinizione.

E per rispondere alla tua seconda preoccupazione:

Nella programmazione C ++ come pratica generale separiamo lo sviluppo in due tipi di file. Uno è con un'estensione di .h e lo chiamiamo un "file di intestazione". Di solito forniscono una dichiarazione di funzioni, classi, strutture, variabili globali, errori di battitura, macro e definizioni di preelaborazione, ecc. Fondamentalmente, forniscono semplicemente informazioni sul codice. Quindi abbiamo l' estensione .cpp che chiamiamo "file di codice". Ciò fornirà definizioni per quelle funzioni, membri della classe, tutti i membri della struttura che hanno bisogno di definizioni, variabili globali, ecc. Quindi il file .h dichiara il codice e il file .cpp implementa tale dichiarazione. Per questo motivo, generalmente durante la compilazione compiliamo ogni .cppfile in un oggetto e quindi collegare tali oggetti (poiché non si vede quasi mai un file .cpp che include un altro file .cpp ).

Come vengono risolti questi esterni è un lavoro per il linker. Quando il compilatore elabora main.cpp , ottiene le dichiarazioni per il codice in class.cpp includendo class.h . Deve solo sapere come appaiono queste funzioni o variabili (che è ciò che ti dà una dichiarazione). Quindi compila il tuo file main.cpp in qualche file oggetto (chiamalo main.obj ). Allo stesso modo, class.cpp è compilato in un class.objfile. Per produrre l'eseguibile finale, viene richiamato un linker per collegare insieme questi due file oggetto. Per eventuali variabili o funzioni esterne non risolte, il compilatore inserirà uno stub in cui avviene l'accesso. Il linker prenderà quindi questo stub e cercherà il codice o la variabile in un altro file oggetto elencato e, se viene trovato, combina il codice dei due file oggetto in un file di output e sostituisce lo stub con la posizione finale della funzione o variabile. In questo modo, il tuo codice in main.cpp può chiamare funzioni e usare variabili in class.cpp SE E SOLO SE SONO DICHIARATE IN class.h .

Spero sia stato utile.


Stavo cercando di capire .h e .cpp negli ultimi giorni. Questa risposta mi ha permesso di risparmiare tempo e interesse per imparare il C ++. Ben scritta. Grazie Giustino!
Rajkumar R

hai davvero spiegato alla grande! Forse la risposta sarebbe abbastanza buona se fosse con le immagini
alamin

13

Il CLASS_Hè un includono guardia ; viene utilizzato per evitare che lo stesso file di intestazione venga incluso più volte (tramite percorsi diversi) all'interno dello stesso file CPP (o, più precisamente, la stessa unità di traduzione ), il che porterebbe a errori a definizione multipla.

Le protezioni di inclusione non sono necessarie sui file CPP perché, per definizione, i contenuti del file CPP vengono letti solo una volta.

Sembra che tu abbia interpretato le guardie di inclusione come aventi le stesse funzioni delle importistruzioni in altre lingue (come Java); non è così, comunque. Lo #includestesso è all'incirca equivalente a quello delle importaltre lingue.


2
"all'interno dello stesso file CPP" dovrebbe leggere "all'interno della stessa unità di traduzione".
dreamlax,

@dreamlax: buon punto - questo è quello che stavo per scrivere inizialmente, ma poi ho pensato che qualcuno che non capisce includere le guardie verrà confuso dal termine "unità di traduzione". Modificherò la risposta per aggiungere "unità di traduzione" tra parentesi: dovrebbe essere il migliore dei due mondi.
Martin B,

6

Non lo fa - almeno durante la fase di compilazione.

La traduzione di un programma c ++ dal codice sorgente al codice macchina viene eseguita in tre fasi:

  1. Preelaborazione : il preprocessore analizza tutto il codice sorgente per le righe che iniziano con # ed esegue le direttive. Nel tuo caso, il contenuto del tuo file class.hviene inserito al posto della riga #include "class.h. Poiché potresti includere il file di intestazione in più punti, le #ifndefclausole evitano errori di dichiarazione duplicati, poiché la direttiva del preprocessore non è definita solo la prima volta che viene incluso il file di intestazione.
  2. Compilazione : il compilatore ora traduce tutti i file di codice sorgente preelaborati in file di oggetti binari.
  3. Collegamento : il collegamento collega (da qui il nome) insieme ai file oggetto. Un riferimento alla tua classe o ad uno dei suoi metodi (che dovrebbe essere dichiarato in class.h e definito in class.cpp) viene risolto nel rispettivo offset in uno dei file oggetto. Scrivo "uno dei tuoi file oggetto" poiché la tua classe non ha bisogno di essere definita in un file chiamato class.cpp, potrebbe essere in una libreria collegata al tuo progetto.

In sintesi, le dichiarazioni possono essere condivise tramite un file di intestazione, mentre il mapping delle dichiarazioni alle definizioni viene eseguito dal linker.


4

Questa è la distinzione tra dichiarazione e definizione. I file di intestazione in genere includono solo la dichiarazione e il file di origine contiene la definizione.

Per usare qualcosa devi solo sapere che è una dichiarazione, non una definizione. Solo il linker deve conoscere la definizione.

Ecco perché includerai un file header in uno o più file sorgente ma non includerai un file sorgente all'interno di un altro.

Anche tu intendi #includee non importare.


3

Questo viene fatto per i file di intestazione in modo che il contenuto appaia una sola volta in ogni file sorgente preelaborato, anche se è incluso più di una volta (di solito perché è incluso da altri file di intestazione). La prima volta che viene incluso, il simbolo CLASS_H(noto come protezione include ) non è stato ancora definito, quindi tutti i contenuti del file sono inclusi. In questo modo si definisce il simbolo, quindi se viene di nuovo incluso, il contenuto del file (all'interno del blocco #ifndef/ #endif) viene ignorato.

Non è necessario eseguire questa operazione per il file sorgente stesso poiché (normalmente) non è incluso da nessun altro file.

Per la tua ultima domanda, class.hdovrebbe contenere la definizione della classe e le dichiarazioni di tutti i suoi membri, le funzioni associate e qualsiasi altra cosa, in modo che ogni file che la include abbia abbastanza informazioni per usare la classe. Le implementazioni delle funzioni possono andare in un file sorgente separato; hai solo bisogno delle dichiarazioni per chiamarle.


2

main.cpp non deve sapere cosa c'è in class.cpp . Deve solo conoscere le dichiarazioni delle funzioni / classi che deve utilizzare e queste dichiarazioni sono in class.h .

Il linker collega i luoghi in cui vengono utilizzate le funzioni / classi dichiarate in class.h e le loro implementazioni in class.cpp


1

.cppi file non sono inclusi (usando #include) in altri file. Pertanto non è necessario includere la protezione. Main.cppconoscerà i nomi e le firme della classe che hai implementato class.cppsolo perché hai specificato tutto ciò in class.h- questo è lo scopo di un file di intestazione. (Spetta a te accertarti che class.hdescriva accuratamente il codice in cui implementi class.cpp.) Il codice eseguibile class.cppsarà reso disponibile al codice eseguibile main.cppgrazie agli sforzi del linker.


1

Si prevede generalmente che moduli di codice come i .cppfile vengano compilati una volta e collegati in più progetti, per evitare inutili compilazioni ripetitive di logica. Ad esempio, g++ -o class.cppprodurrebbe quello class.oche potresti quindi collegare da più progetti all'utilizzo g++ main.cpp class.o.

Potremmo usare #includecome nostro linker, come sembri implicare, ma sarebbe sciocco se sapessimo collegarci correttamente usando il nostro compilatore con meno sequenze di tasti e ripetizione meno dispendiosa della compilazione, piuttosto che il nostro codice con più sequenze di tasti e più dispendioso ripetizione della compilazione ...

I file di intestazione devono comunque essere inclusi in ciascuno dei progetti multipli, tuttavia, poiché ciò fornisce l'interfaccia per ciascun modulo. Senza queste intestazioni il compilatore non sarebbe a conoscenza di nessuno dei simboli introdotti dai .ofile.

È importante rendersi conto che i file di intestazione sono ciò che introduce le definizioni dei simboli per quei moduli; una volta realizzato, ha senso che inclusioni multiple possano causare ridefinizioni di simboli (che causano errori), quindi usiamo le protezioni di inclusione per impedire tali ridefinizioni.


0

è a causa di Headerfile definire ciò che la classe contiene (membri, strutture di dati) e i file cpp la implementano.

E, naturalmente, la ragione principale di ciò è che potresti includere un file .h più volte in altri file .h, ma ciò comporterebbe più definizioni di una classe, che non è valida.

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.