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 const
Schlü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?
c++
header-files
Mark A. Donohoe
quelle
quelle
const
Qualifikationsmerkmal; Dies bedeutet nur, dass die Methode den Status des Objekts nicht ändert.Antworten:
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
#include
Anweisung 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 vonFoo::GetNumberChannels()
. Nach dem Kompilieren vergleicht der Linker die OBJ-Dateien und verknüpft sie miteinander, um die endgültige ausführbare Datei zu erstellen.Durch die Aufnahme der Methodenimplementierung in die Methodendeklaration wird sie implizit als inline deklariert (es gibt ein tatsächliches
inline
Schlü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 eineCALL
Anweisung 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.Nein. Das
const
Schlüsselwort zeigt dem Compiler lediglich an, dass die Methode den Status des Objekts, das zur Laufzeit aufgerufen wird, nicht ändert.Bei effektiver Verwendung kann der Compiler normalerweise schnelleren und besser optimierten Maschinencode erstellen.
quelle
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
const
signalisiert 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 dasthis
alle Klassenmitglieder und damit auch alle Klassenmitglieder zeigen, berücksichtigt wirdconst
. Das heißt, wenn Sie versuchen, sie zu ändern, wird ein Fehler bei der Kompilierung generiert.quelle
const
Wort?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 mitconst
* 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
const
Ende einer Mitgliedsfunktion.quelle
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
static
sonst 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.
quelle
Soweit ich weiß, gibt es zwei Arten von Methoden, die sicher in der Header-Datei implementiert werden können.
Ich glaube, Ihr Beispiel passt zum ersten Fall.
quelle
Die Implementierung in der Klassenheaderdatei beizubehalten funktioniert, da Sie sicher wissen, ob Sie Ihren Code kompiliert haben. Das
const
Schlüsselwort stellt sicher, dass Sie keine Mitglieder ändern. Es hält die Instanz für die Dauer des Methodenaufrufs unveränderlich .quelle
C ++ - Standardzitate
Der C ++ 17 N4659-Standardentwurf 10.1.6 "Der Inline-Spezifizierer" besagt, dass Methoden implizit inline sind:
und dann weiter unten sehen wir, dass Inline-Methoden nicht nur für alle Übersetzungseinheiten definiert werden können, sondern müssen :
Dies wird auch ausdrücklich in einem Hinweis unter 12.2.1 "Mitgliedsfunktionen" erwähnt:
Implementierung von GCC 8.3
main.cpp
struct MyClass { void myMethod() {} }; int main() { MyClass().myMethod(); }
Symbole kompilieren und anzeigen:
Ausgabe:
U _GLOBAL_OFFSET_TABLE_ 0000000000000000 W MyClass::myMethod() U __stack_chk_fail 0000000000000000 T main
dann sehen wir,
man nm
dass dasMyClass::myMethod
Symbol in den ELF-Objektdateien als schwach markiert ist, was impliziert, dass es in mehreren Objektdateien erscheinen kann:quelle