Advanced Java Services | (Komplexe) Funktionsdeklarationen |
In ANSI-C müssen Funktionen zuerst deklariert werden und dann implementiert. Anschließend kann man sie verwenden. Meistens finden Deklarationen in Header-Dateien statt. Die Implementation befindet sich dann in einer zugehörigen C-Quellcode-Datei. Jede andere Quellcodedatei kann dann diese Funktionen aufrufen, wenn sie den zugehörigen Header mit #include einbindet. Für den Compiler reicht die Headerdatei. Um eine ausführbare Datei zu erzeugen muß allerdings noch dem Linker die Implementierungsdatei bekanntgemacht werden.
Eine einfache Funktionsdeklarationen folgt dem Schema <returntyp> <funktionsname>(<parametertyp>, <parametertyp>, ...); Der zu Parametertyp gehörende Variablenname kann bei der Deklaration weggelassen werden. Die Anzahl der Parameter ist quasibeliebig. Als Parametertyp und Returntyp sind alle Typen zugelassen, also auch struct-, union- und Pointertypen. Die Deklaration muß mit einem Strichpunkt abgeschlossen werden. In C können Funktionen nicht überladen werden. Der Compiler registriert nur den Namen der Funktion. Eine Änderung in der Parameterliste oder des Returntyps zieht daher immer eine Änderung des Funktionsnamen nach sich.
Ein erstes Beispiel
double addInt(int, int);
addInt ist eine Funktion die zwei int-Parameter bekommt und ein double zurückgibt.
Ergänzt man die Variablennamen, sieht das folgendermaß aus:
double addInt(int a, int b);
Beispiel 2
void initArr(int* arr, int len, int value);
initArr ist eine Funktion die einen int-Pointer, und zwei ints bekommt und nichts zurückgibt.
Das Ganze ohne Variablennamen:
void initArr(int*, int, int);
Beispiel 3
void kriegtNixGibtNix();
kriegtNixGibtNix ist eine Funktion die keine Parameter hat und nichts zurückgibt.
Hinweis: void kann in der Parameterliste weggelassen werden, nicht aber als Returntyp!
Funktionsdeklarationen werden vertrackt, wenn sie Pointer auf Funktionen enthalten. Man kann sie aber mit einer einfachen Regel auflösen, die man gut bei den obigen einfachen Beispielen einüben kann. Man hält sich dabei an die Regel, daß die runden (Funktions-)Klammern die höchste Priorität besitzen.
Vom Funktionsnamen ausgehend nach rechts das erste runde Klammerpaar auswerten. Dieses enthält immer die Parameterliste. Anschließend vom Funktionsnamen ausgehend nach links gehen. Hier beginnt immer der Returntyp. Bei einfachen Deklarationen steht der Returntyp direkt links vom Funktionsnamen.
Diese Leseregel läßt sich auch für komplexe Deklarationen anwenden. Allerdings wird das Auslesen des Returntyps dann aufwendiger. Man beachte, daß alle textuellen Beschreibungen der obigen Deklarationen mit dem Funktionsnamen beginnen und dann die Parameterliste auswerten.
Das nächste Beispiel verwendet ein struct
struct komplex *getKonjugierte(struct komplex);
getKonjugierte ist eine Funktion die einen Parameter vom Typ struct komplex bekommt und einen Pointer auf struct komplex zurückgibt.
Ein Stern * links vom Funktionsnamen gehört nie zum Funktionsnamen, sondern ist immer der Beginn des Returntyps. Man beachte oben den zweiten fettgedruckten Teil der Formulierung.
Mit der nächsten Deklaration/Definition erinnern wir an Funktionspointer:
char (*fpoi)(int, int);
fpoi ist hier ein Funktionspointer für Funktionen, die zwei Parameter vom Typ int erhalten und ein char zurückgeben. Für alle anderen Funktion kann fpoi nicht verwendet werden.
Ein Beispiel mit einem Funktionspointer in der Parameterliste. Wir verwenden als Funktionspointer den obigen Typ.
double beispiel( char (*)(int, int) , int, int);
beispiel ist eine Funktion die drei Parameter erhält. Der erste Parameter ist ein Funktionspointer für Funktionen die zwei int-Parameter haben und ein char zurückgeben. Parameter zwei und drei sind vom Typ int. Die Funktion gibt ein double zurück. Auch hier gelingt die Auflösung mit Hilfe der oben erwähnten Leseregel 1 (rechts vor links).
Es ist bei Funktionspointern in der Parameterliste durchaus üblich, daß kein Variablenname bei der Deklaration verwendet wird. Bei der Implementierung sind natürlich dann Variablennamen notwendig. Hier eine Quasiimplementierung:
double beispiel( char (*fpoi)(int, int) , int a, int b) { double x; char ch = (*fpoi)(a,b); // Aufruf mit Hilfe des Funktionspointers //.... compute x return x; }
Dieser letzte Fall ist tricky. Aber mit Leseregel 2 werden wir es schaffen. Wir wollen die folgende Deklaration entziffern. Um die Leseregeln leichter anwenden zu können sind zusammengehörende Klammerpaare farbig gekennzeichnet.
double (*getPoi(char))(int, int);
Wir gehen von getPoi aus nach rechts. getPoi ist eine Funktion, die ein char bekommt. Damit ist die Parameterliste erklärt. Für den Rückgabetyp gehen wir nun wieder zum Funktionsnamen und dann nach links. Mit dem Stern beginnt der Rückgabetyp, also Pointer. Wegen der Klammer kann sich der Stern nicht auf double beziehen. Das Pattern (* bedeutet vielmehr einen Funktionspointer. Die zum Pointer gehörende Parameterliste steht nun rechts der schließenden grünen Klammer, der Returntyp links der öffnenden grünen Klammer. Also ist der Returntyp ein Funktionspointer für Funktionen, die zwei int bekommen und ein double zurückgeben.
Mit typedef kann man hier die Lesbarkeit deutlich verbessern. Wir erklären ein Alias für den Funktionspointer, der zurückgegebnen wird.
typedef double (*fpoi)(int, int);
Noch ein Beispiel
double (*getPoi(char, double (*)(int, int)))(int, int);
getPoi ist eine Funktion, die zwei Parameter bekommt. Der erste Parameter ist ein char. Der zweite Parameter ist ein Funktionspointer. Der Funktionspointer ist für Funktionen, die zwei int bekommen und ein double zurückgeben. Jetzt gehts links vom Funktionsnamen weiter. (* sagt uns, daß der Rückgabetyp ein Pointer auf eine Funktion ist. Die zugehörige Parameterliste steht rechts von der schließenden grünen Klammer. Der zugehörige Returntyp links von der öffnenden grünen Klammer. Der Rückgabetyp ist also ein Funktionspointer für Funktionen, die zwei int bekommen und ein double zurückgeben. Der Returntyp ist also der gleiche wie der zweite Parameter der Funktion.