Funktionenzeiger - Eine Einführung

Inhaltsverzeichnis

 Einleitung
 Was sind Funktionenzeiger?
 Wozu sind sie gut?
 Die Syntax von Funktionenzeigern
   Definition eines Funktionenzeigers
   Zuweisung einer Funktionsadresse
   Aufruf einer Funktion über den Zeiger
   Arrays mit Funktionenzeigern
 Callback-Funktionen


Einleitung

Funktionenzeiger ermöglichen einige äußerst interessante,effiziente und elegante Programmiertechniken. Leider werden sie - wohl aufgrund ihrer etwas komplizierten Syntax - in Computerbüchern und Dokumentationen recht stiefmütterlich behandelt und wenn überhaupt, so doch nur recht kurz und oberflächlich angesprochen.



Was sind Funktionenzeiger?

Funktionenzeiger sind Zeiger, d.h. Variablen, die auf die Speicheradresse einer Funktion zeigen. Dazu muß man wissen, daß bei den heutigen Betriebssystemen 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, 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, daß 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 ließe 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 charcter - 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, wenn man mittels eines Funktionenzeigers aus mehreren Funktionen eine auswählen will, diese Funktionen alle die gleiche Signatur haben!



Die Syntax von Funktionenzeigern

Es gibt zwei von ihrer Syntax her unterschiedliche Arten von Funktionenzeigern: Die Zeiger auf normale Funktionen (Standard c) und die Zeiger auf member-functions einer Klasse (object-orientierte Programmierung mit c++).

Definition eines Funktionenzeigers
Da ein Funktionenzeiger nichts anderes als eine Variable ist, muß diese wie üblich definiert werden. Ob dies global oder aber lokal in einer Funktion geschieht ist egal. Im folgenden Beispiel wird eine Variable namens paraFunc definiert, die auf eine Funktion zeigt, die eine float- und zwei bool-Variablen als Übergabeparameter bekommt und ein int zurückgibt.



int (* paraFunc)(float, bool, bool);             // standard c
int (TMyClass::*paraFunc)(float, bool, bool);    // c++



Zuweisung einer Funktionsadresse
Die Zuweisung einer Funktionsadresse an die Funktionenzeiger-Variable ist einfach. Beim Standard c nehme man einfach den Namen einer passenden und dem Copiler bekannten Funktion, im Falle von c++ bilde man mittels des Adressoperators & die Adresse einer Member-Funktion einer Klasse und weise diese zu:



// standard 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 ein Text ausgegeben.



if(paraFunc == DoIt)              cout << "Alles in Ordnung"; // standard c
if(paraFunc == &TMyClass::DoIt)   cout << "Alles in Ordnung"; // c++



Aufruf einer Funktion über den Zeiger
In Standard c erfolgt der Aufruf einer Funktion über einen Funktionenzeiger genauso wie der normale statische Aufruf einer Funktion, jedoch wird statt des Funktionsnamens der Name der Funktionenzeiger-Variable verwendet. Bei c++ dagegen ist es wichtig zu bedenken, da es sich um eine member-funktion einer Klasse handelt, daß solche Funktionen nur über eine Instanz der Klasse aufgerufen werden können! Im folgenden Beispiel ist davon ausgegangen worden, daß 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);    // standard c
int para = (*this.*paraFunc)(12, true, false);    // c++



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 ;-)



// standard 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

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öße 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öße 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ößer 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ößer 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öße eines elements*/ sizeof(field[0]),
           /*vergleichsfunktion*/ cmpFunc);

  /* do something useless ;-) with 'field' */

  delete[] field;
}