Wie funktionieren Header- und Quelldateien in C?

Lesezeit: 7 Minuten

Benutzeravatar von Dan Lugg
Dan Lugg

Ich habe die möglichen Duplikate durchgesehen, aber keine der Antworten dort sinkt ein.

tl; dr: Wie hängen Quell- und Header-Dateien zusammen? C? Sortieren Projekte Deklarations-/Definitionsabhängigkeiten implizit zur Erstellungszeit?

Ich versuche zu verstehen, wie der Compiler versteht die Beziehung zwischen .c und .h Dateien.

Angesichts dieser Dateien:

header.h:

int returnSeven(void);

Quelle.c:

int returnSeven(void){
    return 7;
}

Haupt c:

#include <stdio.h>
#include <stdlib.h>
#include "header.h"
int main(void){
    printf("%d", returnSeven());
    return 0;
}

Wird dieses Chaos kompilieren? Ich mache gerade meine Arbeit in NetBeans 7.0 mit gcc von Cygwin, das einen Großteil der Build-Aufgabe automatisiert. Wenn ein Projekt kompiliert wird, klären die beteiligten Projektdateien diese implizite Einbeziehung von source.c aufgrund der Erklärungen in header.h?

  • Ja, das wird kompiliert (und warum denken Sie, dass es ein “Durcheinander” ist?). Die zu lernenden Konzepte sind Zusammenstellungseinheiten und Verknüpfung.

    – Jesper

    5. Mai 2011 um 21:56 Uhr

  • Vielen Dank Jesper; Haha, es ist kein Durcheinander, ich nehme an, dieses Wort ist am besten für die Beschreibung meines Gehirns reserviert, Lesen zwischen 3 Anfängerniveau C Bücher. Ich werde auf jeden Fall nachsehen Zusammenstellungseinheiten und Verknüpfungaber um mich auf das Erlernen der Syntax zu konzentrieren, lasse ich es NetBeans + gcc finde das für mich heraus. Da immer dann, wenn eine bestimmte Header-Datei Deklarationen enthält, für die Definitionen an anderer Stelle im Projekt vorhanden sind, die Einbeziehung dieser Header-Datei ausreicht, um Zugriff auf die definierte Funktionalität zu gewähren, und der Compiler die Details sortiert?

    – Dan Lugg

    5. Mai 2011 um 22:00 Uhr


  • header.h braucht Wachen 😉

    – Alternative

    5. Mai 2011 um 22:01 Uhr

  • Ich empfehle auch, dies von Hand zu kompilieren. gcc main.c -c -o main.o, gcc source.c -c -o source.o, gcc main.o source.o -o program werde das zusammenstellen. Es macht es einfach, die einzelnen zusammengestellten Einheiten und die Verknüpfung am Ende zu sehen.

    – Alternative

    5. Mai 2011 um 22:02 Uhr

Das Konvertieren von C-Quellcodedateien in ein ausführbares Programm erfolgt normalerweise in zwei Schritten: kompilieren und verlinken.

Zuerst konvertiert der Compiler den Quellcode in Objektdateien (*.o). Dann nimmt der Linker diese Objektdateien zusammen mit statisch gelinkten Bibliotheken und erstellt ein ausführbares Programm.

Im ersten Schritt nimmt der Compiler a Kompilationseinheitdie normalerweise eine vorverarbeitete Quelldatei ist (also eine Quelldatei mit dem Inhalt aller Header, die sie enthält #includes) und wandelt diese in eine Objektdatei um.

In jeder Übersetzungseinheit müssen alle verwendeten Funktionen enthalten sein erklärt, um den Compiler wissen zu lassen, dass die Funktion existiert und was ihre Argumente sind. In Ihrem Beispiel die Deklaration der Funktion returnSeven befindet sich in der Header-Datei header.h. Beim Kompilieren main.cschließen Sie den Header in die Deklaration ein, damit der Compiler dies weiß returnSeven existiert, wenn es kompiliert wird main.c.

Wenn der Linker seine Arbeit erledigt, muss er die finden Definition jeder Funktion. Jede Funktion muss genau einmal in einer der Objektdateien definiert werden – wenn es mehrere Objektdateien gibt, die die Definition derselben Funktion enthalten, bricht der Linker mit einem Fehler ab.

Ihre Funktion returnSeven ist darin definiert source.c (und die main Funktion ist definiert in main.c).

Zusammenfassend haben Sie also zwei Kompilierungseinheiten: source.c und main.c (mit den darin enthaltenen Header-Dateien). Sie kompilieren diese zu zwei Objektdateien: source.o und main.o. Der erste enthält die Definition von returnSevendie zweite die Definition von main. Dann klebt der Linker diese beiden in einem ausführbaren Programm zusammen.

Über die Verknüpfung:

Es gibt externe Verknüpfung und interne Verknüpfung. Standardmäßig haben Funktionen eine externe Verknüpfung, was bedeutet, dass der Compiler diese Funktionen für den Linker sichtbar macht. Wenn Sie eine Funktion machen static, hat es eine interne Verknüpfung – es ist nur innerhalb der Kompilierungseinheit sichtbar, in der es definiert ist (der Linker weiß nicht, dass es existiert). Dies kann für Funktionen nützlich sein, die etwas intern in einer Quelldatei tun und die Sie vor dem Rest des Programms verbergen möchten.

Benutzeravatar von Oliver Charlesworth
Oliver Charlesworth

Die C-Sprache hat kein Konzept von Quelldateien und Header-Dateien (und der Compiler auch nicht). Dies ist lediglich eine Konvention; Denken Sie daran, dass eine Header-Datei immer vorhanden ist #included in eine Quelldatei; Der Präprozessor kopiert buchstäblich nur den Inhalt und fügt ihn ein, bevor die eigentliche Kompilierung beginnt.

Ihr Beispiel sollte compilieren (ungeachtet dummer Syntaxfehler). Wenn Sie beispielsweise GCC verwenden, können Sie zuerst Folgendes tun:

gcc -c -o source.o source.c
gcc -c -o main.o main.c

Dadurch wird jede Quelldatei separat kompiliert, wodurch unabhängige Objektdateien erstellt werden. In diesem Stadium, returnSeven() wurde intern nicht gelöst main.c; der Compiler hat lediglich die Objektdatei so markiert, dass sie in Zukunft aufgelöst werden muss. In diesem Stadium ist es also kein Problem main.c kann nicht sehen Definition von returnSeven(). (Hinweis: Dies unterscheidet sich von der Tatsache, dass main.c muss in der Lage sein, a zu sehen Erklärung von returnSeven() um zu kompilieren; es muss wissen, dass es sich tatsächlich um eine Funktion handelt und was ihr Prototyp ist. Deshalb müssen Sie #include "source.h" in main.c.)

Sie tun dann:

gcc -o my_prog source.o main.o

Dies Verknüpfungen die beiden Objektdateien zusammen in eine ausführbare Binärdatei und führt die Auflösung von Symbolen durch. In unserem Beispiel ist dies möglich, weil main.o erfordert returnSeven()und dies wird durch ausgesetzt source.o. In Fällen, in denen nicht alles zusammenpasst, würde ein Linker-Fehler resultieren.

  • (Hinweis: Dies unterscheidet sich von der Tatsache, dass main.c in der Lage sein muss, eine Deklaration von returnSeven() zu sehen: Ich bin pedantisch, aber das ist nicht ganz richtig. Der Compiler wird dies gerne kompilieren (mit einer Warnung in C99). Code, und der Linker löst ihn auf, meist mit schlechten Auswirkungen, zB in Datei ac, call x=bob(1,2,3,4) und in der Datei bc, void bob(char *a){} wird kompiliert, gelinkt und ausgeführt.

    – mattnz

    5. Mai 2011 um 23:49 Uhr


  • Absolute Weltklasse-Antwort. Ich liebe die Beispiele für minimalistische GCC-Compiler-Anweisungen

    – Dekan P

    18. Juli 2017 um 13:58 Uhr

Das Kompilieren hat nichts Magisches. Auch nicht automatisch!

Header-Dateien liefern dem Compiler grundsätzlich Informationen, fast nie Code.
Diese Informationen allein reichen normalerweise nicht aus, um ein vollständiges Programm zu erstellen.

Betrachten Sie das “Hello World”-Programm (mit der einfacheren puts Funktion):

#include <stdio.h>
int main(void) {
    puts("Hello, World!");
    return 0;
}

ohne Header weiß der Compiler nicht damit umzugehen puts() (es ist kein C-Schlüsselwort). Der Header teilt dem Compiler mit, wie die Argumente und der Rückgabewert zu verwalten sind.

Wie die Funktion funktioniert, wird jedoch nirgendwo in diesem einfachen Code angegeben. Jemand anderes hat den Code für geschrieben puts() und den kompilierten Code in eine Bibliothek aufgenommen. Der Code in dieser Bibliothek wird als Teil des Kompilierungsprozesses in den kompilierten Code für Ihre Quelle eingeschlossen.

Stellen Sie sich nun vor, Sie wollten Ihre eigene Version von puts()

int main(void) {
    myputs("Hello, World!");
    return 0;
}

Das Kompilieren nur dieses Codes führt zu einem Fehler, da der Compiler keine Informationen über die Funktion hat. Sie können diese Informationen bereitstellen

int myputs(const char *line);
int main(void) {
    myputs("Hello, World!");
    return 0;
}

und der Code wird jetzt kompiliert — aber nicht verlinkt, dh es wird keine ausführbare Datei erstellt, da kein Code für vorhanden ist myputs(). Also schreibst du den Code für myputs() in einer Datei namens “myputs.c”

#include <stdio.h>
int myputs(const char *line) {
    while (*line) putchar(*line++);
    return 0;
}

und Sie müssen daran denken, zu kompilieren beide Ihre erste Quelldatei und “myputs.c” zusammen.

Nach einer Weile hat sich Ihre “myputs.c”-Datei auf eine Handvoll Funktionen erweitert und Sie müssen die Informationen über alle Funktionen (ihre Prototypen) in die Quelldateien aufnehmen, die sie verwenden möchten.
Es ist bequemer, alle Prototypen in eine einzige Datei zu schreiben und #include diese Datei. Mit der Einbindung laufen Sie beim Tippen des Prototyps nicht Gefahr, einen Fehler zu machen.

Sie müssen jedoch noch alle Codedateien kompilieren und verknüpfen.


Wenn sie noch mehr wachsen, legst du den gesamten bereits kompilierten Code in eine Bibliothek … und das ist eine andere Geschichte 🙂

Header-Dateien werden verwendet, um die Schnittstellendeklarationen zu trennen, die den Implementierungen in den Quelldateien entsprechen. Sie werden auf andere Weise missbraucht, aber das ist der Normalfall. Dies ist nicht für den Compiler, sondern für die Menschen, die den Code schreiben.

Die meisten Compiler sehen die beiden Dateien nicht getrennt, sie werden vom Präprozessor kombiniert.

Der Compiler selbst hat kein spezifisches “Wissen” von Beziehungen zwischen Quelldateien und Header-Dateien. Diese Arten von Beziehungen werden typischerweise durch Projektdateien (z. B. Makefile, Lösung usw.) definiert.

Das angegebene Beispiel sieht so aus, als würde es korrekt kompiliert werden. Sie müssten beide Quelldateien kompilieren, und dann benötigt der Linker beide Objektdateien, um die ausführbare Datei zu erstellen.

1416370cookie-checkWie funktionieren Header- und Quelldateien in C?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy