Warum müssen wir die .h einschließen, während alles funktioniert, wenn wir nur die .cpp-Datei einschließen?

18

Warum müssen wir sowohl die .hund .cpp-Dateien einschließen, während wir dafür sorgen können, dass es nur durch Einschließen der .cppDatei funktioniert ?

Beispiel: Erstellen einer file.henthaltenden Deklaration, anschließendes Erstellen einer file.cppenthaltenden Definition und Einbeziehen von beidem in main.cpp.

Alternativ: Erstellen einer file.cppenthaltenen 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.

erneuern
quelle
Das Teilen Ihrer Forschung hilft allen . Sagen Sie uns, was Sie versucht haben und warum es nicht Ihren Bedürfnissen entsprach. Dies zeigt, dass Sie sich die Zeit genommen haben, um sich selbst zu helfen, es erspart uns, offensichtliche Antworten zu wiederholen, und vor allem hilft es Ihnen, eine spezifischere und relevantere Antwort zu erhalten. Siehe auch Wie man fragt
Mücke
Ich empfehle dringend, die entsprechenden Fragen hier auf der Seite zu betrachten. programmers.stackexchange.com/questions/56215/… und programmers.stackexchange.com/questions/115368/… lesen sich gut
Warum ist das bei Programmierern und nicht bei SO?
Leichtigkeit Rennen mit Monica
@LightnessRacesInOrbit, weil es eine Frage des Codierungsstils und nicht der Vorgehensweise ist.
CashCow
@CashCow: Klingt so, als würdest du SO falsch verstehen, was keine "How to" -Seite ist . Es hört sich auch so an, als ob Sie diese Frage missverstehen, bei der es nicht um den Codierungsstil geht.
Leichtigkeit Rennen mit Monica

Antworten:

29

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 .cppDatei 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 ( .cppDatei) 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.cppund Main.cppmüssen mit einbezogen werden Foo.h. Foo.cppbenötigt es, weil es den Code definiert, der die Klassenschnittstelle unterstützt. Daher muss es wissen, was diese Schnittstelle ist. Main.cppbenö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.odaraus Foo.cppden gesamten Foo-Klassencode in kompilierter Form. Es generiert Main.oauch die Hauptmethode und nicht aufgelöste Verweise auf die Klasse Foo.

Jetzt kommt der Linker, der die beiden Objektdateien kombiniert Foo.ound Main.oin eine ausführbare Datei. Es sieht die ungelösten Foo-Referenzen in Main.o, sieht aber, dass diese Foo.odie notwendigen Symbole enthalten, so dass es sozusagen "die Punkte verbindet". Ein Funktionsaufruf Main.oist nun mit der tatsächlichen Position des kompilierten Codes verbunden, sodass das Programm zur Laufzeit zur richtigen Position springen kann.

Wenn Sie die Foo.cppDatei Main.cppeingefü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.cppaber warum ist es dann in einer separaten .cppDatei?)

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 .cDatei ohne Compiler-Optionen und es wird C angenommen, während eine .ccoder eine .cppErweiterung es veranlassen würde, C ++ anzunehmen. Ich kann einem Compiler jedoch leicht anweisen, eine .hoder sogar eine .docxDatei 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 ich Foo.hund sehe Foo.cpp, gehe ich sofort davon aus, dass die erste die Deklaration der Klasse und die zweite die Definition enthält.


quelle
Ich verstehe das Argument nicht, das Sie hier vorbringen. Wenn Sie gerade sind Foo.cppin Main.cppSie nicht brauchen , um die enthalten .hDatei, 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 ist
Benjamin Gruenbaum
2
Legen Sie für ein einfaches Programm einfach alles in einer Datei ab. Fügen Sie nicht einmal Projekt-Header ein (nur Bibliotheks-Header). Das Einbeziehen von Quelldateien ist eine schlechte Angewohnheit, die Probleme für alle anderen als die kleinsten Programme verursacht, sobald Sie mehrere Kompilierungseinheiten haben und diese tatsächlich separat kompilieren.
2
If you had included the Foo.cpp file in Main.cpp, there would be two definitions of class Foo.Wichtigster Satz zur Frage.
Marczellm
@Snowman Richtig, worauf sollten Sie sich meines Erachtens konzentrieren - auf mehrere Zusammenstellungseinheiten. Das Festhalten von Code mit / ohne Header-Dateien zum Aufteilen von Code auf kleinere Teile ist nützlich und orthogonal zum Zweck von Header-Dateien. Wenn Sie es nur in Dateien aufteilen möchten, haben Sie mit Header-Dateien nichts zu tun - wenn Sie dynamische Verknüpfungen, bedingte Verknüpfungen und fortgeschrittenere Builds benötigen, sind sie plötzlich sehr, sehr nützlich.
Benjamin Gruenbaum
Es gibt viele Möglichkeiten, den Quellcode für einen C / C ++ - Compiler / Linker zu organisieren. Der Fragesteller war sich des Prozesses nicht sicher, daher erklärte ich die bewährten Methoden zum Organisieren von Code und zur Funktionsweise des Prozesses. Sie können sich die Haare spalten, weil Code anders organisiert werden kann, aber die Tatsache bleibt, dass ich erklärt habe, wie 99% der Projekte dies tun, was aus einem bestimmten Grund die beste Vorgehensweise ist. Es funktioniert gut und erhält Ihre geistige Gesundheit.
9

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 Compiler cc1 oder integriert cc1plus). Lesen Sie insbesondere die Dokumentation des GNU- cppPrä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.hwenn sie enthält (wie durch Konventionen und Gewohnheiten vorgegeben ):

  • Makrodefinitionen
  • Typen - Definitionen (zB typedef, struct, classetc, ...)
  • Definitionen von static inlineFunktionen
  • Deklarationen von externen Funktionen.

Beachten Sie, dass es eine Frage der Konventionen (und der Zweckmäßigkeit) ist, diese in eine Header-Datei einzufügen.

Natürlich file.cppbenö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. gccoder g++) können Sie das -HFlag verwenden, um über jede Aufnahme informiert zu werden, und die -C -EFlags, 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 -Deinige 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 .

Basile Starynkevitch
quelle
1
Wenn die Links rot, wäre das noch eine gute Antwort?
Dan Pichelman
4
Ich habe meine Antwort bearbeitet, um sie zu verbessern, und ich wähle sorgfältig Links zu Wikipedia oder zur GNU-Dokumentation aus, von denen ich glaube, dass sie für lange Zeit relevant bleiben werden.
Basile Starynkevitch
3

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.

Leichtigkeit Rennen mit Monica
quelle
0

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:

  1. Trennung von Schnittstelle und Implementierung
  2. Verbesserte Kompilierungs- / Verknüpfungszeiten für große Programme

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.

Bill Door
quelle
0

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:

  • Alles in einem Header
  • alles innerhalb einer Kompilierungseinheit und überhaupt kein Header.

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.

Goldesel
quelle
-1

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.

SkaarjFace
quelle
1
Das ist es auch nicht. Das könnte genauso gelöst werden, wie es .hohnehin in Dateien gelöst wird - mit include guards und ist genau das gleiche Problem, das Sie mit .hDateien haben. Dafür sind .hDateien überhaupt nicht gedacht .
Benjamin Gruenbaum
Ich bin nicht sicher, wie Sie Include-Guards verwenden würden, da diese nur pro Datei funktionieren (wie Preprocessod). Da Main.cpp und Bar.cpp unterschiedliche Dateien sind, würde Foo.cpp in beiden nur einmal enthalten sein (geschützt oder nicht, macht keinen Unterschied). Main.cpp und Bar.cpp würden beide kompilieren. Aufgrund der doppelten Definition der Klasse Foo tritt jedoch weiterhin ein Verbindungsfehler auf. Es gibt jedoch auch Vererbung, die ich nicht einmal erwähnt habe. Deklaration für externe Module (DLLs) usw.
SkaarjFace
Nein, es wäre eine Zusammenstellungseinheit, es würde überhaupt keine Verknüpfung stattfinden, da Sie die .cppDatei einschließen .
Benjamin Gruenbaum
Ich habe nicht gesagt, es wird nicht kompiliert. Es werden jedoch nicht sowohl main.o als auch bar.o in derselben ausführbaren Datei verknüpft. Ohne zu verlinken, worum es überhaupt beim Kompilieren geht.
SkaarjFace
Ähh ... den Code zum Laufen bringen? Sie können es weiterhin verknüpfen, die Dateien jedoch nicht miteinander verknüpfen - es handelt sich vielmehr um dieselbe Zusammenstellungseinheit.
Benjamin Gruenbaum
-2

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.

Talha Junaid
quelle