Wofür Headerdateien?!



  • Okay, sorry aber ich muss nochmal pushen. Supertux' Antwort beantwortet mir nur einen Teil der Fragen. Zumindest hab ich jetzt festgestellt, dass ich auf in einer Headerdatei deklarierten Konstanten zugreifen kann, indem ich diese includiere.

    Ist das der einzige Sinn von Headerdateien? Konstanten in seiner Unit verfügbar zu haben? Und eben die Dokumentation, damit andere Programmierer wissen wie sie eine Lib anzusprechen haben, ohne dass sie mit dem Quelltext in Berührung kommen?


  • Administrator

    addiere.h

    double add(double a, double b);
    
    // -> Prototyp
    

    addiere.c

    #include "addiere.h"
    
    double add(double a, double b)
    {
      return a + b;
    }
    
    // -> Implementation
    

    main.c

    #include "addiere.h"
    
    #include <stdio.h>
    
    int main()
    {
      printf("%f", add(0.1, 2.32));
    
      return 0;
    }
    
    gcc addiere.c
    gcc -o test.exe main.c addiere.o
    

    -> funktioniert.

    Nehmen wir an, wir modifizieren mal die main.c und nehmen die erste Zeile raus, also so:
    main2.c

    #include <stdio.h>
    
    int main()
    {
      printf("%f", add(0.1, 2.32));
    
      return 0;
    }
    

    Führen die gcc Schritte aus:

    gcc addiere.c
    gcc -o test2.exe main2.c addiere.o
    

    Ich kann dir garantieren, dass dies einen Fehler wirft! Weil in der main2.c der Prototyp von add fehlt. Der Compiler weiss gar nicht was add ist. Deshalb muss man eben oben den Header inkludieren, damit man die Funktion deklariert. Der Linker fügt dann zur Deklaration über das Objectfile noch die tatsächliche Funktion dazu.

    Und jetzt lies vielleicht nochmals den Beitrag von supertux durch.

    Grüssli



  • Stell dir vor, du wärst ein Compiler und musst folgendes kompilieren:

    int c, d, *e;
    FILE f;
    
    c = abc;
    d = abc(1,2,3);
    e = abc(NULL);
    f = abc("/dev/null", "r");
    

    Duch die Deklaration von c,d,e,f weiß der Compiler, dass diese lokale Variablen sind von Typ int und FILE, e ist sogar ein Zeiger.

    Als erstes wirst du parsen

    c = abc;
    

    Da abc nicht deklariert ist und es nicht wie ein Funktionsuafruf ist, gibst du eine Fehlemeldung aus.

    In einer anderen .c Datei findest du:

    d = abc(1,2,3);
    

    abc ist dir nicht bekannt, also musst du Annehmen, dass es sich um eine Funktion handelt, die einen int zurückliefert.

    In einer anderen Funktion eines anderen Moduls findest du wiederum

    e = abc(NULL);
    

    Der Compiler weiß nichts von den bisherigen Dateien (und darin enthaltenden Symbolen), da der Compiler nur eine einzelne Datei pro Aufruf kompilieren kann. D.h. hier wird der Compiler genau wie oben raten müssen, dabei kann er aber völlig daneben liegen. Jedenfalls bringst du eine Warnung, weil der int Wert einem int-Zeiger zugewiesen wird.

    Im nächsten Modul stosst der Compiler auf

    f = abc("/dev/null", "r");
    

    hier gilt dasselbe wie in den letzten beiden Fällen, allerdings ist int zu FILE inkompatibel, also Fehlermeldung. Stell dir vor f wäre einen Zeiger, dann würdest du nur eine Warnung ausgeben (wie oben).

    Nun, alle Objket Dateien werden gelinkt und der Linker weiß jetzt nicht mehr, was zu tun ist, denn er findet mehrmals ein Symbol abc mit unterschiedlichen Signaturen und dann weiß er natürlich nicht, was zu tun.

    So, an dieser Stelle wirst du bemekert haben, dass der Inhalt von abc für den Compiler ziemlich irrelevant ist. Dem Compiler muss nicht wissen, was oder wie die Funktion abc ihre Arbeit erledigt. Das einzige, was der Compiler wissen möchte ist, was der Bezeichner abc überhaupt ist, sprich wie er deklariert ist.

    In der Header datei meinzeug.h hast du aber folgendes geschrieben:

    #ifndef MYZEUG_H
    #define MYZEUG_H
    ...
    /* function abc */
    FILE abc(const char *name, const char *openmode);
    ...
    #endif
    

    In meinzeug.h deklarierst du abc . Wäre meinzeug.h in allen Modulen inkludiert, wo abc auftaucht, dann wüstest du (bzw. der Compiler), dass es eine Funktion abc gibt, die ein FILE zurückgibt und zwei const char* Parameter bekommt. Sowas nennt man der Prototyp einer Funktion, also sowas wie Visitenkarte der Funktion, damit der Compiler weiß, womit er zu tun hat.

    Wenn der Compiler von Anfang an gewusst hätte, wie abc deklariert ist, dann hätte er beim 2. Modul gemerkt, dass diese Anweisung nicht möglich ist, weil diese die Signatur von abc widerspricht und somit falsch ist.

    So, in den Header Files ist alles entahlten, was der Compiler wissen muss, um den Code zu kompilieren, sprich, welche Funktionen es gibt und wie sie dekariert sind, welche Datenstrukturen du erzeugst hast (mit struct, typdef, union, usw), welche Konstanten, Markos, globale Variablen existieren, usw. In den Header Files sind also nur die Visitenkarten der verwendeten Symbolen, es kommt aber selten vor, dass man gleich den ganzen Code einer Funktion in den Header Files schreibt.

    Versuch mal folgendes zu kompilieren:

    int main(void)
    {
      FILE f;
      return 0;
    }
    

    ohne stdio.h zu inkludieren wüsste der Compiler nie, was FILE ist, als könnte er nicht kompilieren.



  • Dravere schrieb:

    Nehmen wir an, wir modifizieren mal die main.c und nehmen die erste Zeile raus, also so:
    main2.c

    #include <stdio.h>
    
    int main()
    {
      printf("%f", add(0.1, 2.32));
    
      return 0;
    }
    

    Führen die gcc Schritte aus:

    gcc addiere.c
    gcc -o test2.exe main2.c addiere.o
    

    Ich kann dir garantieren, dass dies einen Fehler wirft! Weil in der main2.c der Prototyp von add fehlt. Der Compiler weiss gar nicht was add ist. Deshalb muss man eben oben den Header inkludieren, damit man die Funktion deklariert. Der Linker fügt dann zur Deklaration über das Objectfile noch die tatsächliche Funktion dazu.

    Und jetzt lies vielleicht nochmals den Beitrag von supertux durch.

    Grüssli

    Genau da liegt das Problem...In deinem Beispiel wird _KEIN_ Fehler geworfen! Probiers aus! Ich hab das in den letzten Tagen bestimmt hundertmal gemacht. Wenn ein Fehler geworfen würde, hätte ich nicht von anfang an dieses Problem gehabt, sondern hätte einfach hingegenommen, dass man die Headerdateien braucht wenn man auf Funktionen innerhalb bereits kompilierter Übersetzungseinheiten zugreifen will...braucht man aber nicht! Bitte probier es selbst aus! Das einzige wo der Compiler meckert ist wenn man in der Header-Datei eine Konstante deklariert hat...Aber die Funktion kann auch gerne vollkommen falsch in der Headerdatei stehen, der Compiler kompiliert mir mit "gcc -o test main.c addiere.o" trotzdem zu einer problemlos laufbaren Datei, auch wenn ich in main.c KEINE Header von addiere inkludiere!



  • HansiDampf schrieb:

    dass man die Headerdateien braucht wenn man auf Funktionen innerhalb bereits kompilierter Übersetzungseinheiten zugreifen will...braucht man aber nicht! Bitte probier es selbst aus! Das einzige wo der Compiler meckert ist wenn man in der Header-Datei eine Konstante deklariert hat...Aber die Funktion kann auch gerne vollkommen falsch in der Headerdatei stehen, der Compiler kompiliert mir mit "gcc -o test main.c addiere.o" trotzdem zu einer problemlos laufbaren Datei, auch wenn ich in main.c KEINE Header von addiere inkludiere!

    liest du denn überhaupt nicht, was man schreibt?



  • Sorry. Ich habe warum auch immer deinen letzten Beitrag garnicht gesehen...

    Auf jedenfall schon mal vielen Dank für so einen ausführlichen und übersichtlichen Beitrag...

    Jetzt langsam wird es klarer denke ich...

    Also die Headerdatei sagt dem Compiler wie eine Funktion ABC aussehen muss.

    Daher kann der Compiler direkt einen Fehler werfen wenn er eine Funktion ABC findet die nicht dem in der Headerdatei beschriebenen Schema findet.

    Und der Umkehrschluss, also wenn man keine Headerdatei hätte, wäre, dass der Compiler in 3 verschiedenen .c-dateien drei verschiedene "Arten" einer Funktion mit demselben Namen bekommt..?

    Das bedeutet dann: Die Headerdatei ist eigentlich nur eine Hilfe für den Programmierer? Weil der Compiler dann "echte" Fehler ausgeben kann?
    Wenn der Programmierer alles richtig machen würde, bräuchte man keine Header-Datei? Nein, das kann doch nicht sein...

    Nein, ich glaube so wirklich verstehe ich es immernoch nicht. Tut mir leid. Ich weiss nicht was es ist, aber irgendwas ist an diesem Konzept einfach hoffnungslos unlogisch.

    Der Punkt ist: Genau das was Dravere beschrieben hat wäre das, dass ich mir logisch erklärt habe. Das habe ich dann ausprobiert und musste feststellen, dass es OHNE Headerdateien eben keinen Fehler gibt. Tja und seitdem versuche ich vergeblich zu verstehen was der Sinn dieser Dateien ist.

    Was Dravere beschrieben hat deckte sich wie gesagt mit meiner Vermutung: Nehmen wir an du hast eine Objekt-Datei oder von mir aus auch mehrere Objektdateien die einfach mir ar zu einem Archiv, und somit zu einer Library verbunden wurden.

    In den Objekt-Dateien ist bereits nur noch der aus dem Assembler kommende Maschinencode: Sprich der Compiler kann damit nichts mehr anfangen.

    So: Woher weiss der Compiler jetzt aber trotzdem, dass es in diesem Kauderwelsch an Maschinencode die Funktion gibt, die er in der main.c aufrufen muss? Das kann nur dadurch gehen, dass es eine Art Inhaltverzeichniss gibt. Sprich eine Liste die sagt: An Stelle XYZ in der Objektdatei findest du Funktion so und so, die so und so aufzurufen ist. Ich dachte, diese Liste wäre die Headerdatei. So ist es aber nicht. Diese Liste wird anscheinend bereits in der Objektdatei...dann kannst du sagen: Okay, passt! Der Linker linkt einfach diesen Maschinensprachen-Kram aus der Objekt-Datei mit Maschinensprachen-Kram der Main.c Datei zusammen. Beim Kompilieren der Main.c Datei hat er im "Inhaltsverzeichniss" der Objektdatei nachgesehen und konnte so die Verknüpfungen einfügen.

    Verstehst du mein Problem? Ich denke mir halt: Ja, es ist alles da, für was brauche ich jetzt noch eine Headerdatei?!

    oh mann...es tut mir wirklich leid, aber hier häng ich einfach schon seit Tagen total aufm Schlauch... 🙄

    Danke für eure Geduld.



  • HansiDampf schrieb:

    Das bedeutet dann: Die Headerdatei ist eigentlich nur eine Hilfe für den Programmierer? Weil der Compiler dann "echte" Fehler ausgeben kann?
    Wenn der Programmierer alles richtig machen würde, bräuchte man keine Header-Datei? Nein, das kann doch nicht sein...

    So ist das. Wenn die Autofahrer alles richtig machen würden, brauchten wir keine Verkehrspolizei.

    Die Headerdatei hilft Dir, Deinen Code zu organisieren. Möglicherweise änderst Du Deine add-Funktion irgendwann mal, weil Du nicht mehr ints sondern floats addieren willst. Wenn Du diese Funktion in mehreren c-Dateien aufrufst, übersiehst Du evtl. eine oder mehrere anzupassen.
    Nun kann es passieren, daß der Compiler der Funktion int - Werte auf den Stack legt, die Funktion liest aber float - Werte vom Stack.
    Weder beim Kompilieren noch beim Linken gibt es Fehler, aber das Programm gibt Blödsinn aus, wenn Du Pech hast, merkst Du das nicht.

    Probier mal folgendes Beispiel aus:
    Datei 1:

    int main ()
    {
    	printf ("5 + 3 = %d", add(5, 3));
    }
    

    Datei 2:

    double add (double a, double b)
    {
    	return a + b;
    }
    

    und stell Dir vor, anfangs hätte die Funktion nur ints genommen/geliefert und später hättest Du festgestellt, daß Du doubles möchtest. Unterstell weiterhin, Du rufst die Funktion aus vielen Quelldateien auf, so daß Du leicht welche übersehen kannst. Eine Header-Datei mit Funktionsprototyp in jeder Quelldatei schützt Dich davor, etwas zu übersehen. Solche Fehler sind nicht immer so trivial wie dieses Beispiel hier, und deshalb unter Umständen nur sehr mühsam zu finden.

    Das ist übrigens auch der Grund dafür, daß in C++ ohne Deklaration gar nichts geht - anders als hier in C.



  • Öhh, das Beispiel wird noch schöner, wenn Du den Rückgabewert der Funktion in int änderst:

    int add(double a, double b);

    Wenn Du dann mal abwechselnd mit und ohne passender Deklaration in Datei 1 experimentierst, wirst Du sehen, daß mit Deklaration das Ergebnis richtig ist, ohne Deklaration aber nicht.

    Und damit man das immer in allen benutzenden Quelldateien richtig hat, verwendet man Headerdateien mit der richtigen Deklaration.



  • callisto schrieb:

    Mich würde es mal interesieren wie man Header so aufbaut, dass man dem Hauptprogramm nur die Headerdatei übergibt und das Programm dann auf die Methoden/Funktionen der C-Datei zum Header zugreifen kann ohne dem Compiler die
    C-Datei explizit anzusagen.

    Includiere ich dort einfach die C-Datei im Header ???

    HansiDampf schrieb:

    @callsito: Meines Wissens geht das garnicht.

    Nicht so, wie es wohl gemeint ist, nein. Aber man kann ja z.B. auch die ganze Implementierung im Header machen und sich so eine kleine Bibliothek aufbauen, für die nichts hinzugelinkt werden muss. Genauso könnte man Header ignorieren und .c-Dateien inkludieren (hab ich sogar schon mal in irgeneinem SDK gesehen). Zu empfehlen ist das aber eher nicht...



  • HansiDampf schrieb:

    Das bedeutet dann: Die Headerdatei ist eigentlich nur eine Hilfe für den Programmierer? Weil der Compiler dann "echte" Fehler ausgeben kann?

    es ist eine "hilfe" für euch beide, denn der Compiler muss in manchen Fällen wissen, worum es sich handelt, um z.b. die genaue Größe einer Struktur auszurechnen. Merke, der Compiler kann raten, um was es sich handelt, manchmal wird es gehen, wenn die Datentypen zu int kompatibel sind. Wenn nicht, dann gibt es Fehler, denn der Compiler wüsste nicht, was er zu tun hat. Also ist es am besten, dass der Compiler nichts raten muss sondern dass du ihm mittels Prototypen (die am besten im Header File stehen müssen) die Funktionen vorstellst.

    HansiDampf schrieb:

    Wenn der Programmierer alles richtig machen würde, bräuchte man keine Header-Datei? Nein, das kann doch nicht sein...

    das würde nur gelten, wenn du nur Funktionen hättest, die zu int kompatibel sind. Eine Funktion, die selbstdefinierte Datentypen zurückgibt oder empfängt würde dann zum Compilerfehler führen.

    HansiDampf schrieb:

    Der Punkt ist: Genau das was Dravere beschrieben hat wäre das, dass ich mir logisch erklärt habe. Das habe ich dann ausprobiert und musste feststellen, dass es OHNE Headerdateien eben keinen Fehler gibt. Tja und seitdem versuche ich vergeblich zu verstehen was der Sinn dieser Dateien ist.

    das liegt daran, dass ein double zu int kompatibel ist und der Compiler in der Läge war den double in int umzuwandeln. Je nach Benachrichtigungsstufe des Compilers wird er schweigen, dir ne Warnung geben oder sogar anhalten. Damit du es verstehst:

    /* a.c */
    struct rgb {
      int r;
      int g;
      int b;
    };
    
    /* nur ein Bsp, die Funktion könnte auch völlig falsch sein */
    struct rgb set_color(int color)
    {
        struct rgb result;
        result.r = color & 0xff;
        result.g = (color >> 8) & 0xff;
        result.b = (color >> 16) & 0xff;
    
        return result;
    }
    

    Da sollte der Compiler eine Objketdatei a.o ausgeben, keine Probleme damit.

    /* b.c */
    
    struct rgb {
      int r;
      int g;
      int b;
    };
    
    int main(int argc, char **argv)
    {
        struct rgb col;
    
        col = set_color(atoi(argv[0]));
    }
    

    - Da du keine header Files hast, musst du struct rgb zwei mal deklarieren, ändert sich etwas daran, dann musst du an 2 Dateien ändern. Das ist blöd.

    - Der Compiler weiß nicht, was atoi ist, deswegen nimmt er an, es handelt sich um folgende Funktion: int atoi(char*) . In diesem Fall hat der Compiler richtig geraten (je nach Warnungsstufe des Compilers wird er dich warnen, dass er annehmen muss, dass atoi einen Integer zurückgibt).

    - Der Compiler weiß nicht, was set_color ist und nimmt an, es handelt sich um folgende Funktion: int set_color(int); . Der Datentype struct rgb ist leider nicht zu int kompatibel, der Compiler muss an dieser Stelle abbrechen und dir eine Fehlermeldung abgeben, obowhl eigentlich korrekt ist, denn im a.c liefert set_color ein struct rgb , nur der Compiler weiß das nicht und kann das nie wissen.

    Erkennst du langsam das Problem? Und wie kann man dieses Problem lösen? Mit Header Files:

    /* rgb.h */
    #ifndef RGB_H
    #define RGB_H
    
    struct rgb {
      int r;
      int g;
      int b;
    };
    
    /* proototyp, damit der Compiler weiß, worum es sich handelt */
    struct rgb set_color(int color);
    
    #endif
    

    Dann a.c

    #include "rgb.h"
    
    /* nur ein Bsp, die Funktion könnte auch völlig falsch sein */
    struct rgb set_color(int color)
    {
        struct rgb result;
        result.r = color & 0xff;
        result.g = (color >> 8) & 0xff;
        result.b = (color >> 16) & 0xff;
    
        return result;
    }
    

    und b.c

    /* b.c */
    
    #include <stdlib.h> /* für atoi */
    #include "rgb.h" /* für set_color und struct rgb */
    
    int main(int argc, char **argv)
    {
        struct rgb col;
    
        col = set_color(atoi(argv[0]));
    }
    

    Durch das inkludieren von rgb.h weiß der Compiler immer, was set_color ist und wie es deklariert ist. Auch wenn du die Struktur verändern willst, musst du jetzt nur an eine einzige Stelle tun, was besser ist.

    Der Compiler kann jetzt problemlos die Objektdateien a.o und b.o generieren, und der linker wird sich darum kümmern, alles zu einer Binary zusammenzufügen, so dass der Aufruf von set_color in main auch gelingt.

    Soweit klar? Wenn du das nicht mehr verstehest, dann bist du hoffnugslos oder du willst nicht verstehen.

    HansiDampf schrieb:

    So: Woher weiss der Compiler jetzt aber trotzdem, dass es in diesem Kauderwelsch an Maschinencode die Funktion gibt, die er in der main.c aufrufen muss?

    das muss der Compiler nicht wissen. Er muss nur wissen, wie diese Funktion deklariert ist, damit er sagen kann: "Ja, syntaktisch ist es korrekt". Diese Aufgabe, die enstsprechende Adresse in shared library (oder windows dll) zu finden und zuzuordnen ist eine Aufgabe des Linkers, der das machen kann, weil der Compiler ihm sagt, dass syntaktisch alles in Ordnung war.

    HansiDampf schrieb:

    Diese Liste wird anscheinend bereits in der Objektdatei...dann kannst du sagen: Okay, passt! Der Linker linkt einfach diesen Maschinensprachen-Kram aus der Objekt-Datei mit Maschinensprachen-Kram der Main.c Datei zusammen. Beim Kompilieren der Main.c Datei hat er im "Inhaltsverzeichniss" der Objektdatei nachgesehen und konnte so die Verknüpfungen einfügen.

    der Linker hat überhaupt keine Ahnung von Source Code Dateien, er bekommt nur objekt dateien, sprich "Maschinencode". Das muss nicht einmal von .c Datei kommen, es kann genausogut Assembler oder C++ sein. Binäre Dateien haben ein spezielles Format (ELF unter Linux) und damit weiß der Linker, wo er zu suchen kann, wenn er die Objekt dateien untersucht.

    HansiDampf schrieb:

    Verstehst du mein Problem? Ich denke mir halt: Ja, es ist alles da, für was brauche ich jetzt noch eine Headerdatei?!

    ja, dein problem ist, dass du das Compilieren und Linken in deinem kopf mischt. Siehe Compiler und Linker.



  • Hallo,

    ich habe ein Problem mit globalen Konstanten.
    Um Mehrfach-Deklaraationen zu vermeiden, definiere ich Konstanten für Zeichenketten(längen) in einer Headerdatei. Diese Headerdatei binde ich anschließend in zwei *.c-Dateien ein.
    Nachdem ich versuche dies zu kompilieren, erhalte ich eine Fehlermeldung, dass die Konstanten mehrfach deklariert wurden.

    Woran liegt das?

    globConst.h

    const int maxStrLen1 = 512
    const int maxStrLen2 = 1024
    

    main.c

    #include <stdio.h>
    #include "globConst.h"
    
    void main(void) {
    ...
    readme(...);
    }
    

    readme.c

    #include <stdio.h>
    #include "globConst.h"
    
    void readme_f1() {
    ....
    }
    


  • 1. vermutlich verwendest du keine Include Guards.

    2. C const int verhält sich anders als C++ const int . Für Konstanten verwende lieber #define oder enums.

    3. Globale Variablen, die in headers deklariert werden, sollten am besten mit extern deklariert werden und sie erst in einer Source-Datei deklarieren.



  • Die zweite Möglichkeit neben Include Guards ist ein "#pragma once" am Anfang des Headers. Include Guards sind imho nur dann die nötige Wahl, wenn man das define auch zu anderen Zwecken abfragen will.



  • HansiDampf schrieb:

    Das bedeutet dann: Die Headerdatei ist eigentlich nur eine Hilfe für den Programmierer? Weil der Compiler dann "echte" Fehler ausgeben kann?
    Wenn der Programmierer alles richtig machen würde, bräuchte man keine Header-Datei? Nein, das kann doch nicht sein...

    Wenn der Compiler eine Funktion nicht kennt, nimmt er an sie habe die Form:

    int f()

    Was wenn du aber keinen int returnen lassen willst? Nehmen wir zB an dass du einen double zurück geben willst. double ist idR doppelt so groß wie int.

    double d=f();

    das führt nun dazu, dass d nur die hälfte der Daten beinhaltet. Denn der Compiler denkt dass f einen int liefert und konvertiert diesen in einen double.

    Deshalb: eine Funktion muss immer einen Prototypen haben, damit der Compiler weiss was sie macht. Dieser Standard Prototyp den der C Compiler annimmt wenn er keinen findet ist idR falsch. Deshalb wurde im neuen C Standard dieses verhalten auch abgeschafft.

    Es ist eigentlich trivial:

    Sachen die du verwendest musst du bekanntmachen. Wenn wir über Hubsidupsi reden, dann wäre es praktisch wenn wir beide wissen was ein Hubsidupsi ist. Wenn du nur ratest was es ist, kannst du falsch raten und wir reden aneinander vorbei.

    Selbes bei prototypen. Nie den Compiler raten lassen...

    PS:
    sorry, 2. Seite übersehen...


Anmelden zum Antworten