Funktionen auslagern (C)

Aus der Mikrocontroller.net Artikelsammlung, mit Beiträgen verschiedener Autoren (siehe Versionsgeschichte)
Wechseln zu: Navigation, Suche

Dieser Artikel basiert auf folgendem Post: http://www.mikrocontroller.net/forum/read-1-259088.html#259151

Autor: Karl Heinz Buchegger (Heinzi) Datum: 11.11.2005 18:48

Funktionen/Prototypen in Header Files verteilen

Den Prototypen deklarierst Du im Headerfile. Und dieses Headerfile #include -st Du dann dort wo du's brauchst, zb. im main().

Das Grundprinzip ist doch folgendes in C:

Jedes Source Code File wird für sich alleine vom Compiler übersetzt. Und in jeder dieser Source Code Einheit muss alles deklariert sein, was der Compiler nicht von sich aus kennt. Und zwar bevor man es verwendet. Dazu gehören zb alle Funktionen aber auch globale Variablen. Natürlich kannst Du auch komplett auf den Header pfeifen und in jedem Source Code die Dinge einzeln deklarieren. Etwa so:

File1.c

int foo( int bar );

void foo2()
{
  int j = foo( 5 );
}

File2.c

int foo( int bar );

void foo3()
{
  int k = foo( 7 );
}

Main.c

int foo( int bar );
void foo2( void );
void foo3( void );

int main()
{
  int l = foo( 8 );
  foo2();
  foo3();
}

In jedem C-File sind alle Funktionen als Prototyp aufgeführt, bevor die Funktione dann verwendet (aufgerufen) wird. Dadurch kennt der Compiler die Funktion und weiß, dass es sich beim Funktionsnamen um keinen Tippfehler handelt und er weiß auch welche Argument die jeweilige Funktion benutzt und welchen Return Wert sie hat.

Aber Du merkst schon, wo das hinführt. Wenn Du foo() veränderst, zb. indem ein zusätzlicher Parameter mit aufgenommen wird, so musst Du die Änderung in diesem Fall an 4 Stellen machen:

  1. foo selbst
  2. den Prototypen in File1.c
  3. den Prototypen in File2.c
  4. den Prototypen in Main.c

Das das bei vielen Funktionen und vielen Files mehr als fehleranfällig ist, brauchen wir wohl nicht zu diskutieren.

Dazu gibt es aber in C den Mechanismus des #include

Bevor sich der Compiler den eigentlichen Programtext vornimmt, rauscht der erstmal durch den Preprozessor. Der ersetzt alle Zeilen mit

  #include <Dateiname>

durch den aktuellen Inhalt der Datei namens 'Dateiname'.

Du schreibst also zb.

Foo.h

int foo( int bar );

File1.c

#include "Foo.h"

void foo2()
{
  int j = foo( 5 );
}

File2.c

#include "Foo.h"

void foo3()
{
  int k = foo( 7 );
}

Übersetzt du zb. File1.c, dann passiert Folgendes: Zuerst nimmt sich der Preprozessor den Text vor und ersetzt die Zeile

  #include "Foo.h"

mit dem Inhalt der Datei 'Foo.h'. Als Ergebnis erhält er:

int foo( int Bar );

void foo2()
{
  int j = foo( 5 );
}

und erst dieses Ergebnis wird durch den tatsächlichen C Compiler gejagt. Der ist natürlich happy damit, denn in der Funktion foo2() wird eine andere Funktion foo() benutzt und deren Protoyp wurde ja korrekt angegeben. Korrekt bedeutet: Bevor die Funktion verwendet wird (aufgerufen wird), steht in der Source Code Datei der Prototyp. Der Prorotyp teilt dem Compiler mit, daß es diese Funktion tatsächlich gibt, welche Argumente sie erwartet und welchen Rückgabetyp sie liefert. Diese Information benötigt der Compiler um zu überprüfen, ob

  • es die aufgerufene Funktion tatsächlich gibt. Man hätte ja auch einen Tippfehler machen können
  • Ob die Anzahl der Argumente stimmt
  • Ob die Datentypen beim Aufruf für jeden Datentyp stimmt oder ob eventuell Anpassungen (falls automatisch möglich) gemacht werden müssen
  • Ob die Funktion einen Wert zurückliefert und wenn ja von welchem Datentyp dieser Rückgabewert ist, um eventuelle automatische Anpassungen bei der Verwendung dieses Rückgabewertes gemacht werden müssen.

Aber: Ändert sich an der Funktion foo() etwas, dann änderst Du einfach nur an einer einzigen Stelle den Protoypen: in Foo.h. Alle anderen, File1.c, File2.c und Main.c kriegen das automatisch mit, da sie ja Foo.h inkludieren und somit die Änderung beim nächsten kompilieren indirekt über Foo.h einfliessen.

Variablen/Lookup Tabellen

Im Prinzip machst Du genau das gleiche, nur kommt Dir hier die ODR (One Definition Rule) in die Quere. Jedes Teil darf nur einmal definiert werden! Bei einem Protoypen weiss der Compiler auch so, dass es sich um einen Prototypen handelt. Etwas in der Form

  int foo( int bar );

kann nur ein Protoyp (also eine Deklaration) sein. Eine Definition wuerde ganz anders aussehen

  int foo( int bar )
  {
    /* Implementierung von foo */
  }

bei globalen Variablen ist das aber anders. Ist

  int Global1;

nun eine Definition oder eine Deklaration?

Antwort: Es ist eine Definition.

Wenn Du sowas in Foo.h stehen hättest, dann würde der Compiler klarerweise beim übersetzen von File1.c eine Variable namens Global1 anlegen. Dasselbe würde er aber auch machen, wenn File2.c und Main.c übersetzt werden. Das geht aber nicht, denn dann würde der Linker 3 Variablen namens Global1 sehen und nicht wissen, welche denn die Richtige ist. Also muss man das anders machen: Die Definition muss zu einer Deklaration werden:

Foo1.h

int foo( int bar );
extern int Global1;

Das extern bewirkt, dass für Global1 jetzt keine Variable mehr erzeugt wird. Es ist nur noch die Information, daß es irgendwo eine Variable namens Global1 gibt und das diese vom Typ int ist.
Soweit so gut. Aber irgendwo muss diese Variable auch tatsächlich existieren, sonst könnte der Linker sie nicht finden, wenn er alle Einzelteile zu einem kompletten Programm zusammenbaut. Alle einzelnen *.c Dateien referenzieren sich ja nur noch auf eine Variable, die irgendwo anders erzeugt wird. Nur wo?

Na zb. hier:

Main.c

#include "Foo.h"

int Global1;   /* Da ist sie. Die Variable 'Global1'
                  Alle anderen die Foo.h inkludieren wissen nur
                  dass es sie gibt. Aber hier ist sie tatsaechlich */

int main()
{
  Global1 = 5;
}

Damit existiert die Variable Global1 tatsächlich und alle anderen Verweise in den anderen *.c Dateien haben somit ein tatsächliches Ziel.

Definition und Deklaration

Was ist also der Unterschied zwischen einer Definition und einer Deklaration?

Bei einer Definition wird im fertigen Programm vom Compiler tatsächlich etwas erzeugt. Eine Definition mündet also darin, dass im fertigen Programm Speicher für etwas reserviert wird. Eine Definition ist zb. eine Funktionsdefinition oder eine Variablendefinition.

Bei einer Deklaration teilt man dem Compiler lediglich mit, dass es etwas gibt, wie es heißt und welche Eigenschaften es hat. Der Compiler speichert dieses Wissen in Compilerinternen Tabellen und benutzt es, wenn er ein Programm übersetzt.

Eine Definition ist immer gleichzeitig auch eine Deklaration. Denn durch die Definition wird dem Compiler ja auch mitgeteilt, dass etwas existiert, wie es heißt und welche Eigenschaften es hat. Aber im Falle einer Funktion enthält die Definition zusätzlich noch den tatsächlichen Funktionsinhalt; bzw. im Falle einer Variablen enthält die Definition implizit noch die Anweisung: Diese Variable müsste jetzt erzeugt werden.