1. C++-Kompilierungsmodell
Normalerweise enthält ein C++-Programm nur zwei Arten von Dateien — .cpp-Dateien und .h-Dateien. Davon werden .cpp-Dateien als C++-Quelldateien bezeichnet, die C++-Quellcode enthalten; .h-Dateien werden als C++-Headerdateien bezeichnet, die ebenfalls C++-Quellcode enthalten.
Die Sprache C++ unterstützt die "separate Kompilierung". Das heißt, alle Inhalte eines Programms können in verschiedene Teile aufgeteilt und in verschiedenen .cpp-Dateien abgelegt werden. Die Inhalte in einer .cpp-Datei sind relativ unabhängig; während der Kompilierung müssen sie nicht mit anderen Dateien kommunizieren. Es ist nur erforderlich, sie nach dem Kompilieren in Objektdateien mit anderen Objektdateien zu linken. Zum Beispiel wird eine globale Funktion "void a(){}" in der Datei a.cpp definiert, und die Datei b.cpp muss diese Funktion aufrufen. Trotzdem müssen die Datei a.cpp und die Datei b.cpp nichts von der Existenz der jeweils anderen wissen. Sie können separat kompiliert werden, und nach dem Kompilieren in Objektdateien werden sie gelinkt, und das gesamte Programm kann ausgeführt werden.
Wie wird das erreicht? Aus Sicht des Schreibens eines Programms ist es sehr einfach. In der Datei b.cpp muss vor dem Aufruf der Funktion "void a()" nur die Funktion "void a();" deklariert werden. Das liegt daran, dass der Compiler beim Kompilieren von b.cpp eine Symboltabelle erzeugt. Symbole ohne Definitionen, wie "void a()", werden in dieser Tabelle gespeichert. Beim Linken sucht der Compiler in anderen Objektdateien nach der Definition dieses Symbols. Sobald sie gefunden wurde, kann das Programm erfolgreich erzeugt werden.
Beachten Sie, dass hier zwei Konzepte erwähnt werden: „Definition“ und „Deklaration“. Einfach gesagt beschreibt eine „Definition“ ein Symbol vollständig: ob es sich um eine Variable oder eine Funktion handelt, welchen Rückgabetyp sie hat, welche Parameter sie benötigt und so weiter. Eine „Deklaration“ erklärt lediglich die Existenz des Symbols, das heißt, sie teilt dem Compiler mit: Dieses Symbol ist in einer anderen Datei definiert; ich werde es hier verwenden, und beim Linken suche bitte woanders, um herauszufinden, was es tatsächlich ist. Beim Definieren müssen Sie ein Symbol (Variable oder Funktion) gemäß der C++-Syntax vollständig definieren; beim Deklarieren müssen Sie jedoch nur den Prototyp des Symbols ausschreiben. Es ist zu beachten, dass ein Symbol im gesamten Programm mehrfach deklariert werden kann, aber genau einmal definiert werden muss. Stellen Sie sich vor: Wenn ein Symbol zwei unterschiedliche Definitionen hat, auf welche sollte der Compiler hören? Dieser Mechanismus bringt C++-Programmierern viele Vorteile und führt auch zu einer Methode, Programme zu schreiben. Betrachten Sie Folgendes: Wenn es eine häufig verwendete Funktion „void f() {}“ gibt, die im gesamten Programm in vielen .cpp-Dateien aufgerufen wird, dann müssen wir die Funktion nur in einer Datei definieren und sie in den anderen Dateien deklarieren. Eine Funktion ist leicht zu handhaben; sie zu deklarieren ist nur ein Satz. Aber was ist, wenn es viele Funktionen gibt, zum Beispiel eine große Sammlung mathematischer Funktionen, mehrere Hundert davon? Können wir garantieren, dass jeder Programmierer sich die Formen aller Funktionen genau merken und sie aufschreiben kann?2. Was ist eine Header-Datei?
Offensichtlich ist die Antwort unmöglich. Aber es gibt eine sehr einfache Möglichkeit, Programmierern die Mühe zu ersparen, sich so viele Funktionsprototypen zu merken: Wir können alle Deklarationsanweisungen dieser Hunderte von Funktionen im Voraus schreiben, sie in eine Datei legen, und wenn Programmierer sie benötigen, können sie alle diese in ihren Quellcode kopieren.
Diese Methode ist zwar machbar, aber sie ist immer noch zu umständlich und ziemlich ungeschickt. Daher können Header-Dateien ihre Rolle spielen. Die sogenannte Header-Datei hat tatsächlich denselben Inhalt wie .cpp-Dateien; beide sind C++-Quellcode. Header-Dateien müssen jedoch nicht kompiliert werden. Wir legen alle Funktionsdeklarationen in eine Header-Datei. Wenn eine .cpp-Quelldatei sie benötigt, können sie über eine Makro-Anweisung "#include" in die .cpp-Datei eingebunden werden, wodurch deren Inhalte in die .cpp-Datei eingefügt werden. Wenn die .cpp-Datei kompiliert wird, werden die eingebundenen .h-Dateien wirksam.
Zum Beispiel: Angenommen, es gibt nur zwei mathematische Funktionen: f1 und f2. Dann legen wir ihre Definitionen in math.cpp ab:
Und legen Sie die Deklarationen "dieser" Funktionen in eine Header-Datei math.h:
In einer anderen Datei, main.cpp, möchte ich diese beiden Funktionen aufrufen, also muss ich nur die Header-Datei einbinden:
Auf diese Weise ist es ein vollständiges Programm. Es sollte beachtet werden, dass die .h-Datei nicht nach dem Compiler-Befehl geschrieben werden muss, aber sie muss an einem Ort sein, an dem der Compiler sie finden kann (zum Beispiel im selben Verzeichnis wie main.cpp). Sowohl main.cpp als auch math.cpp können getrennt kompiliert werden, wodurch main.o und math.o erzeugt werden. Nachdem diese beiden Objektdateien gelinkt wurden, kann das Programm ausgeführt werden.
3. #include
#include ist ein Makro-Befehl aus der Sprache C. Er wird wirksam, bevor der Compiler kompiliert, das heißt während der Präprozessierung. Die Funktion von #include besteht darin, den Inhalt der danach angegebenen Datei vollständig und ohne ein Wort zu ändern in die aktuelle Datei einzufügen. Erwähnenswert ist, dass er keine weiteren Funktionen oder Nebenwirkungen hat. Seine Funktion besteht darin, jede Stelle, an der er vorkommt, durch den Inhalt der danach angegebenen Datei zu ersetzen. Einfache Textersetzung, nichts weiter. Daher wird die erste Zeile in main.cpp (#include "math.h") vor der Kompilierung durch den Inhalt von math.h ersetzt. Das heißt, wenn der Kompilierungsprozess gerade beginnen soll, hat sich der Inhalt von main.cpp bereits geändert:
Nicht mehr, nicht weniger, genau richtig. Nach derselben Logik: Wenn es zusätzlich zu main.cpp viele andere .cpp-Dateien gibt, die ebenfalls die Funktionen f1 und f2 nutzen, dann müssen alle nur #include "math.h" schreiben, bevor sie diese beiden Funktionen verwenden.
4. Was sollte in eine Header-Datei geschrieben werden?
Aus der obigen Diskussion können wir verstehen, dass die Rolle von Header-Dateien darin besteht, von anderen .cpp-Dateien eingebunden zu werden. Sie selbst nehmen nicht an der Kompilierung teil, aber tatsächlich wird ihr Inhalt in mehreren .cpp-Dateien kompiliert. Durch die Regel, dass "eine Definition nur einmal vorkommen darf", können wir leicht schlussfolgern, dass Header-Dateien nur Deklarationen von Variablen und Funktionen enthalten sollten, nicht deren Definitionen. Denn der Inhalt einer Header-Datei wird tatsächlich in mehrere verschiedene .cpp-Dateien eingefügt, und sie werden alle kompiliert. Deklarationen hineinzuschreiben ist in Ordnung. Wenn Definitionen dort hineingeschrieben werden, ist das gleichbedeutend damit, dass eine Definition für ein Symbol (Variable oder Funktion) in mehreren Dateien auftaucht. Selbst wenn diese Definitionen alle identisch sind, ist das für den Compiler illegal.
Ein Punkt, den man sich merken sollte, ist also, dass in .h-Header-Dateien nur Deklarationen von Variablen oder Funktionen stehen sollten, keine Definitionen. Das heißt, Sie können in Header-Dateien nur Anweisungen wie: extern int a; und void f(); schreiben. Das sind Deklarationen. Wenn Sie Anweisungen wie int a; oder void f() {} schreiben, sobald diese Header-Datei von zwei oder mehr .cpp-Dateien eingebunden wird, meldet der Compiler sofort einen Fehler. (Bezüglich extern haben wir es früher besprochen; hier werden wir den Unterschied zwischen Definitionen und Deklarationen nicht erneut diskutieren.)
Es gibt jedoch drei Ausnahmen von dieser Regel:
- Erstens können Definitionen von const-Objekten in Header-Dateien geschrieben werden. Da globale const-Objekte standardmäßig keine extern-Deklarationen haben, sind sie nur in der aktuellen Datei gültig. Wenn man solche Objekte in eine Header-Datei schreibt, selbst wenn diese in mehrere andere .cpp-Dateien eingebunden wird, ist das Objekt nur in der Datei gültig, die sie einbindet, und für andere Dateien unsichtbar, sodass es nicht zu Mehrfachdefinitionen kommt. Gleichzeitig wird dadurch, dass das Objekt in diesen .cpp-Dateien aus einer Header-Datei eingebunden wird, sichergestellt, dass der Wert des const-Objekts in diesen .cpp-Dateien derselbe ist. Man kann sagen, dass man zwei Fliegen mit einer Klappe schlägt. Ebenso können Definitionen von static-Objekten in Header-Dateien platziert werden.
- Zweitens können Definitionen von Inline-Funktionen in Header-Dateien geschrieben werden. Da eine Inline-Funktion den Compiler benötigt, um sie an der Stelle, an der sie gemäß ihrer Definition angetroffen wird, inline zu expandieren, und sie im Gegensatz zu gewöhnlichen Funktionen nicht zuerst deklariert und dann gelinkt werden kann (Inline-Funktionen werden nicht gelinkt), muss der Compiler die vollständige Definition der Inline-Funktion zur Compile-Zeit sehen. Wenn eine Inline-Funktion wie eine gewöhnliche Funktion nur einmal definiert werden könnte, wäre dies schwierig. Denn in einer Datei ist es in Ordnung; ich kann die Definition der Inline-Funktion ganz am Anfang platzieren, sodass die Definition überall dort gesehen werden kann, wo sie später verwendet wird. Aber was, wenn ich diese Funktion auch in anderen Dateien verwende? Es gibt fast keine gute Lösung. Daher legt C++ fest, dass eine Inline-Funktion in einem Programm mehrfach definiert werden kann; solange die Inline-Funktion nur einmal in einer .cpp-Datei vorkommt und die Definition der Inline-Funktion in allen .cpp-Dateien gleich ist, kann sie die Kompilierung bestehen. Offensichtlich ist es eine sehr kluge Praxis, Inline-Funktionsdefinitionen in eine Header-Datei zu legen.
- Drittens können Klassendefinitionen in Header-Dateien geschrieben werden. Denn wenn ein Programm ein Objekt einer Klasse erzeugt, kann der Compiler nur dann wissen, wie das Objekt dieser Klasse angeordnet werden soll, wenn die Klassendefinition vollständig sichtbar ist. Daher sind die Anforderungen an Klassendefinitionen im Grunde dieselben wie an Inline-Funktionen. Es ist also eine gute Praxis, eine Klassendefinition in eine Header-Datei zu legen und diese Header-Datei in die .cpp-Dateien einzubinden, die die Klasse verwenden. Hier sei erwähnt, dass eine Klassendefinition Datenmember und Funktionsmember enthält. Datenmember werden erst definiert (Speicher wird zugewiesen), wenn ein konkretes Objekt erzeugt wird, aber Funktionsmember müssen von Anfang an definiert werden, was wir üblicherweise die Implementierung der Klasse nennen. Im Allgemeinen besteht unsere Vorgehensweise darin, die Klassendefinition in die Header-Datei zu legen und den Implementierungscode der Funktionsmember in eine .cpp-Datei zu schreiben. Das ist akzeptabel und eine sehr gute Vorgehensweise. Es gibt jedoch eine andere Vorgehensweise: den Implementierungscode der Funktionsmember direkt ebenfalls innerhalb der Klassendefinition zu schreiben. In C++-Klassen gilt: Wenn ein Funktionsmember innerhalb des Klassendefinitionskörpers definiert wird, behandelt der Compiler diese Funktion als inline. Daher ist es legal, Funktionsmember innerhalb des Klassendefinitionskörpers zu definieren und sie gemeinsam in die Header-Datei zu legen. Beachten Sie, dass es illegal ist, wenn die Definition eines Funktionsmembers in der Header-Datei der Klassendefinition, aber nicht innerhalb der Klassendefinition geschrieben wird, weil das Funktionsmember an diesem Punkt nicht mehr inline ist. Sobald die Header-Datei von zwei oder mehr .cpp-Dateien eingebunden wird, wird das Funktionsmember neu definiert.
5. Schutzmaßnahmen in Header-Dateien
Stellen Sie sich Folgendes vor: Wenn die Header-Datei nur Deklarationsanweisungen enthält, dann ist es kein Problem, egal wie oft sie von derselben .cpp-Datei eingebunden wird – denn das Vorkommen von Deklarationsanweisungen ist nicht eingeschränkt. Die oben besprochenen drei Ausnahmen in Header-Dateien sind jedoch auch eine sehr häufige Verwendung von Header-Dateien. Sobald also eine der oben genannten drei Ausnahmen in einer Header-Datei auftritt und diese mehrfach von einer .cpp-Datei eingebunden wird, gibt es ein großes Problem. Das liegt daran, dass die Syntaxelemente in diesen drei Ausnahmen, obwohl sie "in mehreren Quelldateien definiert werden können", "in einer einzelnen Quelldatei nur einmal vorkommen dürfen". Stellen Sie sich vor, a.h enthält die Definition der Klasse A und b.h enthält die Definition der Klasse B. Da die Definition der Klasse B von Klasse A abhängt, bindet b.h auch a.h über #include ein. Nun gibt es eine Quelldatei, die sowohl Klasse A als auch Klasse B verwendet, also bindet der Programmierer sowohl a.h als auch b.h in diese Quelldatei ein. An diesem Punkt tritt das Problem auf: Die Definition der Klasse A erscheint zweimal in dieser Quelldatei! Infolgedessen kann das gesamte Programm nicht kompiliert werden. Sie könnten denken, dies sei der Fehler des Programmierers – er hätte wissen müssen, dass b.h a.h einbindet –, aber tatsächlich sollte er es nicht wissen müssen.
Die Verwendung von "#define" in Kombination mit bedingter Kompilierung kann dieses Problem gut lösen. Definieren Sie in einer Header-Datei einen Namen über #define und verwenden Sie die bedingte Kompilierung #ifndef...#endif, damit der Compiler anhand dessen, ob dieser Name definiert ist, entscheiden kann, ob er mit dem Kompilieren des restlichen Teils der Header-Datei fortfahren soll. Diese Methode ist einfach, aber Sie müssen daran denken, sie beim Schreiben von Header-Dateien einzufügen.
Der Unterschied zwischen C++-Header-Dateien und Quelldateien
1. Wie Quelldateien anhand von #include mit Header-Dateien verknüpft werden
- 1. Vom System bereitgestellte Header-Dateien werden in spitze Klammern eingeschlossen, daher sucht der Compiler sie im Systemdateiverzeichnis.
- 2. Benutzerdefinierte Dateien werden in doppelte Anführungszeichen eingeschlossen. Der Compiler sucht zuerst im Benutzerverzeichnis, dann im C++-Installationsverzeichnis (zum Beispiel kann man in VC den Suchpfad für Bibliotheksdateien angeben und ändern, und unter Unix und Linux kann man ihn über Umgebungsvariablen festlegen) und schließlich in Systemdateien.
#include "xxx.h" (Ich dachte immer, "" und <> machten keinen Unterschied, aber tinyxml.h ist eine Nicht-Systemdatei, also muss "" verwendet werden)
2. Wie Header-Dateien mit Quelldateien zusammenhängen
Diese Frage bedeutet eigentlich: Angenommen, die Header-Datei "a.h" deklariert eine Reihe von Funktionen und "b.cpp" implementiert diese Funktionen; wenn ich in "c.cpp" die in "a.h" deklarierten und in "b.cpp" implementierten Funktionen verwenden möchte, verwende ich normalerweise #include "a.h" in "c.cpp". Wie findet c.cpp dann die Implementierungen in b.cpp?
Tatsächlich haben die Dateinamen von .cpp und .h keine direkte Beziehung. Viele Compiler können andere Erweiterungen akzeptieren. Zum Beispiel sehe ich jetzt im Quellcode unserer Firma, dass .cpp-Dateien durch .cc-Dateien ersetzt wurden.
In Turbo C erfolgt die Kompilierung über die Kommandozeile, und der Kommandozeilenparameter ist der Dateiname, dessen Standarderweiterungen .cpp und .h sind, aber sie können auch auf .xxx und so weiter angepasst werden.
In dem Buch "C Programming" von Lehrer Tan Haoqiang wird erwähnt, dass während der Compiler-Vorverarbeitung der Befehl #include als "Dateieinbindung" verarbeitet wird: Der gesamte Inhalt von file2.c wird an die Position von #include "file2.c" kopiert. Das erklärt auch, warum es vielen Compilern egal ist, welche Endung die Datei hat – weil die #include-Vorverarbeitung lediglich eine Aufgabe des "Kopierens und Einfügens von Code" erledigt.
Während der Kompilierung wird nicht nach den Funktionsimplementierungen in b.cpp gesucht; dies geschieht erst zur Linkzeit. Die Verwendung von #include "a.h" in b.cpp oder c.cpp führt tatsächlich die zugehörigen Deklarationen ein, damit die Kompilierung durchläuft. Das Programm kümmert sich nicht darum, wo die Implementierung liegt oder wie sie implementiert ist. Nachdem die Quelldateien kompiliert wurden, werden Objektdateien (.o- oder .obj-Dateien) erzeugt. In diesen Objektdateien werden diese Funktionen und Variablen als Symbole behandelt. Zur Linkzeit muss das Makefile angeben, welche .o- oder .obj-Datei gelinkt werden soll (hier die von b.cpp erzeugte .o- oder .obj-Datei). An diesem Punkt geht der Linker zu dieser .o- oder .obj-Datei, um die in b.cpp implementierten Funktionen zu finden, und baut sie dann in die im Makefile angegebene ausführbare Datei ein.
Unter Unix kann man sogar vermeiden, Header-Dateien in Quelldateien einzubinden; man muss sie nur im Makefile angeben (aber das verringert die Lesbarkeit des Programms stark, was eine schlechte Angewohnheit ist, ^_^). In VC muss man im Allgemeinen kein Makefile selbst schreiben; man muss nur alle erforderlichen Dateien in das Projekt aufnehmen, und VC schreibt das Makefile automatisch für dich.
Normalerweise sucht der C++-Compiler die benötigten Symbole in jeder .o- oder .obj-Datei, statt nur in einer bestimmten Datei oder nach dem Finden eines Symbols aufzuhören. Wenn daher dieselbe Funktion in mehreren verschiedenen Dateien implementiert oder dieselbe globale Variable definiert wird, meldet der Linker beim Linken "redefined".
Zusammenfassung
.h-Dateien können enthalten:
- Deklarationen von Datenelementen einer Klasse, aber Werte können nicht zugewiesen werden
- Definitionen und Zuweisungen von statischen Datenelementen einer Klasse, aber dies wird nicht empfohlen; eine bloße Deklaration genügt.
- Deklarationen von Memberfunktionen einer Klasse
- Deklarationen von Funktionen, die keine Klassenmember sind
- Konstantendefinitionen: z. B. constint a=5;
- Definitionen von statischen Funktionen
- Definitionen von Inline-Funktionen einer Klasse
Darf nicht enthalten:
- 1. Deklarationen aller nicht-statischen Variablen (keine Klassendatenelemente)
- 2. Standard-Namespace-Deklarationen nicht in Header-Dateien einfügen; using namespace std; usw. sollten in .cpp-Dateien stehen. Verwenden Sie std::string in .h-Dateien.
Original-URL: https://blog.csdn.net/qq_35038153/article/details/71293265