Warum hat C ++ Header- und CPP-Dateien?
Warum hat C ++ Header- und CPP-Dateien?
Antworten:
Nun, der Hauptgrund wäre, die Schnittstelle von der Implementierung zu trennen. Der Header gibt an, "was" eine Klasse (oder was auch immer implementiert wird) tun wird, während die cpp-Datei definiert, "wie" sie diese Funktionen ausführt.
Dies reduziert Abhängigkeiten, sodass Code, der den Header verwendet, nicht unbedingt alle Details der Implementierung und alle anderen Klassen / Header kennen muss, die nur dafür benötigt werden. Dies reduziert die Kompilierungszeiten und auch den Umfang der Neukompilierung, die erforderlich ist, wenn sich etwas in der Implementierung ändert.
Es ist nicht perfekt, und Sie würden normalerweise auf Techniken wie das Pimpl Idiom zurückgreifen, um Schnittstelle und Implementierung richtig zu trennen, aber es ist ein guter Anfang.
Eine Kompilierung in C ++ erfolgt in 2 Hauptphasen:
Die erste ist die Kompilierung von "Quell" -Textdateien in binäre "Objekt" -Dateien: Die CPP-Datei ist die kompilierte Datei und wird ohne Kenntnis der anderen CPP-Dateien (oder sogar Bibliotheken) kompiliert, sofern sie nicht über eine Rohdeklaration oder übermittelt wird Header-Aufnahme. Die CPP-Datei wird normalerweise in eine OBJ- oder eine O-Objektdatei kompiliert.
Die zweite ist die Verknüpfung aller "Objekt" -Dateien und damit die Erstellung der endgültigen Binärdatei (entweder eine Bibliothek oder eine ausführbare Datei).
Wo passt das HPP in all diesen Prozess?
Die Kompilierung jeder CPP-Datei ist unabhängig von allen anderen CPP-Dateien. Wenn A.CPP ein in B.CPP definiertes Symbol benötigt, wie z.
// A.CPP
void doSomething()
{
doSomethingElse(); // Defined in B.CPP
}
// B.CPP
void doSomethingElse()
{
// Etc.
}
Es wird nicht kompiliert, da A.CPP nicht wissen kann, dass "doSomethingElse" vorhanden ist ... Es sei denn, A.CPP enthält eine Deklaration wie:
// A.CPP
void doSomethingElse() ; // From B.CPP
void doSomething()
{
doSomethingElse() ; // Defined in B.CPP
}
Wenn Sie dann C.CPP haben, das dasselbe Symbol verwendet, kopieren Sie die Deklaration / fügen Sie sie ein ...
Ja, es gibt ein Problem. Kopien / Pasten sind gefährlich und schwer zu pflegen. Was bedeutet, dass es cool wäre, wenn wir eine Möglichkeit hätten, NICHT zu kopieren / einzufügen und trotzdem das Symbol zu deklarieren ... Wie können wir das tun? Durch das Einschließen einer Textdatei, die üblicherweise mit .h, .hxx, .h ++ oder, für C ++ - Dateien bevorzugt, mit .hpp versehen wird:
// B.HPP (here, we decided to declare every symbol defined in B.CPP)
void doSomethingElse() ;
// A.CPP
#include "B.HPP"
void doSomething()
{
doSomethingElse() ; // Defined in B.CPP
}
// B.CPP
#include "B.HPP"
void doSomethingElse()
{
// Etc.
}
// C.CPP
#include "B.HPP"
void doSomethingAgain()
{
doSomethingElse() ; // Defined in B.CPP
}
include
?Durch das Einfügen einer Datei wird der Inhalt im Wesentlichen analysiert und anschließend kopiert und in die CPP-Datei eingefügt.
Zum Beispiel im folgenden Code mit dem A.HPP-Header:
// A.HPP
void someFunction();
void someOtherFunction();
... die Quelle B.CPP:
// B.CPP
#include "A.HPP"
void doSomething()
{
// Etc.
}
... wird nach Aufnahme:
// B.CPP
void someFunction();
void someOtherFunction();
void doSomething()
{
// Etc.
}
Im aktuellen Fall ist dies nicht erforderlich, und B.HPP hat die doSomethingElse
Funktionsdeklaration und B.CPP hat diedoSomethingElse
Funktionsdefinition (die für sich genommen eine Deklaration ist). In einem allgemeineren Fall, in dem B.HPP für Deklarationen (und Inline-Code) verwendet wird, kann es jedoch keine entsprechende Definition geben (z. B. Aufzählungen, einfache Strukturen usw.), sodass das Include erforderlich sein kann, wenn B.CPP verwendet diese Erklärung von B.HPP. Alles in allem ist es "guter Geschmack", wenn eine Quelle standardmäßig ihren Header enthält.
Die Header-Datei ist daher erforderlich, da der C ++ - Compiler nicht allein nach Symboldeklarationen suchen kann. Sie müssen daher helfen, indem Sie diese Deklarationen einschließen.
Ein letztes Wort: Sie sollten den Inhalt Ihrer HPP-Dateien mit Header-Schutzvorrichtungen versehen, um sicherzustellen, dass mehrere Einschlüsse nichts beschädigen. Insgesamt glaube ich jedoch, dass der Hauptgrund für die Existenz von HPP-Dateien oben erläutert wurde.
#ifndef B_HPP_
#define B_HPP_
// The declarations in the B.hpp file
#endif // B_HPP_
oder noch einfacher
#pragma once
// The declarations in the B.hpp file
You still have to copy paste the signature from header file to cpp file, don't you?
Keine Notwendigkeit. Solange der CPP den HPP "enthält", führt der Precompiler automatisch das Kopieren und Einfügen des Inhalts der HPP-Datei in die CPP-Datei durch. Ich habe die Antwort aktualisiert, um dies zu verdeutlichen.
While compiling A.cpp, compiler knows the types of arguments and return value of doSomethingElse from the call itself
. Nein, das tut es nicht. Es sind nur die vom Benutzer bereitgestellten Typen bekannt, die sich in der Hälfte der Zeit nicht einmal die Mühe machen, den Rückgabewert zu lesen. Dann finden implizite Konvertierungen statt. Und dann, wenn Sie den Code foo(bar)
haben:, können Sie nicht einmal sicher sein, ob foo
es sich um eine Funktion handelt. Der Compiler muss also Zugriff auf die Informationen in den Headern haben, um zu entscheiden, ob die Quelle korrekt kompiliert wird oder nicht ... Sobald der Code kompiliert ist, verknüpft der Linker einfach Funktionsaufrufe.
Seems, they're just a pretty ugly arbitrary design.
: Wenn C ++ tatsächlich 2012 erstellt worden wäre. Denken Sie jedoch daran, dass C ++ in den 1980er Jahren auf C aufgebaut war und zu dieser Zeit die Einschränkungen sehr unterschiedlich waren (IIRC, es wurde zu Adoptionszwecken beschlossen, dieselben Linker als Cs beizubehalten).
foo(bar)
eine Funktion ist - wenn sie als Zeiger erhalten wird? Wenn ich von schlechtem Design spreche, beschuldige ich C, nicht C ++. Ich mag einige Einschränkungen von reinem C wirklich nicht, wie z. B. Header-Dateien oder Funktionen, die nur einen Wert zurückgeben, während mehrere Argumente für die Eingabe verwendet werden (fühlt es sich nicht natürlich an, wenn sich Eingabe und Ausgabe ähnlich verhalten ; warum mehrere Argumente, aber einzelne Ausgabe?) :)
Why can't I be sure, that foo(bar) is a function
foo könnte ein Typ sein, also hätten Sie einen Klassenkonstruktor namens. In fact, speaking of bad design, I blame C, not C++
: Ich kann C für viele Dinge verantwortlich machen, aber in den 70ern entworfen worden zu sein, wird nicht einer von ihnen sein. Wiederum Einschränkungen dieser Zeit ... such as having header files or having functions return one and only one value
: Tupel können dazu beitragen, dies zu mildern und Argumente als Referenz zu übergeben. Wie würde nun die Syntax zum Abrufen mehrerer zurückgegebener Werte lauten, und würde es sich lohnen, die Sprache zu ändern?
Da C, aus dem das Konzept stammt, 30 Jahre alt ist, war es damals die einzig praktikable Möglichkeit, Code aus mehreren Dateien miteinander zu verknüpfen.
Heute ist es ein schrecklicher Hack, der die Kompilierungszeit in C ++ völlig zerstört, unzählige unnötige Abhängigkeiten verursacht (weil Klassendefinitionen in einer Header-Datei zu viele Informationen über die Implementierung enthalten) und so weiter.
Da in C ++ der endgültige ausführbare Code keine Symbolinformationen enthält, handelt es sich um mehr oder weniger reinen Maschinencode.
Daher benötigen Sie eine Möglichkeit, die Schnittstelle eines Codeteils zu beschreiben, die vom Code selbst getrennt ist. Diese Beschreibung befindet sich in der Header-Datei.
Weil C ++ sie von C geerbt hat. Leider.
Weil die Leute, die das Bibliotheksformat entworfen haben, keinen Platz für selten verwendete Informationen wie C-Präprozessor-Makros und Funktionsdeklarationen "verschwenden" wollten.
Da Sie diese Informationen benötigen, um Ihrem Compiler mitzuteilen, dass diese Funktion später verfügbar ist, wenn der Linker seine Arbeit erledigt, mussten sie eine zweite Datei erstellen, in der diese freigegebenen Informationen gespeichert werden können.
Die meisten Sprachen nach C / C ++ speichern diese Informationen in der Ausgabe (z. B. Java-Bytecode) oder verwenden überhaupt kein vorkompiliertes Format, werden immer in Quellform verteilt und im laufenden Betrieb kompiliert (Python, Perl).
Dies ist die Präprozessor-Methode zum Deklarieren von Schnittstellen. Sie fügen die Schnittstelle (Methodendeklarationen) in die Header-Datei und die Implementierung in die cpp ein. Anwendungen, die Ihre Bibliothek verwenden, müssen nur die Schnittstelle kennen, auf die sie über #include zugreifen können.
Oft möchten Sie eine Definition einer Schnittstelle haben, ohne den gesamten Code versenden zu müssen. Wenn Sie beispielsweise über eine gemeinsam genutzte Bibliothek verfügen, senden Sie eine Header-Datei mit, in der alle in der gemeinsam genutzten Bibliothek verwendeten Funktionen und Symbole definiert sind. Ohne Header-Dateien müssten Sie die Quelle versenden.
Innerhalb eines einzelnen Projekts werden Header-Dateien, IMHO, für mindestens zwei Zwecke verwendet:
Auf die Antwort von MadKeithV antworten ,
Dies reduziert Abhängigkeiten, sodass Code, der den Header verwendet, nicht unbedingt alle Details der Implementierung und alle anderen Klassen / Header kennen muss, die nur dafür benötigt werden. Dies reduziert die Kompilierungszeiten und auch den Umfang der Neukompilierung, die erforderlich ist, wenn sich etwas in der Implementierung ändert.
Ein weiterer Grund ist, dass ein Header jeder Klasse eine eindeutige ID gibt.
Also wenn wir so etwas haben
class A {..};
class B : public A {...};
class C {
include A.cpp;
include B.cpp;
.....
};
Wir werden Fehler haben, wenn wir versuchen, das Projekt zu erstellen, da A Teil von B ist. Mit Headern würden wir diese Art von Kopfschmerzen vermeiden ...