Warum müssen wir sowohl die .h
und .cpp
-Dateien einschließen, während wir dafür sorgen können, dass es nur durch Einschließen der .cpp
Datei funktioniert ?
Beispiel: Erstellen einer file.h
enthaltenden Deklaration, anschließendes Erstellen einer file.cpp
enthaltenden Definition und Einbeziehen von beidem in main.cpp
.
Alternativ: Erstellen einer file.cpp
enthaltenen Deklaration / Definition (keine Prototypen), die diese enthält main.cpp
.
Beide arbeiten für mich. Ich kann den Unterschied nicht erkennen. Vielleicht kann ein Einblick in den Kompilierungs- und Verknüpfungsprozess hilfreich sein.
Antworten:
Sie können zwar
.cpp
Dateien wie erwähnt einschließen , dies ist jedoch keine gute Idee.Wie Sie bereits erwähnt haben, gehören Deklarationen in Header-Dateien. Diese verursachen keine Probleme, wenn sie in mehreren Kompilierungseinheiten enthalten sind, da sie keine Implementierungen enthalten. Das mehrfache Einfügen der Definition einer Funktion oder eines Klassenmitglieds verursacht normalerweise ein Problem (aber nicht immer), da der Linker verwirrt wird und einen Fehler auslöst.
Was passieren sollte, ist, dass jede
.cpp
Datei Definitionen für eine Teilmenge des Programms enthält, wie z. B. eine Klasse, eine logisch organisierte Gruppe von Funktionen, globale statische Variablen (wenn überhaupt, sparsam verwenden) usw.Jede Kompilierungseinheit (
.cpp
Datei) enthält dann alle Deklarationen, die zum Kompilieren der darin enthaltenen Definitionen erforderlich sind. Es verfolgt die Funktionen und Klassen, auf die es verweist, die es jedoch nicht enthält, sodass der Linker sie später auflösen kann, wenn er den Objektcode in eine ausführbare Datei oder Bibliothek kombiniert.Beispiel
Foo.h
-> enthält Deklaration (Interface) für Klasse Foo.Foo.cpp
-> enthält die Definition (Implementierung) für die Klasse Foo.Main.cpp
-> enthält Hauptmethode, Programmeinstiegspunkt. Dieser Code instanziiert ein Foo und verwendet es.Beides
Foo.cpp
undMain.cpp
müssen mit einbezogen werdenFoo.h
.Foo.cpp
benötigt es, weil es den Code definiert, der die Klassenschnittstelle unterstützt. Daher muss es wissen, was diese Schnittstelle ist.Main.cpp
benötigt es, weil es ein Foo erstellt und sein Verhalten aufruft. Daher muss es wissen, was dieses Verhalten ist, wie groß ein Foo im Speicher ist und wie es seine Funktionen findet usw., aber es benötigt noch keine eigentliche Implementierung.Der Compiler generiert
Foo.o
darausFoo.cpp
den gesamten Foo-Klassencode in kompilierter Form. Es generiertMain.o
auch die Hauptmethode und nicht aufgelöste Verweise auf die Klasse Foo.Jetzt kommt der Linker, der die beiden Objektdateien kombiniert
Foo.o
undMain.o
in eine ausführbare Datei. Es sieht die ungelösten Foo-Referenzen inMain.o
, sieht aber, dass dieseFoo.o
die notwendigen Symbole enthalten, so dass es sozusagen "die Punkte verbindet". Ein FunktionsaufrufMain.o
ist nun mit der tatsächlichen Position des kompilierten Codes verbunden, sodass das Programm zur Laufzeit zur richtigen Position springen kann.Wenn Sie die
Foo.cpp
DateiMain.cpp
eingefügt hätten, gäbe es zwei Definitionen der Klasse Foo. Der Linker würde dies sehen und sagen: "Ich weiß nicht, welchen ich wählen soll, also ist dies ein Fehler." Der Kompilierungsschritt wäre erfolgreich, das Verknüpfen jedoch nicht. (Es sei denn, Sie kompilieren einfach nicht,Foo.cpp
aber warum ist es dann in einer separaten.cpp
Datei?)Schließlich ist die Idee der verschiedenen Dateitypen für einen C / C ++ - Compiler irrelevant. Es kompiliert "Textdateien", die hoffentlich gültigen Code für die gewünschte Sprache enthalten. Manchmal kann es vorkommen, dass die Sprache anhand der Dateierweiterung ermittelt wird. Kompilieren Sie zum Beispiel eine
.c
Datei ohne Compiler-Optionen und es wird C angenommen, während eine.cc
oder eine.cpp
Erweiterung es veranlassen würde, C ++ anzunehmen. Ich kann einem Compiler jedoch leicht anweisen, eine.h
oder sogar eine.docx
Datei als C ++ zu kompilieren , und er gibt eine object (.o
) - Datei aus, wenn sie gültigen C ++ - Code im Nur-Text-Format enthält. Diese Erweiterungen kommen eher dem Programmierer zugute. Wenn ichFoo.h
und seheFoo.cpp
, gehe ich sofort davon aus, dass die erste die Deklaration der Klasse und die zweite die Definition enthält.quelle
Foo.cpp
inMain.cpp
Sie nicht brauchen , um die enthalten.h
Datei, haben Sie eine weniger Datei, noch Sie beim Spalten Code in separate Dateien zur besseren Lesbarkeit gewinnen, und Ihr Kompilierungsbefehl ist einfacher. Obwohl ich die Wichtigkeit von Header-Dateien verstehe, glaube ich nicht, dass es das istIf you had included the Foo.cpp file in Main.cpp, there would be two definitions of class Foo.
Wichtigster Satz zur Frage.Lesen Sie mehr über die Rolle des C- und C ++ - Präprozessors , der konzeptionell die erste "Phase" des C- oder C ++ - Compilers darstellt (in der Vergangenheit war es ein separates Programm
/lib/cpp
; jetzt ist es aus Leistungsgründen im eigentlichen Compilercc1
oder integriertcc1plus
). Lesen Sie insbesondere die Dokumentation des GNU-cpp
Präprozessors . In der Praxis verarbeitet der Compiler Ihre Kompilierungseinheit (oder Übersetzungseinheit ) also konzeptionell zuerst vor und arbeitet dann an dem vorverarbeiteten Formular.Sie müssen wahrscheinlich immer die Header-Datei einschließen,
file.h
wenn sie enthält (wie durch Konventionen und Gewohnheiten vorgegeben ):typedef
,struct
,class
etc, ...)static inline
FunktionenBeachten Sie, dass es eine Frage der Konventionen (und der Zweckmäßigkeit) ist, diese in eine Header-Datei einzufügen.
Natürlich
file.cpp
benötigt Ihre Implementierung all das oben Genannte und möchte dies#include "file.h"
zunächst tun .Dies ist eine Konvention (aber eine sehr verbreitete). Sie könnten Header-Dateien vermeiden und deren Inhalt in Implementierungsdateien (dh Übersetzungseinheiten) kopieren und einfügen. Aber das wollen Sie nicht (außer vielleicht, wenn Ihr C- oder C ++ - Code automatisch generiert wird; dann könnten Sie das Generatorprogramm so kopieren und einfügen lassen, dass es die Rolle des Präprozessors nachahmt).
Der Punkt ist, dass der Präprozessor nur Textoperationen ausführt . Sie können es (im Prinzip) ganz vermeiden, indem Sie es kopieren und einfügen oder durch einen anderen "Präprozessor" oder C-Code-Generator (wie gpp oder m4 ) ersetzen .
Ein weiteres Problem besteht darin, dass die aktuellen C- (oder C ++ -) Standards mehrere Standardheader definieren. Die meisten Implementierungen implementieren diese Standard- Header tatsächlich als (implementierungsspezifische) Dateien , aber ich glaube, dass es für eine konforme Implementierung möglich wäre, Standard-Includes (wie
#include <stdio.h>
für C oder#include <vector>
C ++) mit einigen Zaubertricks (z. B. unter Verwendung einer Datenbank oder einer anderen) zu implementieren Informationen im Compiler).Bei Verwendung von GCC- Compilern (z. B.
gcc
oderg++
) können Sie das-H
Flag verwenden, um über jede Aufnahme informiert zu werden, und die-C -E
Flags, um das vorverarbeitete Formular zu erhalten. Natürlich gibt es viele andere Compiler-Flags, die sich auf die Vorverarbeitung auswirken (z. B.-I /some/dir/
um/some/dir/
enthaltene Dateien für die Suche hinzuzufügen und-D
einige Präprozessor-Makros usw. usw. vorab zu definieren).NB. Zukünftige Versionen von C ++ (möglicherweise C ++ 20 , möglicherweise sogar später) enthalten möglicherweise C ++ - Module .
quelle
Aufgrund des Build-Modells für mehrere C ++ - Einheiten benötigen Sie eine Möglichkeit, Code in Ihrem Programm nur einmal anzuzeigen (Definitionen), und Sie benötigen eine Möglichkeit, Code in jeder Übersetzungseinheit Ihres Programms anzuzeigen (Deklarationen).
Daraus entsteht die C ++ - Header-Idiom. Es ist Konvention aus einem Grund.
Sie können Ihr gesamtes Programm in eine einzelne Übersetzungseinheit sichern, dies führt jedoch zu Problemen bei der Wiederverwendung von Code, beim Testen von Einheiten und bei der Behandlung von Abhängigkeiten zwischen Modulen. Es ist auch nur ein großes Durcheinander.
quelle
Die ausgewählte Antwort von Warum müssen wir eine Header-Datei schreiben? ist eine vernünftige Erklärung, aber ich wollte zusätzliche Details hinzufügen.
Es scheint, dass das Rationale für Header-Dateien beim Unterrichten und Erörtern von C / C ++ verloren geht.
Die Header-Datei bietet eine Lösung für zwei Probleme bei der Anwendungsentwicklung:
C / C ++ kann von kleinen Programmen bis zu sehr großen Programmen mit mehreren Millionen Zeilen und mehreren Tausend Dateien skaliert werden. Die Anwendungsentwicklung kann von Teams aus einem Entwickler bis zu Hunderten von Entwicklern skaliert werden.
Sie können als Entwickler mehrere Hüte tragen. Insbesondere können Sie der Benutzer einer Schnittstelle zu Funktionen und Klassen sein, oder Sie können der Schreiber einer Schnittstelle von Funktionen und Klassen sein.
Wenn Sie eine Funktion verwenden, müssen Sie die Funktionsschnittstelle kennen, die zu verwendenden Parameter kennen, die zurückgegebenen Funktionen kennen und wissen, was die Funktion tut. Dies kann leicht in der Header-Datei dokumentiert werden, ohne jemals die Implementierung zu betrachten. Wann haben Sie die Implementierung von gelesen
printf
? Kaufen wir es jeden Tag.Wenn Sie der Entwickler einer Benutzeroberfläche sind, ändert der Hut die andere Richtung. Die Header-Datei enthält die Deklaration der öffentlichen Schnittstelle. Die Header-Datei definiert, was eine andere Implementierung benötigt, um diese Schnittstelle zu verwenden. Interne und private Informationen zu dieser neuen Schnittstelle werden in der Header-Datei nicht deklariert (und sollten nicht deklariert werden). Die öffentliche Header-Datei sollte alles sein, was jemand zur Verwendung des Moduls benötigt.
Für die Entwicklung in großem Maßstab kann das Kompilieren und Verknüpfen viel Zeit in Anspruch nehmen. Von vielen Minuten bis zu vielen Stunden (sogar zu vielen Tagen!). Die Aufteilung der Software in Schnittstellen (Header) und Implementierungen (Quellen) bietet eine Methode, mit der nur Dateien kompiliert werden können, die kompiliert werden müssen, anstatt alles neu zu erstellen.
Darüber hinaus können Entwickler mithilfe von Header-Dateien eine Bibliothek (bereits kompiliert) sowie eine Header-Datei bereitstellen. Andere Benutzer der Bibliothek sehen möglicherweise nie die richtige Implementierung, können die Bibliothek jedoch weiterhin mit der Headerdatei verwenden. Sie tun dies jeden Tag mit der C / C ++ - Standardbibliothek.
Selbst wenn Sie eine kleine Anwendung entwickeln, ist die Verwendung umfangreicher Softwareentwicklungstechniken eine gute Angewohnheit. Wir müssen uns aber auch daran erinnern, warum wir diese Gewohnheiten anwenden.
quelle
Sie können sich auch fragen, warum Sie nicht einfach Ihren gesamten Code in einer Datei speichern.
Die einfachste Antwort ist die Pflege des Codes.
Manchmal ist es sinnvoll, eine Klasse zu erstellen:
Die Zeit, in der es sinnvoll ist, vollständig in einen Header einzufügen, ist, wenn die Klasse wirklich eine Datenstruktur mit ein paar grundlegenden Gettern und Setzern und möglicherweise einem Konstruktor ist, der Werte zur Initialisierung seiner Member benötigt.
(Vorlagen, die alle inline sein müssen, sind ein etwas anderes Problem).
Die andere Möglichkeit, eine Klasse innerhalb eines Headers zu erstellen, besteht darin, dass Sie die Klasse möglicherweise in mehreren Projekten verwenden und insbesondere das Verknüpfen in Bibliotheken vermeiden möchten.
Ein Zeitpunkt, zu dem Sie möglicherweise eine ganze Klasse in eine Kompilierungseinheit einschließen und deren Header überhaupt nicht verfügbar machen, ist:
Eine "impl" -Klasse, die nur von der implementierten Klasse verwendet wird. Es ist ein Implementierungsdetail dieser Klasse und wird extern nicht verwendet.
Eine Implementierung einer abstrakten Basisklasse, die von einer Factory-Methode erstellt wird, die einen Zeiger (Referenz) auf die Basisklasse zurückgibt. Die Factory-Methode würde von der Klasse selbst nicht verfügbar gemacht werden. (Wenn die Klasse eine Instanz hat, die sich über eine statische Instanz in einer Tabelle registriert, muss sie nicht einmal über eine Factory verfügbar gemacht werden.)
Eine "functor" Typenklasse.
Mit anderen Worten, wenn Sie nicht möchten, dass jemand den Header einfügt.
Ich weiß, was Sie vielleicht denken ... Wenn es nur um die Wartbarkeit geht, können Sie durch Einfügen der cpp-Datei (oder eines vollständig eingebetteten Headers) eine Datei leicht bearbeiten, um den Code zu "finden" und einfach neu zu erstellen.
"Wartbarkeit" bedeutet jedoch nicht nur, dass der Code ordentlich aussieht. Es ist eine Frage der Auswirkungen von Veränderungen. Es ist allgemein bekannt, dass, wenn Sie einen Header unverändert lassen und nur eine Implementierungsdatei ändern
(.cpp)
, keine Neuerstellung der anderen Quelle erforderlich ist, da keine Nebenwirkungen auftreten sollten.Dies macht es "sicherer", solche Änderungen vorzunehmen, ohne sich Gedanken über den Anstoßeffekt zu machen, und das ist es, was wirklich unter "Wartbarkeit" zu verstehen ist.
quelle
Das Beispiel von Snowman benötigt eine kleine Erweiterung, um genau zu zeigen, warum .h-Dateien benötigt werden.
Fügen Sie dem Spiel eine weitere Klassenleiste hinzu, die ebenfalls von der Klasse Foo abhängt.
Foo.h -> enthält Deklaration für Klasse Foo
Foo.cpp -> enthält die Definition (Implementierung) der Klasse Foo
Main.cpp -> benutzt eine Variable vom Typ Foo.
Bar.cpp -> verwendet auch Variablen vom Typ Foo.
Jetzt müssen alle cpp-Dateien Foo.h enthalten. Es wäre ein Fehler, Foo.cpp in mehr als eine andere cpp-Datei aufzunehmen. Der Linker würde fehlschlagen, da die Klasse Foo mehr als einmal definiert würde.
quelle
.h
ohnehin in Dateien gelöst wird - mit include guards und ist genau das gleiche Problem, das Sie mit.h
Dateien haben. Dafür sind.h
Dateien überhaupt nicht gedacht ..cpp
Datei einschließen .Wenn Sie den gesamten Code in dieselbe Datei schreiben, wird Ihr Code hässlich.
Zweitens können Sie Ihren schriftlichen Unterricht nicht mit anderen teilen. Laut Software-Engineering sollten Sie Client-Code separat schreiben. Der Client sollte nicht wissen, wie Ihr Programm funktioniert. Sie müssen nur ausgegeben werden. Wenn Sie das gesamte Programm in dieselbe Datei schreiben, wird die Sicherheit Ihres Programms beeinträchtigt.
quelle