Warum haben Header- und CPP-Dateien? [geschlossen]

484

Warum hat C ++ Header- und CPP-Dateien?

Peter Mortensen
quelle
3
Verwandte Frage: stackoverflow.com/questions/1945846/…
Spoike
Es ist ein allgemeines OOP-Paradigma. .h ist eine Klassendeklaration und cpp ist die Definition. Man muss nicht wissen, wie es implementiert ist, er / sie sollte nur die Schnittstelle kennen.
Manish Kakati
Dies ist der beste Teil von C ++, der die Schnittstelle von der Implementierung trennt. Es ist immer gut, anstatt den gesamten Code in einer einzigen Datei zu behalten. Wir haben die Schnittstelle getrennt. Es gibt immer eine gewisse Menge Code wie Inline-Funktionen, die Teil von Header-Dateien sind. Sieht gut aus, wenn eine Header-Datei angezeigt wird, in der die Liste der deklarierten Funktionen und Klassenvariablen angezeigt wird.
Miank
Es gibt Zeiten, in denen Header-Dateien für die Kompilierung unerlässlich sind - nicht nur eine Organisationspräferenz oder eine Möglichkeit, vorkompilierte Bibliotheken zu verteilen. Angenommen, Sie haben eine Struktur, in der game.c sowohl von physik.c als auch von math.c abhängt. Physik.c hängt auch von math.c. ab. Wenn Sie .c-Dateien einschließen und .h-Dateien für immer vergessen würden, hätten Sie doppelte Deklarationen von math.c und keine Hoffnung auf Kompilierung. Dies ist für mich am sinnvollsten, warum Header-Dateien wichtig sind. Hoffe es hilft jemand anderem.
Samy Bencherif
Ich denke, das hat damit zu tun, dass in Erweiterungen nur alphanumerische Zeichen erlaubt sind. Ich weiß nicht einmal, ob das wahr ist, nur raten
user12211554

Antworten:

201

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.

MadKeithV
quelle
178
Nicht wirklich wahr. Der Header enthält noch einen Großteil der Implementierung. Seit wann waren private Instanzvariablen Teil der Schnittstelle einer Klasse? Private Mitgliederfunktionen? Was zum Teufel machen sie dann im öffentlich sichtbaren Header? Und es fällt mit Vorlagen weiter auseinander.
Jalf
13
Deshalb habe ich gesagt, dass es nicht perfekt ist und die Pimpl-Sprache für mehr Trennung benötigt wird. Vorlagen sind eine ganz andere Dose Würmer - selbst wenn das Schlüsselwort "Exporte" in den meisten Compilern vollständig unterstützt würde, würde es mir eher syntaktischen Zucker als echte Trennung geben.
Joris Timmermans
4
Wie gehen andere Sprachen damit um? zum Beispiel - Java? In Java gibt es kein Header-Dateikonzept.
Lazer
8
@Lazer: Java ist einfacher zu analysieren. Der Java-Compiler kann eine Datei analysieren, ohne alle Klassen in anderen Dateien zu kennen, und die Typen später überprüfen. In C ++ sind viele Konstrukte ohne Typinformationen mehrdeutig, sodass der C ++ - Compiler Informationen zu referenzierten Typen benötigt, um eine Datei zu analysieren. Deshalb braucht es Header.
Niki
15
@nikie: Was hat "Leichtigkeit" des Parsens damit zu tun? Wenn Java eine Grammatik hätte, die mindestens so komplex wie C ++ ist, könnte es immer noch nur Java-Dateien verwenden. Was ist in beiden Fällen mit C? C ist einfach zu analysieren, verwendet jedoch sowohl Header- als auch C-Dateien.
Thomas Eding
609

C ++ - Kompilierung

Eine Kompilierung in C ++ erfolgt in 2 Hauptphasen:

  1. 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.

  2. 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?

Eine arme einsame CPP-Datei ...

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 ...

COPY / PASTE ALERT!

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
}

Wie funktioniert das 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.
}

Eine kleine Sache - warum B.HPP in B.CPP aufnehmen?

Im aktuellen Fall ist dies nicht erforderlich, und B.HPP hat die doSomethingElseFunktionsdeklaration 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.

Fazit

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
paercebal
quelle
2
@nimcap :: 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.
Paercebal
7
@ Bob : 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 fooes 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.
Paercebal
3
@ Bob: [Fortsetzung] ... Nun könnte der Linker die Arbeit des Compilers erledigen, denke ich, was dann Ihre Option ermöglichen würde. (Ich denke, dies ist das Thema des Vorschlags "Module" für den nächsten Standard). 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).
Paercebal
1
@paercebal Danke für Erklärung und Hinweise, paercebal! Warum kann ich nicht sicher sein, dass dies 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?) :)
Boris Burkov
1
@Bobo :: Why can't I be sure, that foo(bar) is a functionfoo 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?
Paercebal
93

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.

jalf
quelle
3
Ich frage mich, warum Header-Dateien (oder was auch immer tatsächlich zum Kompilieren / Verknüpfen benötigt wurde) nicht einfach "automatisch generiert" wurden.
Mateen Ulhaq
54

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.

entspannen
quelle
16

Weil C ++ sie von C geerbt hat. Leider.

andref
quelle
4
Warum ist die Vererbung von C ++ von C unglücklich?
Lokesh
3
@Lokesh Wegen seines Gepäcks :(
陳 力
1
Wie kann das eine Antwort sein?
Shuvo Sarker
14
@ShuvoSarker, da es, wie Tausende von Sprachen gezeigt haben, keine technische Erklärung für C ++ gibt, die Programmierer dazu bringt, Funktionssignaturen zweimal zu schreiben. Die Antwort auf "warum?" ist "Geschichte".
Boris
15

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).

Aaron Digulla
quelle
Würde nicht funktionieren, zyklische Referenzen. Sie können a.lib nicht aus a.cpp erstellen, bevor Sie b.lib aus b.cpp erstellen, aber Sie können b.lib auch nicht vor a.lib erstellen.
MSalters
20
Java hat das gelöst, Python kann es, jede moderne Sprache kann es. Aber zu der Zeit, als C erfunden wurde, war RAM so teuer und knapp, dass es einfach keine Option war.
Aaron Digulla
6

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.

Martin v. Löwis
quelle
4

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:

  • Klarheit, dh wenn die Schnittstellen von der Implementierung getrennt bleiben, ist es einfacher, den Code zu lesen
  • Kompilierzeit. Wenn möglichst nur die Schnittstelle anstelle der vollständigen Implementierung verwendet wird, kann die Kompilierungszeit reduziert werden, da der Compiler einfach auf die Schnittstelle verweisen kann, anstatt den eigentlichen Code analysieren zu müssen (was idealerweise nur erforderlich wäre ein einziges Mal).
user21037
quelle
3
Warum konnten Bibliotheksanbieter nicht einfach eine generierte "Header" -Datei versenden? Eine vorprozessorfreie "Header" -Datei sollte eine viel bessere Leistung bieten (es sei denn, die Implementierung war wirklich fehlerhaft).
Tom Hawtin - Tackline
Ich denke, es ist irrelevant, ob die Header-Datei generiert oder von Hand geschrieben wurde. Die Frage war nicht "Warum schreiben die Leute Header-Dateien selbst?", Sondern "Warum haben wir Header-Dateien?". Gleiches gilt für preprozessorfreie Header. Klar, das wäre schneller.
-5

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 ...

Alex v
quelle