| The site dedicated to C/C++ function-pointers |
| Exit | Download | Links | Contact | Disclaimer | Main | English |
Funktionenzeiger ermöglichen einige äusserst interessante und effiziente Programmiertechniken. So lassen sich mit ihnen switch/if-Statements ersetzen, späte Bindung (Polymorphismus) emulieren oder Callbacks realisieren. Leider werden sie - wohl aufgrund ihrer etwas komplizierten Syntax - in der Literatur recht stiefmütterlich behandelt und wenn überhaupt, so doch nur recht kurz und oberflächlich angesprochen. Funktionenzeiger sind keine so grosse Fehlerquelle wie normale Zeiger, da mit ihnen niemals Speicher allokiert bzw. deallokiert wird. Alles, was man verstehen muss, ist ihre Syntax. Aber: Man sollte sich immer fragen, ob man wirklich einen Funktionenzeiger braucht. Es mag zwar schön sein, seinen eigenen Polymorphismus zu realisieren aber die Verwendung von virtuellen Funktionen ist meistens übersichtlicher und auch einfacher.
| Was sind Funktionenzeiger ? |
Funktionenzeiger sind Zeiger, d.h. Variablen, die auf die Speicheradresse einer Funktion zeigen. Dazu muss man wissen, dass ein laufendes Programm einen bestimmten Speicherplatz im Hauptspeicher zur Verfügung gestellt bekommt und sich in diesem sowohl der ausführbare compilierte Programmcode, als auch die vom Programm verwendeten Variablen befinden. Eine Funktion im Programmcode ist also, genauso wie z.B. ein Charakter-Feld, zunächst einmal nichts anderes als eine Speicheradresse. Wichtig ist nur, wie man, bzw. der Compiler/Prozessor, den Speicherinhalt, der an der entsprechenden Adresse beginnt, interpretiert.
| Wozu sind sie gut ? |
Normalerweise wird, wenn an einer bestimmten Stelle im Programm eine Funktion DoIt() aufgerufen werden soll, im Quellcode an eben jener bestimmten Stelle der Aufruf von DoIt() statisch eingefügt. Nun kann es jedoch sein, dass zum Zeitpunkt der Programmierung, d.h. zur Compilierzeit, noch gar nicht feststeht, welche Funktion aufgerufen werden soll. Dies könnte z.B. sein, wenn es sich um eine sogenannte Callbackfunktion handelt oder wenn dynamisch zur Laufzeit aus mehreren möglichen Funktionen eine aktuell aufzurufende ausgewählt werden soll. Letzteres liesse sich freilich auch mit einer Case-Anweisung lösen, bei der die jeweiligen Funktionen in den Zweigen der Case-Anweisung aufgerufen werden.
Im Folgenden ist als Beispiel, bei dem natürlich aufgrund der Einfachheit desselben der Einsatz von Funktionenzeigern nicht unbedingt sinnvoll ist, die Aufgabe betrachtet, zwei Argumente mittels einer der vier Grundrechenarten zu verknüpfen. Die Aufgabe wird einmal mittels einer Switch-Anweisung und einmal mittels eines Funktionenzeigers gelöst.
// definition der funktionen, aus denen eine zur laufzeit ausgewählt werden soll
float Plus (const float arg1, const float arg2) { return arg1+arg2; }
float Minus (const float arg1, const float arg2) { return arg1-arg2; }
float Multiply(const float arg1, const float arg2) { return arg1*arg2; }
float Divide (const float arg1, const float arg2) { return arg1/arg2; }
float arg1=2, arg2=5.5; // die beiden argumente der operation
// aufgabe gelöst mittels switch-anweisung
int main1()
{
char op = '+'; // operator codiert als character - zulässige zeichen: + - * /
// auswahl der passenden funktion, welche die operation durchführt
switch(op)
{
case '+' : return Plus (arg1, arg2); break; // das break ist ein bissl unnötig ;-)
case '-' : return Minus (arg1, arg2); break;
case '*' : return Multiply(arg1, arg2); break;
case '/' : return Divide (arg1, arg2); break;
}
}
// aufgabe gelöst mittels funktionen-zeiger
int main2()
{
// funktionen-zeiger-variable 'opFunc', initialisiert mit adresse der funktion 'Plus()'
// 'opFunc' zeigt auf eine Funktion, die zwei floats übernimmt und ein float zurückgibt
float (*opFunc)(const float, const float)= Plus;
return opFunc(arg1, arg2); // aufruf der funktion 'Plus()' über den funktionenzeiger
}
Wichtige Anmerkung: Ein Funktionenzeiger zeigt auf eine Funktion, die eine feststehende Signatur, d.h. Rückgabetyp und Übergabeparameter, hat. Dies wird vom Compiler im Rahmen der Typkontrolle überprüft. Daher müssen natürlich alle Funktionen, die mit demselben Funktionenzeiger verwenden sollen, die gleiche Signatur haben!
| Die Syntax von Funktionenzeigern |
Es gibt zwei von ihrer Syntax her unterschiedliche Arten von Funktionenzeigern: Zum einen gibt es Zeiger auf normale C Funktionen oder auf statische Member-Funktionen, zum anderen gibt es Zeiger auf nicht statische Member-Funktionen. Der wesentliche Unterschied ist, dass für Funktionsaufrufe von nicht statischen Member-Funktionen ein verstecktes Argument benötigt wird: der this-Zeiger auf eine Instanz der Klasse. Im folgenden werden Zeiger auf statische Member-Funktionen einfach den Zeigern auf normale C-Funktionen zugerechnet.
Definition eines Funktionenzeigers
Da ein Funktionenzeiger nichts anderes als eine Variable ist, muss diese wie üblich definiert werden. Im folgenden Beispiel wird eine Variable namens paraFunc definiert, die auf eine Funktion zeigt, welche ein float und zwei bool als Übergabeparameter bekommt und ein int zurückgibt. Im C++ Beispiel wird davon ausgegangen, dass der Funktionenzeiger auf eine Member-Funktion der Klasse TMyClass zeigt.
int (* paraFunc)(float, bool, bool); // C
int (TMyClass::*paraFunc)(float, bool, bool); // C++
Aufrufkonvention (Calling Convention)
Normalerweise muss man sich über die Aufrufkonvention einer Funktion keine Gedanken machen. Soweit keine Aufrufkonvention spezifiziert wird, wählt der Compiler __cdecl als default. Mittels der Aufrufkonvention wird dem Compiler unter anderem mitgeteilt, wie die Funktionsnamen zu generieren sind oder wie die Aufrufargumente über den Stack übergeben werden sollen. Beispiele für weitere Aufrufkonventionen sind __stdcall, __pascal und __fastcall. Die Aufrufkonvention gehört zur Signatur einer Funktion. Daher sind Funktionenzeiger und Funktionen mit unterschiedlicher Aufrufkonvention inkompatibel zueinander! Die Aufrufkonvention wird vor dem Namen einer Funktion aber nach ihrem Rückgabetyp angegeben. Anmerkung: Dies ist korrekt für meinen Borland Compiler. Ich habe andere
Angaben im Netz gefunden. Also, wenn es jemand weiss, so maile er es mir.
Zuweisung einer Funktionsadresse
Die Zuweisung einer Funktionsadresse an den Funktionenzeiger ist einfach. In C nehme man einfach den Namen einer passenden und dem Compiler bekannten Funktion, in C++ bilde man mittels des Adressoperators & die Adresse einer Member-Funktion und weise diese dem Funktionenzeiger zu. Anmerkung: Es mag sein, dass der vollständige Name einer Member-Funktion verwendet werden muss, d.h. der Funktionsname inklusive Klassenname und Scope-Operator. Ebenfalls muss sichergestellt sein, dass auf die Member-Funktion zugegriffen werden darf.
// C
// definition der funktion 'DoIt'
int DoIt(float arg1, bool arg2, bool arg3){ /* do something and return an int */}
paraFunc = DoIt;
// C++
// definition der klasse TMyClass
class TMyClass
{
int DoIt(float, bool, bool) { /* do something and return an int */ };
/* more of TMyClass */
};
paraFunc = &TMyClass::DoIt;
Es ist übrigens genauso einfach möglich, den Vergleichsoperator '==' zu verwenden. Im folgenden Beispiel wird geprüft, ob paraFunc momentan die Adresse der Funktion DoIt enthält und im Gleichheitsfalle wird ein Text ausgegeben.
if(paraFunc == DoIt) cout << "Alles in Ordnung"; // C
if(paraFunc == &TMyClass::DoIt) cout << "Alles in Ordnung"; // C++
Aufruf einer Funktion über den Zeiger
In C erfolgt der Aufruf einer Funktion über einen Funktionenzeiger genauso wie der normale Aufruf einer Funktion. Statt des Funktionsnamens wird einfach der Name des Funktionenzeigers verwendet. In C++ dagegen ist es schwieriger: Es handelt sich um Member-Funktion einer Klasse und solche Funktionen benötigen zum Aufruf eine Instanz der Klasse. Im folgenden Beispiel ist davon ausgegangen worden, dass der Aufruf innerhalb einer (anderen) Member-Funktion der Klasse TMyClass erfolgt und daher der this-Zeiger zur Verfügung steht.
int para = paraFunc (12, true, false); // C
int para = (*this.*paraFunc)(12, true, false); // C++
Nun gehen wir davon aus, dass die Funktion paraFunc über einen Funktionenzeiger von ausserhalb der Klasse TMyClass aufgerufen werden soll. Dafür ist es wichtig, paraFunc als public zu deklarieren. Vergleiche den Aufruf von außerhalb der Klasse zu dem vorherigen: instance übernimmt den Part des this-Zeigers und wird verwendet, um auf den Funktionenzeiger zuzugreifen.
class TMyClass
{
public:
int (TMyClass::*paraFunc) (float, bool, bool); // definiere FZ
int DoIt(float, bool, bool) { /* do something and return an int */};
TMyClass() { paraFunc=&TMyClass::DoIt; } // initialize FZ im konstruktor
};
int test()
{
TMyClass* instance = new TMyClass; // erzeuge instanz
// rufe funktion über den funktionenzeiger auf
return (*instance.*instance->paraFunc) (4.2, true, false);
delete instance;
}
Eigentlich einfach: Arrays mit Funktionen-Zeigern
Sehr interessant ist das Arbeiten mit Arrays von Funktionenzeigern. Dies bietet die Möglichkeit, eine aufzurufende Funktion einfach über einen Index auszuwählen. Die Syntax erscheint zwar etwas kryptisch, was häufig zu Verwirrung führt, ist aber genau betrachtet halb so wild ;-)
// C ///////////////////////////////////////////////////////////////////////////////
// typdefinition: 'pt2ParaFuncs' kann damit als typ für den array verwendet werden
typedef int (*pt2ParaFuncs)(float, bool, bool);
// 'funcArray' ist ein array mit zeigern auf funtkionen der signatur: int(float, bool, bool)
pt2ParaFuncs* funcArray;
// array mit 10 zeigern dynamisch erzeugen
funcArr = new pt2ParaFunc[10];
// adressen der funktionen zuweisen - 'DoIt' und 'DoMore' sind passende funktionen
funcArr[0] = DoIt;
funcArr[1] = DoMore;
/* weitere zuweisungen */
// aufruf einer funktion aus dem array mittels index 1
int para = funcArr[1](12, true, false);
// wer das freigeben vergisst ist ein i... ;-)
delete[] funcArr;
// C++ //////////////////////////////////////////////////////////////////////////////////////
// typdefinition: 'pt2ParaFuncs' kann damit als typ für den array verwendet werden
typedef int (TMyClass::*pt2ParaFuncs)(float, bool, bool);
// 'funcArray' ist ein array mit zeigern auf funtkionen der signatur: int(float, bool, bool)
pt2ParaFuncs* funcArray;
// array mit 10 zeigern dynamisch erzeugen
funcArr = new pt2ParaFunc[10];
// adressen der funktionen zuweisen - 'DoIt' und 'DoMore' sind passende member-funktionen
// der Klasse TMyClass
funcArr[0] = &TMyClass::DoIt;
funcArr[1] = &TMyClass::DoMore;
/* weitere zuweisungen */
// aufruf einer funktion aus dem array mittels index 1
int para = (*this.*funcArr[1])(12, true, false);
// wer das freigeben vergisst ist ein i... ;-)
delete[] funcArr;
| Callbackfunktionen am Beispiel von qsort (Die englische Variante ist wesentlich aktueller!) |
Funktionenzeiger ermöglichen das Konzept der sogenannten Callbackfunktionen. Dazu ein Beispiel mit der bekannten Sortierfunktion qsort von Borland (u.a. Standardbibliothek BC5.02). Die Funktion sortiert die Elemente eines Feldes, welches ihr per void}-Zeiger übergeben wird, nach einer Rangordnung. Das Feld kann Elemente eines beliebigen Typs enthalten; dem Sortierfunktion ist lediglich die Anzahl der Elemente des Feldes und die Grösse eines Elementes mitzuteilen. Insofern stellt sich die Frage, auf welche Weise die Rangordnung der einzelnen Elemente der Sortierfunktion bekannt sein sollte: Nun, die Sortierfunktion bekommt den Zeiger auf eine vom Programmierer zu schreibende Vergleichs-Funktion, die für zwei per Zeiger auf void übergebene Elemente die Rangordnung bestimmt und im Rückgabewert codiert. Die Implementierung des Sortieralgorithmus' ist somit vollkommen von der Notwendigkeit entkoppelt, die zu sortierenden Elemente im Voraus zu kennen. Die Deklaration der Funktion qsort liest sich wie folgt:
void qsort(void* field, size_t nElements, size_t sizeOfAnElement,
int(_USERENTRY *cmpFunc)(const void*, const void*));
Dabei bezeichnet field den Zeiger auf das zu sortierende Feld, nElements die Anzahl der Elemente im selbigen, sizeOfAnElement die Grösse eines Elementes in Byte und cmpFunc den Zeiger auf die Vergleichsfunktion. Diese Vergleichsfunktion übernimmt zwei Zeiger auf void und gibt ein int zurück. Die Syntax zur Übernahme eines Funktionenzeigers ist dieselbe wie zur Deklaration/Definition desselben. Siehe dazu auch den Absatz über die Definition von Funktionenzeigern. Im Folgenden ist das Beispiel betrachtet, ein Feld mit float-Zahlen zu sortieren.
// beim BC5.02 compiler zu includierende header-dateien
#include <stdlib.h> // wg. qsort()
#include <time.h> // wg. randomize()
/*******************************************************************************/
// vergleichsfunktion für den sortieralgorithmus - zwei elemente werden per
// void-zeiger übernommen, typ-konvertiert und verglichen
int cmpFunc(const void* _a, const void* _b)
{
// explizite typconvertierung auf den 'richtigen' typ
const float* a = (const float*) _a;
const float* b = (const float*) _b;
if(*a > *b) return1; // 1. grösser als 2. element -> gebe 1 zurück
else
if(*a == *b) return 0; // beide elemente gleich -> gebe 0 zurück
else return -1; // 2. grösser als 1. element -> gebe -1 zurück
}
/*******************************************************************************/
// beispielfunktion für den einsatz von qsort()
void example()
{
float* field=new float[1000];
::randomize(); // zufallszahlen-generator initialisieren
for(int c=0;c<1000;c++) // alle elemente des feldes zufällig besetzen
field[c]=random(99);
// sortieren mittels qsort()
qsort((void*) field, /*anzahl elemente*/ 1000, /*grösse eines elements*/ sizeof(field[0]),
/*vergleichsfunktion*/ cmpFunc);
/* do something useless ;-) with 'field' */
delete[] field;
}
| Topic Specific Links Da die Links alle auf englischsprachige Seiten zeigen, habe ich die Beschreibungen aus Bequemlichkeit nicht übersetzt. |
![]() |
Eine (allerdings noch nicht aktualisierte) Postscript Version der Tutorien über Funktionenzeiger und Callbacks gibt es hier ! |