Warum sollten Sie #ifndef CLASS_H und #define CLASS_H in der .h-Datei verwenden, aber nicht in .cpp?

136

Ich habe immer Leute schreiben sehen

class.h

#ifndef CLASS_H
#define CLASS_H

//blah blah blah

#endif

Die Frage ist, warum machen sie das nicht auch für die CPP-Datei, die Definitionen für Klassenfunktionen enthält?

Nehmen wir an, ich habe main.cppund main.cppschließt ein class.h. Die class.hDatei macht includenichts, also woher weiß main.cpp, was in der ist class.cpp?

user385261
quelle
5
"import" ist wahrscheinlich nicht das Wort, das Sie hier verwenden möchten. Einschließen.
Kate Gregory
5
In C ++ gibt es keine 1: 1-Korrelation zwischen Dateien und Klassen. Sie können so viele Klassen in eine Datei einfügen, wie Sie möchten (oder sogar eine Klasse auf mehrere Dateien aufteilen, obwohl dies selten hilfreich ist). Daher sollte das Makro FILE_Hnicht sein CLASS_H.
sbi
1
Siehe meinen Ratschlag .

Antworten:

302

Um Ihre erste Anfrage zu beantworten:

Wenn Sie dies in der .h- Datei sehen:

#ifndef FILE_H
#define FILE_H

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

#endif

Dies ist eine Präprozessortechnik, mit der verhindert wird, dass eine Headerdatei mehrmals enthalten ist, was aus verschiedenen Gründen problematisch sein kann. Während der Kompilierung Ihres Projekts wird (normalerweise) jede CPP- Datei kompiliert. In einfachen Worten bedeutet dies, dass der Compiler Ihre CPP- Datei nimmt, alle Dateien #includeddamit öffnet , sie alle zu einer massiven Textdatei verkettet, dann eine Syntaxanalyse durchführt und sie schließlich in einen Zwischencode konvertiert, andere optimiert / ausführt Aufgaben und generieren schließlich die Assembly-Ausgabe für die Zielarchitektur. Aus diesem Grund, wenn sich eine Datei #includedmehrmals unter einer CPP befindetDatei, der Compiler hängt seinen Dateiinhalt zweimal an. Wenn also Definitionen in dieser Datei vorhanden sind, wird ein Compilerfehler angezeigt, der Sie darüber informiert, dass Sie eine Variable neu definiert haben. Wenn die Datei vom Präprozessorschritt im Kompilierungsprozess verarbeitet wird, prüfen die ersten beiden Zeilen beim ersten Erreichen ihres Inhalts, ob FILE_Hsie für den Präprozessor definiert wurde. Wenn nicht, wird FILE_Hder Code zwischen ihm und der #endifDirektive definiert und weiter verarbeitet . Wenn der Inhalt dieser Datei das nächste Mal vom Präprozessor angezeigt wird, ist die Prüfung gegen FILE_Hfalsch, sodass sofort nach unten gescannt #endifund danach fortgefahren wird. Dies verhindert Neudefinitionsfehler.

Und um Ihr zweites Anliegen anzusprechen:

In der C ++ - Programmierung als allgemeine Praxis trennen wir die Entwicklung in zwei Dateitypen. Eine ist mit einer Erweiterung von .h und wir nennen dies eine "Header-Datei". Sie enthalten normalerweise eine Deklaration von Funktionen, Klassen, Strukturen, globalen Variablen, Typedefs, Vorverarbeitungsmakros und Definitionen usw. Grundsätzlich liefern sie nur Informationen zu Ihrem Code. Dann haben wir die Erweiterung .cpp, die wir als " Codedatei " bezeichnen. Dadurch werden Definitionen für diese Funktionen, Klassenmitglieder und alle Strukturelemente bereitgestellt , die Definitionen, globale Variablen usw. benötigen. Die .h- Datei deklariert also Code, und die .cpp- Datei implementiert diese Deklaration. Aus diesem Grund kompilieren wir in der Regel während der Kompilierung jede .cppDatei in ein Objekt und verknüpfen Sie diese Objekte (da Sie fast nie sehen, dass eine CPP- Datei eine andere CPP- Datei enthält).

Wie diese externen Elemente aufgelöst werden, ist eine Aufgabe für den Linker. Wenn Ihr Compiler verarbeitet main.cpp , wird es Erklärungen für den Code in class.cpp , indem class.h . Es muss nur wissen, wie diese Funktionen oder Variablen aussehen (was eine Deklaration Ihnen gibt). So kompiliert es Ihre main.cpp- Datei in eine Objektdatei (nennen Sie es main.obj ). In ähnlicher Weise wird class.cpp in eine class.obj kompiliertDatei. Um die endgültige ausführbare Datei zu erstellen, wird ein Linker aufgerufen, um diese beiden Objektdateien miteinander zu verknüpfen. Für nicht aufgelöste externe Variablen oder Funktionen platziert der Compiler einen Stub, an dem der Zugriff erfolgt. Der Linker nimmt dann diesen Stub und sucht nach dem Code oder der Variablen in einer anderen aufgelisteten Objektdatei. Wenn er gefunden wird, kombiniert er den Code aus den beiden Objektdateien zu einer Ausgabedatei und ersetzt den Stub durch den endgültigen Speicherort der Funktion oder Variable. Auf diese Weise kann Ihr Code in main.cpp Funktionen aufrufen und Variablen in class.cpp verwenden, WENN UND NUR, WENN SIE IN class.h ERKLÄRT SIND .

Ich hoffe das war hilfreich.

Justin Summerlin
quelle
Ich habe in den letzten Tagen versucht, .h und .cpp zu verstehen. Diese Antwort hat mir Zeit und Interesse gespart, C ++ zu lernen. Gut geschrieben. Danke Justin!
Rajkumar R
du hast es wirklich toll erklärt! Vielleicht wäre die Antwort ziemlich gut, wenn es mit Bildern wäre
Alamin
13

Das CLASS_Hist ein Include Guard ; Es wird verwendet, um zu vermeiden, dass dieselbe Header-Datei mehrmals (über verschiedene Routen) in derselben CPP-Datei (oder genauer gesagt derselben Übersetzungseinheit ) enthalten ist, was zu Fehlern bei der Mehrfachdefinition führen würde.

Include Guards sind für CPP-Dateien nicht erforderlich, da der Inhalt der CPP-Datei per Definition nur einmal gelesen wird.

Sie scheinen die Include-Guards so interpretiert zu haben, dass sie dieselbe Funktion haben wie importAnweisungen in anderen Sprachen (z. B. Java). Dies ist jedoch nicht der Fall. Das #includeselbst entspricht in etwa dem importin anderen Sprachen.

Martin B.
quelle
2
"innerhalb derselben CPP-Datei" sollte "innerhalb derselben Übersetzungseinheit" lauten.
Dreamlax
@dreamlax: Guter Punkt - das wollte ich ursprünglich schreiben, aber dann dachte ich mir, dass jemand, der nicht versteht, dass Wachen eingeschlossen sind, nur durch den Begriff "Übersetzungseinheit" verwirrt wird. Ich werde die Antwort bearbeiten, um "Übersetzungseinheit" in Klammern hinzuzufügen - das sollte das Beste aus beiden Welten sein.
Martin B
6

Zumindest während der Kompilierungsphase nicht.

Die Übersetzung eines C ++ - Programms vom Quellcode in den Maschinencode erfolgt in drei Phasen:

  1. Vorverarbeitung - Der Präprozessor analysiert den gesamten Quellcode für Zeilen, die mit # beginnen, und führt die Anweisungen aus. In Ihrem Fall wird der Inhalt Ihrer Datei class.hanstelle der Zeile eingefügt #include "class.h. Da Sie möglicherweise an mehreren Stellen in Ihre Header-Datei aufgenommen werden, #ifndefvermeiden die Klauseln doppelte Deklarationsfehler, da die Präprozessor-Direktive nur beim ersten Einfügen der Header-Datei undefiniert ist.
  2. Kompilierung - Der Compiler übersetzt jetzt alle vorverarbeiteten Quellcodedateien in binäre Objektdateien.
  3. Verknüpfen - Der Linker verknüpft (daher der Name) die Objektdateien miteinander. Ein Verweis auf Ihre Klasse oder eine ihrer Methoden (die in class.h deklariert und in class.cpp definiert werden sollte) wird in einer der Objektdateien in den entsprechenden Offset aufgelöst. Ich schreibe ‚eine Ihrer Objektdateien‘ , da die Klasse nicht funktioniert muß in einer Datei mit dem Namen class.cpp definiert werden, könnte es in einer Bibliothek sein , die zu einem Projekt verbunden ist.

Zusammenfassend kann gesagt werden, dass die Deklarationen über eine Header-Datei gemeinsam genutzt werden können, während die Zuordnung von Deklarationen zu Definitionen vom Linker vorgenommen wird.

sum1stolemyname
quelle
4

Das ist der Unterschied zwischen Deklaration und Definition. Header-Dateien enthalten normalerweise nur die Deklaration, und die Quelldatei enthält die Definition.

Um etwas zu verwenden, müssen Sie nur die Deklaration und nicht die Definition kennen. Nur der Linker muss die Definition kennen.

Aus diesem Grund fügen Sie eine Header-Datei in eine oder mehrere Quelldateien ein, aber keine Quelldatei in eine andere.

Auch du meinst #includeund nicht importieren.

Brian R. Bondy
quelle
3

Dies geschieht für Header-Dateien, sodass der Inhalt in jeder vorverarbeiteten Quelldatei nur einmal angezeigt wird, selbst wenn er mehr als einmal enthalten ist (normalerweise, weil er aus anderen Header-Dateien stammt). Beim ersten Einfügen wurde das Symbol CLASS_H(als Include Guard bezeichnet ) noch nicht definiert, sodass der gesamte Inhalt der Datei enthalten ist. Dadurch wird das Symbol definiert. Wenn es erneut enthalten ist, wird der Inhalt der Datei (innerhalb des #ifndef/ #endif-Blocks) übersprungen.

Dies ist für die Quelldatei selbst nicht erforderlich, da dies (normalerweise) in keiner anderen Datei enthalten ist.

Für Ihre letzte Frage class.hsollte die Definition der Klasse und Deklarationen aller ihrer Mitglieder, zugehörigen Funktionen und was auch immer enthalten sein, damit jede Datei, die sie enthält, über genügend Informationen verfügt, um die Klasse zu verwenden. Die Implementierungen der Funktionen können in einer separaten Quelldatei erfolgen. Sie brauchen nur die Deklarationen, um sie aufzurufen.

Mike Seymour
quelle
2

main.cpp muss nicht wissen, was in class.cpp enthalten ist . Es muss nur die Deklarationen der Funktionen / Klassen kennen, die verwendet werden sollen, und diese Deklarationen befinden sich in class.h .

Der Linker verknüpft die Stellen, an denen die in class.h deklarierten Funktionen / Klassen verwendet werden, und ihre Implementierungen in class.cpp

Igor Oks
quelle
1

.cppDateien sind nicht #includein anderen Dateien enthalten. Daher müssen sie keine Bewachung einschließen. Main.cppkennt die Namen und Signaturen der Klasse, in der Sie implementiert haben, class.cppnur, weil Sie all das angegeben haben class.h- dies ist der Zweck einer Header-Datei. (Es liegt an Ihnen, sicherzustellen, dass class.hder Code, in dem Sie implementieren , genau beschrieben wird class.cpp.) Der ausführbare Code in class.cppwird main.cppdank der Bemühungen des Linkers dem ausführbaren Code zur Verfügung gestellt .

Kate Gregory
quelle
1

Es wird allgemein erwartet, dass Codemodule wie .cppDateien einmal kompiliert und in mehreren Projekten verknüpft werden, um eine unnötige wiederholte Kompilierung der Logik zu vermeiden. Zum Beispiel g++ -o class.cppwürde produzieren, class.odie Sie dann von mehreren Projekten zur Verwendung verknüpfen könnten g++ main.cpp class.o.

Wir könnten #includeals Linker verwenden, wie Sie zu implizieren scheinen, aber das wäre einfach albern, wenn wir wissen, wie man mit unserem Compiler mit weniger Tastenanschlägen und weniger verschwenderischen Wiederholungen der Kompilierung richtig verknüpft, anstatt unseren Code mit mehr Tastenanschlägen und verschwenderischer Wiederholung der Zusammenstellung ...

Die Header-Dateien müssen jedoch weiterhin in jedem der mehreren Projekte enthalten sein, da dies die Schnittstelle für jedes Modul darstellt. Ohne diese Header würde der Compiler keines der von den .oDateien eingeführten Symbole kennen .

Es ist wichtig zu wissen, dass die Header-Dateien die Definitionen der Symbole für diese Module einführen. Sobald dies realisiert ist, ist es sinnvoll, dass mehrere Einschlüsse zu Neudefinitionen von Symbolen führen können (was zu Fehlern führt). Daher verwenden wir Include-Wachen, um solche Neudefinitionen zu verhindern.

autistisch
quelle
0

Aufgrund von Header-Dateien wird definiert, was die Klasse enthält (Mitglieder, Datenstrukturen), und CPP-Dateien implementieren sie.

Der Hauptgrund dafür ist natürlich, dass Sie eine .h-Datei mehrmals in andere .h-Dateien aufnehmen können. Dies würde jedoch zu mehreren Definitionen einer Klasse führen, was ungültig ist.

Quonux
quelle