Forum: Mikrocontroller und Digitale Elektronik GCC: Header Files überhaupt nötig?


von payce (Gast)


Lesenswert?

Hallo an Alle,

programmiere meine Sachen immer auf AVR Studio und nutze meistens selbst 
geschriebene Libraries (LCD oder sonst was). In der Zwischenzeit bin ich 
etwas ins Zweifeln gekommen über den Sinn/Unsinn von Headerfiles.

Ist es richtig, das ich an und für sich komplett ohne .h Files 
programmieren kann (wenn nicht, bitte Beispiele, bin bisher auf nix 
gestoßen!)? Wenn doch (also ja), warum sollte ich dann überhaupt welche 
anlegen wollen? Wann sind Headers sinnig und wann nicht?

PS: Jupp, habe gesucht & gelesen, aber irgendwie schweigen sich die 
Bücher, die ich hier habe, darüber tot und/oder nehmen das einfach als 
bereits bekannt an. Also wenn die Frage blöd ist, dann sorry, 
programmiere erst seit ca. einem Jahr auf GCC (und damit auch C!).

THNX!!! :) (mal wieder... puh, würde ohne das Forum hier echt alle paar 
Monate irgendwo dick hängen bleiben, also noch mal Danke!)

von Karl H. (kbuchegg)


Lesenswert?

payce wrote:
> Ist es richtig, das ich an und für sich komplett ohne .h Files
> programmieren kann

Das ist grundsätzlich richtig.
Die Behauptung folgt sofort aus der Tatsache, dass
der eigentliche C Compiler die Header Files nie zu Gesicht
kriegt.

Header Files werden vom Präprozessor aufgelöst. Der ersetzt
die Zeile
#include "xyz"
durch den Inhalt der Datei xyz und erst dann geht der so
bearbeitete Quelltext durch den C Compiler.

> gestoßen!)? Wenn doch (also ja), warum sollte ich dann überhaupt welche
> anlegen wollen? Wann sind Headers sinnig und wann nicht?

Das Problem ist ein anderes.
Normalerweise sind Programme ein bischen umfangreicher als ein
paar Dutzend Programmzeilen. Eine Programmgröße von ein paar
Millionen Code Zeilen sind auch in C in professioneller Projekt-
arbeit nicht ungewöhnlich (zugegeben: nicht auf einem AVR. Aber
C wird ja nicht nur im µC Bereich eingesetzt).

Nun kann man aber nicht eine einzelne Textdatei machen in der
2 Millionen Lines of Code enthalten sind. Genauer gesagt:
technisch ist das schon möglich nur im Licht der Projektwartung
macht das keinen Sinn.
Daher geht man her und unterteilt ein Projekt in Module. Jedes
Modul kommt in eine eigene *.c Datei. Jede *.c Datei wird
unabhängig von allen anderen *.c Dateien compiliert und die
einzeln compilierten Module werden dann zum kompletten Programm
zusammengelinkt.
Das hat neben einer besseren Übersicht auch noch einen weiteren
Vorteil: Wenn eine Änderung in einer Funktion in einem Modul
notwendig ist, dann muss auch nur dieses Modul neu compiliert
werden. Durch den abschliessenden Linker-Schritt wird dann aus
den Einzelteilen wieder ein komplettes Programm zusammengestellt.
Ein einzelnes *.c File mit vielleicht 200 Zeilen zu kompilieren
geht aber schneller als wenn 2 Millionen Zeilen übersetzt werden
müssen.

Wie kommen da aber jetzt Header Files ins Spiel?

Nun. Ein Modul stellt ja Funktionen bereit, die irgendwelche
Dinge tun. Damit eine Funktion aber aufgerufen werden kann,
muss den Compiler gezeigt werden
* dass es diese Funktion gibt
* Anzahl und Datentyp der Argumente, die die Funktion nimmt
* Datentyp des Return Typs der Funktion

Das alles kann man dem Compiler mit einem Prototypen zeigen
1
void foo( double a, int b );
2
3
int main()
4
{
5
  // da es einen Prototypen für foo gibt, kann die Funktion
6
  // ohne Probleme aufgerufen werden
7
  foo( 2.0, 4 );
8
}

wird in einer anderen *.c Datei ebenfalls die Funktion foo benutzt,
so braucht sie ebenfalls einen Protoyp (Zur Erinnerung: Einzelne
*.c Dateien werden ja unabhängig voneinander compiliert)

Hast du also 30 *.c Dateien, in denen jeweils die Funktion foo
benutzt werden soll, so muss in jede *.c ein Prototyp hinein.

Aber jetzt kommts: Der Compiler hat keine Chance zu überprüfen,
ob der angegebene Prototyp auch mit der tatsächlichen Funktions-
signatur übereinstimmt. Er muss dem Programmierer in dieser Hinsicht
vertrauen.
Speziell geht es jetzt um den Fall, dass sich die Signatur von
foo aus irgendeinem Grund ändert. Sagen wir mal foo liefert jetzt
einen double zurück.
Dann müssen natürlich all die Protoypen in all den *.c Files
geändert und angepasst werden. Und wehe du vergisst einen.

Aber es gibt einen Ausweg: Man kann den eigentlichen Protoyp
in eine eigene Datei auslagern

xyz.h
*****
void foo( double a, int b );

und diese Datei wird mittels einer Präprozessor Anweisung
in jedes *.c hineingezigen in der der Protoyp gebraucht wird:

main.c
******

#include "xyz.h"

int main()
{
  foor( 2.0, 3 );
}

Nachdem der Präprozessor sich main.c vorgenommen hat, ist
die Zeile #include "xyz.h" durch den Inhalt der Datei xyz.h
ersetzt worden. Der Quelltext von main.c wird also modifiziert zu:

void foo( double a, int b );

int main()
{
  foo( 2.0, 3 );
}

und dieser Quelltext wird durch den C Compiler gejagt.
Aber: Dasselbe kann man auch mit den 30 anderen *.c Dateien
machen, die ebenfalls die Funktion foo benutzen wollen.
Es gibt aber grosse Unterschiede
* erstens sind in einem Header File normalerweise mehrere
  Protoypen enthalten. Durch den #include spart man daher
  Tipparbeit und dadurch wird natürlich auch die Tippfehler-
  häufigkeit reduziert
* zum zweiten ist es so, dass bei einer Änderung am Protoypen
  diese Änderung nur einer einer Stelle gemacht werden muss,
  nämlich in der Datei xyz.h
  Alle *.c Dateien bekommen diese Änderung automatisch mit, da
  ja die Einbindung von xyz.h erst kurz vor dem Compilieren
  automatisch vorgenommen wird.



>
> PS: Jupp, habe gesucht & gelesen, aber irgendwie schweigen sich die
> Bücher, die ich hier habe, darüber tot und/oder nehmen das einfach als
> bereits bekannt an. Also wenn die Frage blöd ist, dann sorry,
> programmiere erst seit ca. einem Jahr auf GCC (und damit auch C!).
>
> THNX!!! :) (mal wieder... puh, würde ohne das Forum hier echt alle paar
> Monate irgendwo dick hängen bleiben, also noch mal Danke!)

von payce (Gast)


Lesenswert?

Also erstmal: WOW!!! Ausführlichste Antwort! Haaaammer! Vielen, vielen 
Dank.

Auf jeden Fall beruhigt mich Deine Antwort schon sehr, hab ich also 
verständnissmäßig nix falsch gemacht. Und auch das anschauliche 
Beispiel, wann sowas sinnig wird war sehr hilfreich. Also dann werde ich 
mir bei der µC Programmierung erstmal Headers sparen. Falls es jemals 
doch notwendig werden sollte (kann ich mir momentan noch nicht 
vorstellen) kann man das ja noch nachpflegen - ist dann halt einmal 
etwas mehr Aufwand. Ansonsten werde ich so weiter machen wie bisher 
(sprich: Kleine, modulare .c & .s Libraries mit entpsrechenden 
Funktionen ohne Headers).

Vorschlag von meiner Seite: Deinen Absatz sollte man unbedingt als 
Artikel abspeichern oder auch gleich mit ins GCC Tutorial einspeisen 
(meinetwegen im Anhang). Mir hats echt weiter geholfen!

Danke noch mal, gute Nacht und Grüße payce! :D

von Karl H. (kbuchegg)


Lesenswert?

payce wrote:
> Also erstmal: WOW!!! Ausführlichste Antwort! Haaaammer! Vielen, vielen
> Dank.
>
> Auf jeden Fall beruhigt mich Deine Antwort schon sehr, hab ich also
> verständnissmäßig nix falsch gemacht. Und auch das anschauliche
> Beispiel, wann sowas sinnig wird war sehr hilfreich. Also dann werde ich
> mir bei der µC Programmierung erstmal Headers sparen.

Dann hab ich mein Ziel verfehlt.
Du sollst dir Header nicht sparen, sondern überlegen wie
du deine Projekte modular aufbauen kannst. In dem Moment,
indem du Module hast, kommst du vernünftigerweise um
Header Files nicht mehr drum herum.

von Karl H. (kbuchegg)


Lesenswert?

Es kann jetzt allerdings auch sein, dass wir aneinander
vorbeireden.

Wenn dein Aufbau so aussieht
1
int main()
2
{
3
  foo()
4
}
5
6
void foo()
7
{
8
  bar();
9
}
10
11
void bar()
12
{
13
}

dann ist ein Header File mit den Protoypen für foo() und bar()
natürlich Blödsinn.
Solche Dinge löst man am besten auch nicht mit Protoypen,
wie hier:
1
void foo();
2
void bar();
3
4
int main()
5
{
6
  foo()
7
}
8
9
void foo()
10
{
11
  bar();
12
}
13
14
void bar()
15
{
16
}

sondern indem man die Reihenfolge der Funktionen umdreht:
1
void bar()
2
{
3
}
4
5
void foo()
6
{
7
  bar();
8
}
9
10
int main()
11
{
12
  foo()
13
}

Grundsatz:
Ein Protoyp der nicht gebraucht wird, kann auch nicht falsch sein.

Nur in dem Fall, in dem du Module hast, sagen wir mal eine UART
Ansteuerung, macht dann das Konzept auch wirklich Sinn.

Du hast vielleicht ein
uart.c
******
1
void InitUart( unsigned int Baud )
2
{
3
   ....
4
}
5
6
void Uart_Putc( char c )
7
{
8
   ....
9
}
10
11
void Uart_puts( const char* String )
12
{
13
   ...
14
}
15
16
void Uart_putint( int Value )
17
{
18
   ...
19
}

dann wäre es natürlich töricht, wenn du das Programm welches
die Uart Funktionen benutzt so schreiben würdest:

main.c
******
1
...
2
3
void InitUart( unsigned int Baud )
4
void Uart_Putc( char c );
5
void Uart_puts( const char* String );
6
void Uart_putint( int Value );
7
8
int main()
9
{
10
  InitUart( 9600 );
11
12
  Uart_puts( "Hello and Welcome\r\n" );
13
}

Diese Protoypen kommen besser in ein Uart.h

Uart.h
******
1
void InitUart( unsigned int Baud )
2
void Uart_Putc( char c );
3
void Uart_puts( const char* String );
4
void Uart_putint( int Value );

und damit wird das Programm zu

main.c
******
1
...
2
3
#include "Uart.h"
4
5
int main()
6
{
7
  InitUart( 9600 );
8
9
  Uart_puts( "Hello and Welcome\r\n" );
10
}

bzw.

uart.c
******
1
#include "Uart.h"
2
3
void InitUart( unsigned int Baud )
4
{
5
   ....
6
}
7
8
void Uart_Putc( char c )
9
{
10
   ....
11
}
12
13
void Uart_puts( const char* String )
14
{
15
   ...
16
}
17
18
void Uart_putint( int Value )
19
{
20
   ...
21
}

Zum einen hast du dadurch in main.c den Tippaufwand weg und
zum anderen hast du durch die Inclusion in Uart.c eine gewisse
Sicherheit, dass die Protoypen mit den Funktionen auch tatsächlich
übereinstimmen.

Deine Uart-Bibliothek besteht damit aus 2 Teilen
* aus Uart.c, welches die tatsächliche Implementierung
  durchführt
* aus Uart.h, welches alles Notwendige enthält, damit ein
  Programm den 'Service' des Uart Moduls auch in Anspruch
  nehmen kann.

von Winfried (Gast)


Lesenswert?

In der Headerdatei wird das Interface einer Bibliothek definiert und 
beschrieben. Das ist doch eine sinnvolle und saubere Form. Ich will doch 
nicht jedesmal, wenn ich eine Bibliothek einbinde, in den .c-Files 
nachschauen, welche Funktionen es gibt und dafür dann Prototypen 
erstellen. Und dann noch schauen, welche anderen Bibliotheken ich alles 
einbinden muss, damit das geschnürte Gesamtpaket dann funktioniert.

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.