Wie kann eine C ++ - Headerdatei die Implementierung enthalten?

76

Ok, keineswegs ein C / C ++ - Experte, aber ich dachte, der Sinn einer Header-Datei besteht darin, die Funktionen zu deklarieren, und dann sollte die C / CPP-Datei die Implementierung definieren.

Als ich heute Abend C ++ - Code überprüfte, fand ich dies in der Header-Datei einer Klasse ...

public:
    UInt32 GetNumberChannels() const { return _numberChannels; } // <-- Huh??

private:
    UInt32 _numberChannels;

Warum gibt es eine Implementierung in einem Header? Hat es mit dem constSchlüsselwort zu tun ? Inline das eine Klassenmethode? Was genau ist der Vorteil / Sinn dieser Vorgehensweise gegenüber der Definition der Implementierung in der CPP-Datei?

Mark A. Donohoe
quelle
6
Die Funktion ist inline .
Einige Programmierer Typ
1
RE das constQualifikationsmerkmal; Dies bedeutet nur, dass die Methode den Status des Objekts nicht ändert.
Michael
4
@Alex: Sie sind falsch, dass der Compiler die Funktion inline muss. Der Compiler / Linker muss sich mit den mehreren Definitionen befassen (Inline-Funktionen unterliegen nicht der Ein-Definitions-Regel).
Michael Burr
3
@Alex nein der Compiler muss es nicht einbinden. Es kann in einigen Übersetzungen inline inline sein, muss dies aber nicht in allen TUs tun. Ja, es gibt mehrere Definitionen, aber da die Funktion (implizit) inline deklariert ist, markiert der Compiler das Symbol, wenn es nicht inline ist, und der Linker weiß, dass er nur eines der exportierten Symbole auswählen muss. Dies gilt auch für Vorlageninstanziierungen.
Arne Mertz
1
VC2010 wird eine solche Funktion nicht inline, wenn beispielsweise sein magisches "Inline-Budget" erschöpft ist.
ActiveTrayPrntrTagDataStrDrvr

Antworten:

135

Ok, keineswegs ein C / C ++ - Experte, aber ich dachte, der Sinn einer Header-Datei besteht darin, die Funktionen zu deklarieren, und dann sollte die C / CPP-Datei die Implementierung definieren.

Der wahre Zweck einer Header-Datei besteht darin, Code für mehrere Quelldateien freizugeben. Es wird häufig verwendet, um Deklarationen von Implementierungen zu trennen, um die Codeverwaltung zu verbessern. Dies ist jedoch keine Voraussetzung. Es ist möglich, Code zu schreiben, der nicht auf Header-Dateien basiert, und es ist möglich, Code zu schreiben, der nur aus Header-Dateien besteht (die STL- und Boost-Bibliotheken sind gute Beispiele dafür). Denken Sie daran, wenn der Präprozessor auf eine #includeAnweisung stößt , ersetzt er die Anweisung durch den Inhalt der Datei, auf die verwiesen wird, und der Compiler sieht nur den vollständigen vorverarbeiteten Code.

Zum Beispiel, wenn Sie die folgenden Dateien haben:

Foo.h:

#ifndef FooH
#define FooH

class Foo
{
public:
    UInt32 GetNumberChannels() const;

private:
    UInt32 _numberChannels;
};

#endif

Foo.cpp:

#include "Foo.h"

UInt32 Foo::GetNumberChannels() const
{
    return _numberChannels;
}

Bar.cpp:

#include "Foo.h"

Foo f;
UInt32 chans = f.GetNumberChannels();

Der Präprozessor analysiert Foo.cpp und Bar.cpp getrennt und erzeugt den folgenden Code, den der Compiler dann analysiert:

Foo.cpp:

class Foo
{
public:
    UInt32 GetNumberChannels() const;

private:
    UInt32 _numberChannels;
};

UInt32 Foo::GetNumberChannels() const
{
    return _numberChannels;
}

Bar.cpp:

class Foo
{
public:
    UInt32 GetNumberChannels() const;

private:
    UInt32 _numberChannels;
};

Foo f;
UInt32 chans = f.GetNumberChannels();

Bar.cpp wird in Bar.obj kompiliert und enthält einen Verweis zum Aufrufen Foo::GetNumberChannels(). Foo.cpp wird in Foo.obj kompiliert und enthält die eigentliche Implementierung von Foo::GetNumberChannels(). Nach dem Kompilieren vergleicht der Linker die OBJ-Dateien und verknüpft sie miteinander, um die endgültige ausführbare Datei zu erstellen.

Warum gibt es eine Implementierung in einem Header?

Durch die Aufnahme der Methodenimplementierung in die Methodendeklaration wird sie implizit als inline deklariert (es gibt ein tatsächliches inlineSchlüsselwort, das auch explizit verwendet werden kann). Die Angabe, dass der Compiler eine Funktion einbinden soll, ist nur ein Hinweis, der nicht garantiert, dass die Funktion tatsächlich inline wird. Wenn dies jedoch der Fall ist, wird der Inhalt der Funktion überall dort, wo die Inline-Funktion aufgerufen wird, direkt in die Aufrufstelle kopiert, anstatt eine CALLAnweisung zu generieren , um in die Funktion zu springen und beim Beenden zum Aufrufer zurückzukehren. Der Compiler kann dann den umgebenden Code berücksichtigen und den kopierten Code nach Möglichkeit weiter optimieren. 

Hat es mit dem Schlüsselwort const zu tun?

Nein. Das constSchlüsselwort zeigt dem Compiler lediglich an, dass die Methode den Status des Objekts, das zur Laufzeit aufgerufen wird, nicht ändert.

Was genau ist der Vorteil / Sinn dieser Vorgehensweise gegenüber der Definition der Implementierung in der CPP-Datei?

Bei effektiver Verwendung kann der Compiler normalerweise schnelleren und besser optimierten Maschinencode erstellen.

Remy Lebeau
quelle
2
Bedeutet dies Ihrer Erklärung nach, dass Sie eine Klasse direkt in Ihrer CPP-Datei deklarieren und sogar den Inhalt der Elementfunktionen in den Klammern deklarieren können, die diese Klassendeklaration umschließen, sodass Sie die :: -Syntax nicht außerhalb verwenden müssen davon? (Ich verstehe, dass dies keine gute Codierung ist. Ich frage nur, ob es sich um eine gültige Codierung handelt.) Und würde dies in diesem Sinne bedeuten, dass alle Mitglieder inline oder zumindest als solche markiert wären? (Und gibt es etwas, das Sie sagen können, nicht inline?)
Mark A. Donohoe
@MarqueIV Was Sie beschreiben, ist technisch möglich, aber es würde Sie daran hindern, diese Klasse irgendwo außerhalb der durch die CPP-Datei definierten Kompilierungseinheit zu verwenden (im Wesentlichen die CPP-Datei selbst, es sei denn, Sie schließen sie in andere Dateien ein riesiges Nein-Nein!). Sie können jedoch weiterhin Zeiger oder Verweise auf die Klasse verwenden (nur niemals dereferenzieren oder über die Zeiger oder Verweise auf Mitglieder zugreifen), wenn Sie die Klasse in anderen Dateien vorwärts deklarieren.
Agentlien
Ja, ich weiß, dass ich sie nicht so benutzen konnte. Dies war eher eine Art "Kannst du es tun", nicht eine "Solltest du es tun". Ich habe eher gefragt, ob es erfolgreich kompiliert werden würde, was es auch tun würde. Wie auch immer, Sie haben die Antwort aufgrund des Details erhalten und sind auch die einzige Person, die das const-Schlüsselwort in Ihrer Antwort verweist (zumindest, als ich es trotzdem genehmigt habe). Danke! :)
Mark A. Donohoe
Der Header Guard im Beispiel scheint nichts zu tun. Können Sie erklären, wann der Header Guard wirksam wird und wann nicht?
Helin Wang
@RemyLebeau Danke! Eine andere Frage, ob sich die Implementierung in einer Header-Datei mit Header Guard befindet. Und diese Header-Datei wurde von einem gemeinsam genutzten Bibliotheksprojekt und einem Hauptprojekt verwendet. Das Hauptprojekt verwendet das Bibliotheksprojekt. Beschwert sich der Linker während der Verknüpfung darüber, dass dieselbe Funktion zweimal definiert wurde (doppeltes Symbol)?
Helin Wang
35

Es ist vollkommen gültig, eine Implementierung einer Funktion in einer Header-Datei zu haben. Das einzige Problem dabei ist das Brechen der Ein-Definitions-Regel. Das heißt, wenn Sie den Header aus mehreren anderen Dateien einfügen, wird ein Compilerfehler angezeigt.

Es gibt jedoch eine Ausnahme. Wenn Sie eine Funktion als Inline deklarieren, ist sie von der Ein-Definitions-Regel ausgenommen. Dies geschieht hier, da in einer Klassendefinition definierte Elementfunktionen implizit inline sind.

Inline selbst ist ein Hinweis für den Compiler, dass eine Funktion ein guter Kandidat für Inlining sein kann. Das heißt, jeder Aufruf wird in die Definition der Funktion erweitert und nicht in einen einfachen Funktionsaufruf. Dies ist eine Optimierung, bei der die Größe der generierten Datei gegen schnelleren Code eingetauscht wird. In modernen Compilern wird das Bereitstellen dieses Inlining-Hinweises für eine Funktion größtenteils ignoriert, mit Ausnahme der Auswirkungen, die es auf die Ein-Definitions-Regel hat. Außerdem kann ein Compiler jede Funktion, die er für richtig hält, jederzeit einbinden, auch wenn sie nicht inline(explizit oder implizit) deklariert wurde .

In Ihrem Beispiel constsignalisiert die Verwendung von nach der Argumentliste, dass die Elementfunktion das Objekt, für das sie aufgerufen wird, nicht ändert. In der Praxis bedeutet dies, dass das Objekt, auf das thisalle Klassenmitglieder und damit auch alle Klassenmitglieder zeigen, berücksichtigt wird const. Das heißt, wenn Sie versuchen, sie zu ändern, wird ein Fehler bei der Kompilierung generiert.

Agentlien
quelle
2
"Da in einer Klassendefinition definierte Elementfunktionen implizit inline sind." Wertvolle Informationen. Wusste ich nicht. Aber was ist mit diesem constWort?
Mark A. Donohoe
5
Vielen Dank für die Erwähnung der One-Definition-Regel !
Dubbaluga
6

Es wird implizit als inline Mitgliedsfunktion deklariert, die in der Klassendeklaration definiert ist . Dies bedeutet nicht, dass der Compiler es einbinden muss, aber es bedeutet, dass Sie die eine Definitionsregel nicht brechen werden . Es hat nichts mit const* zu tun . Es hängt auch nicht mit der Länge und Komplexität der Funktion zusammen.

Wenn es sich um eine Nichtmitgliedsfunktion handeln würde, müssten Sie sie explizit deklarieren als inline:

inline void foo() { std::cout << "foo!\n"; }

* Weitere Informationen finden Sie hier am constEnde einer Mitgliedsfunktion.

Juanchopanza
quelle
Meinen Sie hier mit einer Definitionsregel, dass eine Funktion, wenn sie im Header definiert ist, möglicherweise nicht in einer anderen CPP-Datei definiert werden kann?
Ashu
3

Selbst in einfachem C ist es möglich, Code in eine Header-Datei einzufügen. Wenn Sie dies tun, müssen Sie es normalerweise deklarieren, da staticsonst mehrere .c-Dateien mit demselben Header einen Fehler "Mehrfach definierte Funktion" verursachen.

Der Präprozessor enthält textuell eine Include-Datei, sodass der Code in einer Include-Datei Teil der Quelldatei wird (zumindest aus Sicht des Compilers).

Die Entwickler von C ++ wollten eine objektorientierte Programmierung mit gutem Verstecken von Daten ermöglichen, daher erwarteten sie viele Getter- und Setter-Funktionen. Sie wollten keine unvernünftige Leistungsstrafe. Daher haben sie C ++ so konzipiert, dass die Getter und Setter nicht nur im Header deklariert, sondern auch tatsächlich implementiert werden können, sodass sie inline sind. Diese Funktion, die Sie gezeigt haben, ist ein Getter, und wenn dieser C ++ - Code kompiliert wird, gibt es keinen Funktionsaufruf. Code zum Abrufen dieses Werts wird nur an Ort und Stelle kompiliert.

Es ist möglich, eine Computersprache zu erstellen, die nicht die Unterscheidung zwischen Header- und Quelldatei aufweist, sondern nur tatsächliche "Module", die der Compiler versteht. (C ++ hat das nicht getan; sie haben nur auf dem erfolgreichen C-Modell von Quelldateien und textuell enthaltenen Header-Dateien aufgebaut.) Wenn Quelldateien Module sind, kann ein Compiler Code aus dem Modul ziehen und dann Inline diesen Code. Die Implementierung von C ++ ist jedoch einfacher.

steveha
quelle
1

Soweit ich weiß, gibt es zwei Arten von Methoden, die sicher in der Header-Datei implementiert werden können.

  • Inline-Methoden - Ihre Implementierung wird an die Stellen kopiert, an denen sie verwendet werden, sodass es kein Problem mit Linkerfehlern mit doppelter Definition gibt.
  • Vorlagenmethoden - Sie werden tatsächlich zum Zeitpunkt der Vorlageninstanziierung kompiliert (z. B. wenn jemand einen Typ anstelle der Vorlage eingibt), sodass auch hier keine Möglichkeit eines Problems mit doppelter Definition besteht.

Ich glaube, Ihr Beispiel passt zum ersten Fall.

Spuk
quelle
0

Die Implementierung in der Klassenheaderdatei beizubehalten funktioniert, da Sie sicher wissen, ob Sie Ihren Code kompiliert haben. Das constSchlüsselwort stellt sicher, dass Sie keine Mitglieder ändern. Es hält die Instanz für die Dauer des Methodenaufrufs unveränderlich .

Jonas Byström
quelle
0

C ++ - Standardzitate

Der C ++ 17 N4659-Standardentwurf 10.1.6 "Der Inline-Spezifizierer" besagt, dass Methoden implizit inline sind:

4 Eine in einer Klassendefinition definierte Funktion ist eine Inline-Funktion.

und dann weiter unten sehen wir, dass Inline-Methoden nicht nur für alle Übersetzungseinheiten definiert werden können, sondern müssen :

6 Eine Inline-Funktion oder -Variable muss in jeder Übersetzungseinheit definiert sein, in der sie verwendet wird, und muss in jedem Fall genau dieselbe Definition haben (6.2).

Dies wird auch ausdrücklich in einem Hinweis unter 12.2.1 "Mitgliedsfunktionen" erwähnt:

1 Eine Mitgliedsfunktion kann in ihrer Klassendefinition definiert sein (11.4). In diesem Fall handelt es sich um eine Inline-Mitgliedsfunktion (10.1.6) [...]

3 [Hinweis: Es kann höchstens eine Definition einer Nicht-Inline-Elementfunktion in einem Programm geben. In einem Programm kann es mehr als eine Inline-Elementfunktionsdefinition geben. Siehe 6.2 und 10.1.6. - Endnote]

Implementierung von GCC 8.3

main.cpp

struct MyClass {
    void myMethod() {}
};

int main() {
    MyClass().myMethod();
}

Symbole kompilieren und anzeigen:

g++ -c main.cpp
nm -C main.o

Ausgabe:

                 U _GLOBAL_OFFSET_TABLE_
0000000000000000 W MyClass::myMethod()
                 U __stack_chk_fail
0000000000000000 T main

dann sehen wir, man nmdass das MyClass::myMethodSymbol in den ELF-Objektdateien als schwach markiert ist, was impliziert, dass es in mehreren Objektdateien erscheinen kann:

"W" "w" Das Symbol ist ein schwaches Symbol, das nicht speziell als schwaches Objektsymbol gekennzeichnet wurde. Wenn ein schwach definiertes Symbol mit einem normal definierten Symbol verknüpft ist, wird das normal definierte Symbol fehlerfrei verwendet. Wenn ein schwaches undefiniertes Symbol verknüpft ist und das Symbol nicht definiert ist, wird der Wert des Symbols systemspezifisch fehlerfrei ermittelt. Auf einigen Systemen gibt Großbuchstaben an, dass ein Standardwert angegeben wurde.

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
quelle