Funktions-Pointer (*)()

In C und C++ können Funktionen nicht nur durch die Angabe eines Symbols aufgerufen werden, sondern auch mittels eines Funktionspointers. Ein solcher Funktionspointer speichert die Adresse der aufzurufenden Funktion und hat als Typ einen Pointer auf eine Funktion bestehend aus Rückgabetyp und Parameterliste.

#include <stdio.h> void printnormal(double f) {printf("%f\n", f);} void printformatted(double f){printf("%1.2f\n", f);} int main(){ void (*funcptr)(double); funcptr = &printnormal; (*funcptr)(3.14159); funcptr = &printformatted; (*funcptr)(3.14159); return 0; }

3.141590 3.14

Details

Mittels Funktionspointer ist es möglich, einer Variablen den Pointer auf eine Funktion zuzuordnen, welche an anderer Stelle im Programm durch einen Aufruf ebendieser Variablen angesprochen werden kann. Einem Funktionspointer kann ein beliebiger Pointer auf eine Funktion zugewiesen werden, solange die Funktion denselben Funktionstyp hat, wie dass der Funktionspointer erwartet.

An der Stelle des Aufrufes ist grundsätzlich nicht bekannt, welche Funktion in der Variablen gespeichert ist. Dennoch erfolgt der Aufruf der Funktion wie ein normaler Funktionsaufruf: Es werden Argumente übergeben und die Funktion liefert einen Rückgabewert. Aus dieser Überlegung heraus wird ein Funktionspointer in gewissen Situationen auch als Handler bezeichnet, in dem Sinne, dass die Funktion ein bestimmtes Set an Argumenten (be-)handelt. Was genau die Funktion jedoch ausführt, muss dem Compiler wie auch dem Programmierer nicht bekannt sein.

Die Ausprogrammierung von Funktionspointer ist etwas verwirrend, wird hier jedoch Schritt für Schritt erklärt. Weiter unten findet sich zudem eine Zusammenfassung von häufig gebrauchten Deklarationen im Zusammenhang mit Funktionspointer. Ganz am Ende werden einige Anwendungen angegeben, bei denen Funktionspointer nützlich sein können.

Funktionspointer in C

Die Deklaration eines Funktionspointers muss folgendermassen geschrieben werden:

Return-Type (*variable_name)(Parameterlist);

Der Rückgabetyp sowie die Parameterliste entsprechen dem Prototypen der Funktion, welche in der Variablen gespeichert werden soll. Die Parameterliste kann mit oder ohne Parameternamen angegeben werden, muss jedoch stets die Typen der Parameter beinhalten. Der Name der Variablen, welche mit dem Funktionspointer deklariert wird, steht mit einem vorne angehängten Pointer-Zeichen * in runden Klammern.

Die zusätzlichen Klammern sind zu verstehen als eine Klammerung des Typ-Ausdrucks. Diese Klammern sind notwendig, um dem Compiler mitzuteilen, dass es sich hier um einen Pointer auf eine Funktion handelt und nicht um eine Funktion, welche einen Pointer zurückgibt. Würde man die Klammern weglassen oder das Pointer-Zeichen ausserhalb der Klammern schreiben, so würde diese Zeile vom Compiler als eine normale Funktionsdeklaration angesehen.

Die Zuweisung zu einem Funktionspointer kann auf zwei Arten erfolgen:

variable_name = &global_function_name; variable_name = global_function_name;

Die erste Zeile zeigt die ursprünglich korrekte Variante, bei der dem Symbol, welches den Funktionspointer representiert, mittels des Adress-Operators die Adresse der Funktion zugewiesen wird. Die zweite Variante ist eine vereinfachte Schreibweise (sogenannter Syntactic Sugar) und bewirkt genau dasselbe. Diese Kurzform ist in den heutigen Compilern normalerweise zulässig. Auf dieser Seite wird zum besseren Verständnis stets die erste Variante benutzt, da sie explizit auf das Ansprechen der Adresse hinweist.

Der Aufruf der Funktion kann ebenfalls auf zwei Arten erfolgen:

(*variable_name)(Argumentlist); variable_name (Argumentlist);

Wiederum bezeichnet die erste Zeile die ursprünglich korrekte Variante, wobei die zweite Zeile eine vereinfachte Schreibweise aufzeigt. Die zusätzlichen runden Klammern in der ersten Zeile sind aufgrund der Operatorenrangfolge notwendig: Der Funktionsaufruf-Operator hat höhere Priorität als der Dereferenz-Operator.

Funktionspointer in C++

In C++ können Funktionen einem Namespace zugeordnet sein, welcher durch die Keywords namespace, class oder struct eingeleitet wird. Funktionen, wie sie in C verwendet werden, befinden sich in keinem Namespace und gehören somit dem globalen Bereich an. Obige Ausführungen für Funktionspointer in C können somit auch in C++ verwendet werden, solange Funktionspointer in C++ auf globale Funktionen zeigen.

Befindet sich die gewünschte Funktion jedoch innerhalb eines Namespaces, so sind einige weitere Angaben notwendig. Hier wird nun zuerst die Zuweisung und danach die Deklaration und der Aufruf eines Funktionspointers aufgeführt.

Die Zuweisung eines Funktionspointers, welcher auf eine Funktion innerhalb eines Namespace zeigt, muss folgendermassen geschrieben werden:

variable_name = &namespace_name::class_function_name;

Bei der Deklaration sowie dem Aufruf des Funktionspointers, welcher auf eine Funktion innerhalb eines Namespaces zeigt, muss zusätzlich unterschieden werden zwischen statischen und nicht-statischen Funktionen.

Funktionspointer auf statische Funktionen können genauso wie Funktionspointer auf globale Funktionen deklariert und aufgerufen werden.

Bei nicht-statischen Funktionen handelt es sich um Objekt-Methoden von Klassen und Structs. Nicht-statische Funktionen erwarten stets implizit einen this-Pointer, also ein Pointer auf ein Objekt der eigenen Klasse. Funktionspointer auf nicht-statische Funktionen benötigen somit eine Instanz der Klasse, um darauf zu operieren. Aus diesem Grund sind Funktionstypen von statischen und nicht-statischen Funktionen nicht kompatibel zueinander und können nicht ineinander umgewandelt werden.

Um einen Funktionspointer auf eine nicht-statische Funktion zu deklarieren, muss der Namespace mittels des Bereich-Operators :: vor dem Pointer-Zeichen angegeben werden:

void (namespace_name:: *variable_name)();

Damit wird dem Compiler mitgeteilt, dass der Funktionspointer auf eine nicht-statische Funktion innerhalb der angegebenen Klasse zeigt. Der Funktionstyp ist somit eine Funktion, welche einen impliziten this-Pointer der angegebenen Klasse erwartet.

Anmerkung: Der Autor konnte bislang noch keine vollauf befriedigende Erklärung für die Schreibweise :: * finden. In einigen Quellen wird ::* als eigenständiger Operator angegeben. Tatsächlich konnte der Autor jedoch diesen Operator in keiner öffentlichen Syntax-Beschreibung auffinden.

Durch das implizite this-Objekt ist ein Aufruf des Funktionspointers nicht mehr durch das einfache Ansprechen des Pointers möglich. Die nicht-statische Funktion muss mit einem Pointer-Member-Operator ->* oder Feld-Member-Operator .* aufgerufen werden. Diese beiden Operatoren erzwingen die Angabe eines Objekts auf dem die Funktion operieren kann.

(class_object_name->*variable_name)(); (*class_object_name.*variable_name)();

Man beachte, dass sowohl bei der Deklaration als auch bei dem Aufruf eines Funktionspointers auf eine nicht-statische Funktion innerhalb einer Klasse keine vereinfachte Variante wie bei globalen Funktionen existiert.

Um das Verständnis von Funktionspointer auf nicht-statische Funktionen zu vertiefen, wird hier noch erklärt, wie man ausserhalb einer Klasse auf einen Funktionspointer auf eine nicht-statische Funktion zugreifen kann, wenn der Funktionspointer selbst Teil derselben Klasse ist. Der Aufruf dieses Funktionspointers ausserhalb der Klasse muss folgendermassen geschrieben werden:

(class_object_name.*(class_object_name.variable_name))()

Diese Programmzeile wird folgendermassen vom Compiler interpretiert: Zuerst wird die Variable des angegebenen Objektes dereferenziert (innerste Klammer), was einen Funktionspointer auf eine nicht-statische Funktion der Klasse des Objektes zurückgibt. Um diesen Funktionspointer aufzurufen, muss wiederum dasselbe Objekt als das Objekt angegeben werden, auf welchem die Funktion operieren kann.

Funktionspointer als Rückgabewert

Funktionspointer können auch als Rückgabewert einer Funktion auftreten. Um eine Funktion mit dem entsprechenden Funktionspointer-Typ als Rückgabetyp zu deklarieren, muss der Name der Funktion zusammen mit ihrer Argumentenliste in die runden Klammern () mit dem Pointer-Zeichen * geschrieben werden.

In folgendem Beispiel wird eine Funktion test deklariert, welche ein Argument x vom Typ int erwartet und selbst einen Funktionspointer zurückgibt, welcher auf eine Funktion zeigt, welche als Argument einen double-Typ verlangt und einen float-Typ zurückgibt:

float (*test(int x))(double);

Wenn der Rückgabetyp auf nicht-statische Funktionen innerhalb eines Namespace zeigt, muss wie oben beschrieben zusätzlich der Namespace mit dem Bereichs-Operator :: vor dem Pointer-Zeichen angegeben werden.

float (namespace_name:: *test(int x))(double);

Verwendung von typedefs

Die Deklaration von Funktionspointern ist verwirrend und kann leicht zu Fehlinterpretationen beim Betrachten des Codes führen. Da es sich jedoch bei Funktionspointer um Typen handelt, ist es genauso wie bei jedem anderen Typ auch möglich, mittels typedef einen komplizierten Typausdruck als Symbol zu speichern.

Im folgenden Beispiel wird dem Symbol functiontype ein Funktionspointer mittels typedef zugeordnet, der auf eine Funktion zeigt, welche als Argument einen double-Typ verlangt und einen float-Typ zurückgibt:

typedef float (*functiontype)(double);

Das Beispiel des vorherigen Abschnittes kann dadurch folgendermassen geschrieben werden:

functiontype test(int x);

Durch die Verwendung von typedefs kann der Funktionspointer-Typ genauso wie jeder andere Typ verwendet werden.

Zusammenfassung

Hier sind nochmals einige Fälle aufgelistet in einem einzigen Programm. Man beachte dabei, dass zur Vereinfachung sämtliche Member der Klasse als public deklariert sind und dass nur eine einzige Klasse ohne Vererbung verwendet wird. Es sei dem Leser als Übung überlassen, Arrays von Funktionspointer auf private Operatorenüberladungen von Template-Klassen mit Mehrfachvererbung zu deklarieren. Viel Spass.

#include <cstdio> void globalfunction(int i){printf("Global (%i)\n", i);} static void staticglobalfunction(int i){printf("Static Global (%i)\n", i);} namespace Name_Space{ void namespacefunction(int i){printf("Namespace (%i)\n", i);} } void (*returnfunction(const char* str))(int){ printf("Return \"%s\":\n", str); return &globalfunction; } typedef void (*functiontype)(int); class TestClass{ public: void (*funcptr1)(int i); void (TestClass:: *funcptr2)(int i); void classfunction(int i){ printf("Class (%i)\n", i); } static void staticclassfunction(int i){ printf("Static Class (%i)\n", i); } void callglobal(int i){ funcptr1 = &globalfunction; (*funcptr1)(i); } void callclass(int i){ funcptr2 = &TestClass::classfunction; (this->*funcptr2)(i); } }; int main(){ TestClass test; // Global function using local function-pointer void (*funcptr1)(int i) = &globalfunction; (*funcptr1)(1); // Static global function using local function-pointer void (*funcptr2)(int i) = &staticglobalfunction; (*funcptr2)(2); // Function in namespace using local function pointer void (*funcptr3)(int i) = &Name_Space::namespacefunction; (*funcptr3)(3); // Static member function using local function pointer void (*funcptr4)(int i) = &TestClass::staticclassfunction; (*funcptr4)(4); // Global function using class-function-pointer inside of class test.callglobal(5); // Global function using class-function-pointer outside of class test.funcptr1 = &globalfunction; (*test.funcptr1)(6); // Class function using class-function-pointer inside of class test.callclass(7); // Class function using local function-pointer void (TestClass:: *classfunc)(int i) = &TestClass::classfunction; (test.*classfunc)(8); // Class function using class-function-pointer outside of class test.funcptr2 = &TestClass::classfunction; (test.*(test.funcptr2))(9); (*returnfunction("global"))(10); printf("Using typedef:\n"); functiontype typefuncptr = &globalfunction; (*typefuncptr)(11); return 0; }

Global (1) Static Global (2) Namespace (3) Static Class (4) Global (5) Global (6) Class (7) Class (8) Class (9) Return "global": Global (10) Using typedef: Global (11)

Anwendungen

Funktionspointer sind heutzutags nicht mehr häufig in Gebrauch. In C++ wird mit virtuellen Methoden eine andere Möglichkeit geboten, Funktionen ohne explizite Kenntnis aufzurufen. Dennoch werden hier einige Anwendungsbeispiele für Funktionspointer aufgeführt:

Es ist üblich, Datenstrukturen so zu programmieren, dass die eigentlichen Inhalte nicht direkt angesprochen werden können, sondern nur über Hilfs-Funktionen. Dadurch wird der eigentliche Inhalt abstrahiert und es ist nicht notwendig, die Art der Ausprogrammierung zu kennen. Einige Hilfs-Funktionen erwarten dementsprechend einen Handler, welcher die gewünschten Inhalte verändert, wo auch immer sie sich befinden.

Iteratoren beispielsweise durchlaufen gleichwohl Listen als auch Arrays, Bäume und weitere Strukturen. Häufig können solchen Iteratoren ein Handler mitgeliefert werden, welcher auf das aktuelle Element des Iterators angewendet wird. Funktionspointer werden hierbei häufig für Konvertierungen verwendet und zeigen oftmals auf relativ kleine Funktionen.

Durch solche Konstrukte ist es schlussendlich auch möglich, eine Funktion auf eine gesamte Datenstruktur anzuwenden. Solche Konstrukte werden in höheren Sprachen oftmals mit eingebauten Keywords wie foreach oder forall implementiert, in C und C++ existieren diese Keywords jedoch nicht.

Eine weitere Anwendung von Funktionspointer findet sich in der Geschwindigkeitsoptimierung. Wenn innerhalb einer Schleife aufgrund einer Bedingung ständig mittels if oder switch evaluiert wird, welche Funktion angesprochen werden soll, so kann dies einiges an Zeit beanspruchen. Diese Entscheidung kann jedoch möglicherweise einmal ausserhalb der Schleife gemacht werden, und die daraus resultierende Funktion kann innerhalb der Schleife als Funktionspointer angesprochen werden.

Nach wie vor weit verbreitet ist die Verwendung von Funktionspointer für State-Machines. State-Machines sind Programme, welche einen oder mehrere Einstellungs-Parameter besitzen, die sich nur durch eine explizite Ansprechung durch den Programmierer ändern. Für jede mögliche Einstellung führt das Programm unterschiedliche Anweisungen aus. Um die verschiedenen Ausführungen zu koordinieren, werden je nach Einstellung andere interne Funktionen aufgerufen, welche normalerweise als Funktionspointer angesprochen werden können. Die Änderung einer Einstellung bewirkt somit nur die Änderung eines Funktionspointers, ansonsten bleibt die Maschine unangetastet. Ein Beispiel für eine State-Machine ist OpenGL.

Ebenfalls aus dem Bereich der State-Machines kommt die Überlegung der Callback-Function (Rückruf-Funktion). Eine State-Machine erlaubt es dem Programmierer beispielsweise, eine bestimmte Funktion aufzurufen, wenn ein gewisses Ereignis eintritt. Durch die Angabe eines Funktionspointers kann der Programmierer der Maschine mitteilen, welche Funktion in diesem Falle aufgerufen werden soll.