logo
The site dedicated to C/C++ function-pointers


Exit  Download  Links  Contact  Disclaimer  Main  English

C/C++ Funktionenzeiger - Eine Einführung

Inhaltsverzeichnis

 Einleitung
 Was sind Funktionenzeiger?
 Wozu sind sie gut?
 Die Syntax von Funktionenzeigern
   Definition eines Funktionenzeigers
   Aufrufkonvention (Calling Convention)
   Zuweisung einer Funktionsadresse
   Aufruf einer Funktion über den Zeiger
   Arrays mit Funktionenzeigern
 Callback-Funktionen (Die englische Variante ist wesentlich aktueller!)
 Functors to encapsulate Function-Pointers (Eigene Seite in Englisch!)
 Topic Specific Links

Einleitung

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 ? top

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 ? top

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 top

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!)
top

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.
top


Download zipped Postscript Eine (allerdings noch nicht aktualisierte) Postscript Version der Tutorien über Funktionenzeiger und Callbacks gibt es hier !


last updated: 22.04.01
© 2000/2001 by lars haendel
Alle Marken sind das alleinige Eigentum ihrer Eigentümer. Ich mache das hier, weil es mir Spass macht. Ich arbeite in keinster Weise für irgendein kommerzielles Unternehmen oder ähnliches! Wenn ihr Fragen oder Anmerkungen habt, einen Link vorschlagen wollt oder einen Fehler entdeckt habt ... Schreibt mir!!!!