Dies ist wahrscheinlich eine detailliertere Antwort als Sie wollten, aber ich denke, eine anständige Erklärung ist gerechtfertigt.
In C und C ++ wird eine Quelldatei als eine Übersetzungseinheit definiert . Standardmäßig enthalten Header-Dateien Funktionsdeklarationen, Typdefinitionen und Klassendefinitionen. Die eigentlichen Funktionsimplementierungen befinden sich in Übersetzungseinheiten, dh CPP-Dateien.
Die Idee dahinter ist, dass Funktionen und Klassen- / Strukturelementfunktionen einmal kompiliert und zusammengestellt werden. Andere Funktionen können diesen Code dann von einer Stelle aus aufrufen, ohne Duplikate zu erstellen. Ihre Funktionen werden implizit als "extern" deklariert.
/* Function declaration, usually found in headers. */
/* Implicitly 'extern', i.e the symbol is visible everywhere, not just locally.*/
int add(int, int);
/* function body, or function definition. */
int add(int a, int b)
{
return a + b;
}
Wenn eine Funktion für eine Übersetzungseinheit lokal sein soll, definieren Sie sie als 'statisch'. Was bedeutet das? Wenn Sie Quelldateien mit externen Funktionen einschließen, werden Neudefinitionsfehler angezeigt, da der Compiler mehrmals auf dieselbe Implementierung stößt. Sie möchten also, dass alle Ihre Übersetzungseinheiten die Funktionsdeklaration sehen , nicht jedoch den Funktionskörper .
Wie kommt das alles am Ende zusammen? Das ist die Aufgabe des Linkers. Ein Linker liest alle Objektdateien, die von der Assembler-Phase generiert werden, und löst Symbole auf. Wie ich bereits sagte, ist ein Symbol nur ein Name. Zum Beispiel der Name einer Variablen oder einer Funktion. Wenn Übersetzungseinheiten, die Funktionen aufrufen oder Typen deklarieren, die Implementierung für diese Funktionen oder Typen nicht kennen, werden diese Symbole als ungelöst bezeichnet. Der Linker löst das nicht aufgelöste Symbol auf, indem er die Übersetzungseinheit, die das undefinierte Symbol enthält, mit der Einheit verbindet, die die Implementierung enthält. Puh. Dies gilt für alle extern sichtbaren Symbole, unabhängig davon, ob sie in Ihrem Code implementiert sind oder von einer zusätzlichen Bibliothek bereitgestellt werden. Eine Bibliothek ist eigentlich nur ein Archiv mit wiederverwendbarem Code.
Es gibt zwei bemerkenswerte Ausnahmen. Wenn Sie eine kleine Funktion haben, können Sie sie zunächst inline machen. Dies bedeutet, dass der generierte Maschinencode keinen externen Funktionsaufruf generiert, sondern buchstäblich direkt verkettet wird. Da sie normalerweise klein sind, spielt der Overhead keine Rolle. Sie können sich vorstellen, dass sie in ihrer Arbeitsweise statisch sind. So ist es sicher, Inline-Funktionen in Headern zu implementieren. Funktionsimplementierungen innerhalb einer Klassen- oder Strukturdefinition werden vom Compiler häufig auch automatisch eingefügt.
Die andere Ausnahme sind Vorlagen. Da der Compiler beim Instanziieren die gesamte Definition des Vorlagentyps sehen muss, ist es nicht möglich, die Implementierung wie bei eigenständigen Funktionen oder normalen Klassen von der Definition zu entkoppeln. Nun, vielleicht ist dies jetzt möglich, aber es hat lange gedauert, eine umfassende Compiler-Unterstützung für das Schlüsselwort "export" zu erhalten. Ohne Unterstützung für 'Export' erhalten Übersetzungseinheiten ihre eigenen lokalen Kopien von instanziierten Vorlagen-Typen und -Funktionen, ähnlich wie Inline-Funktionen funktionieren. Mit Unterstützung für 'Export' ist dies nicht der Fall.
Mit Ausnahme der beiden Ausnahmen finden es einige Leute "schöner", die Implementierungen von Inline-Funktionen, Vorlagenfunktionen und Vorlagen-Typen in CPP-Dateien zu platzieren und dann die CPP-Datei einzuschließen. Ob dies ein Header oder eine Quelldatei ist, spielt keine Rolle. Der Präprozessor kümmert sich nicht darum und ist nur eine Konvention.
Eine kurze Zusammenfassung des gesamten Prozesses vom C ++ - Code (mehrere Dateien) bis zur endgültigen ausführbaren Datei:
- Der Präprozessor wird ausgeführt, der alle Anweisungen analysiert, die mit einem '#' beginnen. Die Direktive #include verkettet beispielsweise die enthaltene Datei mit inferior. Es werden auch Makros ersetzt und Token eingefügt.
- Der eigentliche Compiler wird nach der Präprozessorphase in der Zwischentextdatei ausgeführt und gibt Assembler-Code aus.
- Der Assembler wird in der Assembly-Datei ausgeführt und gibt Maschinencode aus. Diese wird normalerweise als Objektdatei bezeichnet und folgt dem binären ausführbaren Format des betreffenden Betriebssystems. Beispielsweise verwendet Windows das PE (Portable Executable Format), während Linux das Unix System V ELF-Format mit GNU-Erweiterungen verwendet. Zu diesem Zeitpunkt sind Symbole noch als undefiniert markiert.
- Schließlich wird der Linker ausgeführt. Alle vorherigen Stufen wurden in der Reihenfolge auf jeder Übersetzungseinheit ausgeführt. Die Linker-Phase arbeitet jedoch mit allen generierten Objektdateien, die vom Assembler generiert wurden. Der Linker löst Symbole auf und macht viel Magie wie das Erstellen von Abschnitten und Segmenten, was von der Zielplattform und dem Binärformat abhängt. Programmierer müssen dies im Allgemeinen nicht wissen, aber es hilft sicherlich in einigen Fällen.
Auch dies war definitiv mehr, als Sie verlangt haben, aber ich hoffe, dass die Details Ihnen helfen, das Gesamtbild zu sehen.