Funktions-Pointer Typ (*)()
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. Ein entsprechender Typausdruck ist schwierig zu lesen, weswegen er oft mittels typedef
deklariert wird.
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 während des Programmierens auch auch dem Compiler 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:
Bevor auf die Teile dieser Deklaration eingegangen wird: Die Schreibweise 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 darauffolgenden Code kann die Funktion dann ganz einfach mit folgender Zeile deklariert werden:
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ürden die Klammern weggelassen oder das Pointer-Zeichen ausserhalb der Klammern geschrieben werden, so würde diese Zeile vom Compiler als eine normale Funktionsdeklaration angesehen.
Die Zuweisung zu einem Funktionspointer kann auf zwei Arten erfolgen:
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.
Der Aufruf der Funktion kann ebenfalls auf zwei Arten erfolgen:
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 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:
Auch dieser Ausdruck ist mittels typedef
deutlich einfacher zu schreiben. Zuerst wird ein Type deklariert, der auf eine Funktion zeigt, welche als Argument einen double
-Typ verlangt und einen float
-Typ zurückgibt:
Danach kann das Beispiel des vorherigen Abschnittes folgendermassen geschrieben werden:
Anwendungen
In C++ sind Funktionspointer nicht häufig in Gebrauch, da in C++ mit virtuellen Methoden eine bequemere (aber nicht zwingendermassen bessere) Möglichkeit geboten wird, Funktionen ohne explizite Kenntnis aufzurufen.
Wenn jedoch beispielsweise explizit auf die Sprache C++ verzichtet wird, so müssen Mechanismen wie virtuelle Methoden mittels C nachgebildet werden. Libraries werden beispielsweise häufig in C und nicht in C++ geschrieben. In C können solche Mechanismen mit Bedingungen (if
oder switch
) oder eben mittels Funktions-Pointern nachgebildet werden.
In diesem Zusammenhang ist zu beachten, dass die Verwendung von Funktionspointern oftmals schneller ist als die Ausführung von Bedingungen. Wenn beispielsweise innerhalb einer Schleife oder einer häufig aufgerufenen Funktion 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 mittels Funktionspointer einmal ausserhalb der Schleife oder Funktion gemacht werden, und die daraus resultierende Funktion kann daraufhin innerhalb der Schleife oder Funktion mittels des Funktionspointer angesprochen werden.
Der Autor hat die Erfahrung gemacht, dass die Verwendung von Funktionspointern nur geringfügig mehr Aufwand bedeutet, im Gegenzug jedoch die Geschwindigkeit verbessert, mittels typedef
s die Lesbarkeit gar erhöht und schwer auffindbare Fehler bei komplizierten Bedingungen vermieden werden können.
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 jedoch nicht direkt verändert werden können, sondern sich bei Aufruf entsprechender Funktionen intern so ändern, dass bei Ende jedes Aufrufes automatisch wiederum ein gültiger Status erreicht ist. 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 Maschinen-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 beispielsweise, eine bestimmte Funktion aufzurufen, wenn ein gewisses Ereignis eintritt. Durch die Angabe eines Funktionspointers kann der Maschine mitgeteilt werden, welche Funktion in diesem Falle aufgerufen werden soll.
Des weiteren werden Funktionspointer auch für Handler benötigt. 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.
Funktionspointer und normale Pointer
Funktionspointer werden semantisch anders gehandhabt wie Pointer auf Daten. Während ein normaler Pointer auf einen Speicherbereich zeigt, welcher beliebig verändert werden darf, zeigen Funktionspointer an Orte im Speicher, wo ein manueller Zugriff durch den Benutzer nicht erlaubt ist.
Die Sprache C erlaubt es grundsätzlich, Pointer auf void*
zu casten, also auch Funktionspointer. Wenngleich der Compiler dies zulässt, ist es jedoch strengstens verboten! Code und Daten werden seit Jahrzehnten getrennt im Speicher gehalten. Ein Missachten dieser Trennung kann zu unvorhergesehenen Konsequenzen führen.
Früher wurde der Speicherbereich mit Code als das Text-Segment
bezeichnet und der Speicherbereich für die Daten mit Daten-Segment
. Manche Systeme definieren gar unterschiedliche Address-Grössen je nachdem, ob ein Pointer auf Daten oder ausführbaren Code zeigt. In diesem Falle würde ein Casting auf void*
möglicherweise gar die Adresse verfälschen.
Es ist möglich, mit dem Casting auf void*
Funktionspointer in eine andere Signatur zu zwängen. Dies wird jedoch mit grosser Wahrscheinlichkeit zu einem Laufzeitfehler führen, da der Auf- und Abbau einer Funktion eine durch das Runtime-System genau vorgegebene Anordnung befolgen muss. Wenn ein Funktionspointer eine falsche Signatur trägt, so wird das Programm unweigerlich fehlerhaft ablaufen und kann zu massiv korruptem Speicher führen. Glücklicherweise besitzen heutige Systeme Schutzmechanismen, um wenigstens das Betriebssystem vor solchen Fehlern zu schützen.
Da in C das Konzept der Referenzen nicht existiert, wurde anderweitig versucht, wenigstens die rudimentärsten Schutzmechanismen bereitzustellen. So ist es beispielsweise nicht möglich, den Code einer Funktion mithilfe des Dereferenz-Operators anzusprechen. Dieser hat speziell für Funktionspointer einen Mechanismus eingebaut, sodass der Rückgabewert derselbe ist wie der Funktionspointer selbst:
Function pointers are equal.
Diese speziellen Mechanismen existieren jedoch nur zum Schutz vor unvorsichtigem Programmieren. Bedarfte können nach diesem Abschnitt beruhigt aufatmen, da sie niemals in solche Situationen geraten werden, da sie niemals versuchen werden, einen Funktionspointer zu dereferenzieren oder anderweitig in einen anderen Pointer umzuwandeln.
Für diejenigen, welche dennoch Code manuell im Speicher ansprechen wollen, lohnt sich ein Blick auf die Funktion mmap
, welche jedoch auf dieser Seite nicht behandelt wird.