Funktionszeiger in C

Aus der Mikrocontroller.net Artikelsammlung, mit Beiträgen verschiedener Autoren (siehe Versionsgeschichte)
Wechseln zu: Navigation, Suche

Um Menüs oder ähnliche Dinge aufzubauen, ist es oft praktisch, ein Array von Funktionszeigern zu definieren. Der Aufruf einer Funktion kann dann indirekt über eine Variable erfolgen, wobei die Variable die Adresse der aufzurufenden Funktion enthält.

Um mit Funktionszeigern zu arbeiten ist es in der Praxis sinnvoll, sich einen eigenen Typen zu definieren. Ein typedef definiert einen neuen Namen für einen Datentyp. Und wie wir sehen werden, ist der Datentyp eines Funktionszeigers in der Schreibweise ganz schön umfangreich.

typedef

Einen typedef zu definieren ist einfach: Man schreibt die Deklaration so, als ob man eine Variable definieren würde. Vor das ganze Konstrukt kommt das Schlüsselwort typedef. Es bewirkt, dass der Name an der Position des Variablennamens zum Namen für den neuen Datentyp wird, der dann in weiterer Folge wie jeder andere Datentyp benutzt werden kann.

Wir wollen einen Funktionszeiger auf eine Funktion definieren, die keine Argumente entgegen nimmt und auch nichts liefert. Also Funktionen nach dem Muster:

void foo (void)
{
}

Ein entsprechender typedef sieht dann so aus:

typedef void (*VoidFnct)(void);

Das vereinbart einen neuen Datentyp VoidFnct. Dieser ist ein Funktionszeiger auf Funktionen, die keine Argumente nehmen und auch nichts zurückliefern.

typedef int (*IntFnct) (void);
typedef int (*IntFnct2) (int);
  • IntFnct ist ein Zeiger auf eine Funktion, die keine Argumente nimmt aber einen int zurückliefert.
  • IntFnct2 ist ein Zeiger auf eine Funktion, die einen int als Argument nimmt und einen solchen zurückliefert.

Andere Argumenttypen bzw. Rückgabetypen folgen dem gleichen Muster. Wichtig ist, dass sowohl Argumenttypen als auch Rückgabetypen Teil der Signatur eines Funktionszeigers sind. Es ist also nicht möglich, einen Funktionszeigertyp zu vereinbaren, der auf beliebige Funktionen mit beliebigen Argumenttypen bzw. Rückgabetypen verweist. Hier muss man ev. auf einen Cast ausweichen. Generell ist das aber meist keine gute Idee.

Funktionszeigertabellen

Mit einem typedef ist es nun ein Leichtes, ein Array von Funktionszeigern zu vereinbaren:

typedef void (*VoidFnct) (void);

VoidFnct MeineFunktionen[5];

Im RAM

Dies vereinbart MeineFunktionen als ein Array von Funktionszeigern, wobei jeder Funktionszeiger auf eine Funktion vom Typ void-void zeigt. Diese Tabelle liegt im RAM.

typedef void (*VoidFnct) (void);

void Funct1 (void)
{
  printf ("Dies ist Funktion 1\n");
}

void Funct2 (void)
{
  printf ("Dies ist Funktion 2\n");
}

VoidFnct MeineFunktionen[] = { Funct1, Funct2 };

int main()
{
  // ruft die Funktion auf, deren Adresse in MeineFunktionen[0]
  // steht. In diesem Fall wäre das Funct1()

  MeineFunktionen[0]();

  // und jetzt die MeineFunktionen[1]

  MeineFunktionen[1]();

  // Jetzt wird MeineFunktionen[0] umgeleitet auf Funct2()
  // Achtung: Auf beiden Seiten wird kein () angegeben.
  // Ansonsten würde ja die jeweilige Funktion (MeineFunktionen[0] und Funct2  
  // aufgerufen. Wir wollen aber nur ihre Speicheradresse haben! Daher   
  //unterbleibt das ().

  MeineFunktionen[0] = Funct2;

  // welche Funktion wird jetzt aufgerufen?
  // Richtig: Die Funktion, deren Adresse in MeineFunktionen[0]
  // steht. Und das ist jetzt Funct2.

  MeineFunktionen[0]();
}

avr-gcc: Im Flash

Steigt die Anzahl der Funktionen innerhalb der Tabelle stark an und soll deren Inhalt nicht veränderbar sein, kann sie in den Flash verlegt werden. Das spart die entsprechenden RAM-Ressourcen.

// stellt Zugriffsfunktionen für Flash zur Verfügung

#include <avr/pgmspace.h>

typedef void (*VoidFnct) (void);

void Funct1 (void)
{
  printf ("Dies ist Funktion 1\n");
}

void Funct2 (void)
{
  printf ("Dies ist Funktion 2\n");
}

// legt die Tabelle im Flash ab

const VoidFnct MeineFunktionen[] PROGMEM = { Funct1, Funct2 };

int main()
{
  // legt eine Variable an, die mit der Funktionsadresse aus 
  // der Tabelle (die im Flash abliegt) gefüllt wird. 
  // Lädt die Adresse der Funktion, die an MeineFunktionen[0] steht

  VoidFnct MeineFunktion = (VoidFnct)pgm_read_word (&MeineFunktionen[0]);

  // Ruft MeineFunktion auf, welche nun auf die Adresse von 
  // MeineFunktionen[0] zeigt. In diesem Fall ist das Funct1()

  MeineFunktion();

  // und jetzt die an MeineFunktionen[1], also Funct2()

  MeineFunktion = (VoidFnct) pgm_read_word (&MeineFunktionen[1]);
  MeineFunktion();
}

Menüs mit Funktionszeigern

Besonders bei Menüs ist es hilfreich, sich eine Struktur bestehend aus dem Menütext und der aufzurufenden Funktion zu definieren. Zusätzlich kann man auch Argumente mit in die Struktur aufnehmen, die beim Funktionsaufruf an die Funktion zu übergeben sind.

typedef void (*MenuFnct) (int);

struct MenuEntry
{
  char     Text[20];
  MenuFnct Function;
  int      ArgumentToFunction;
};

Ein Menü ist dann einfach ein Array aus derartigen Strukturelementen:

void HandleEdit (int arg)
{
}

void HandleCopy (int arg)
{
}

void HandlePaste (int arg)
{
}

struct MenuEntry MainMenu[] =
{
   // Der Funktion HandleEdit soll beim Aufruf 23 mitgegeben werden
   { "Edit",  HandleEdit,  23 },
   { "Copy",  HandleCopy,   0 },
   { "Paste", HandlePaste,  0 }
};

#define ARRAY_SIZE(X) (sizeof(X) / sizeof(*(X)))

void DoMenu (int NrEntries, struct MenuEntry Menu[])
{
  int i;
  int Auswahl;
  
  do
  {
    // Das Menue anzeigen. Für jeden Menuepunkt noch eine Zahl
    // davor stellen, damit der Benutzer auch was zum Eingeben hat
 
    for (i = 0; i < NrEntries; i++)
      printf ("%d: %s\n", i + 1, Menu[i].Text); 

    printf ("9: Exit\n\n");

    // Jetzt die Benutzereingabe abwarten ...

    printf ("Ihre Eingabe: ");
    scanf ("%d", &Auswahl);

    // ... und auswerten

    if (Auswahl == 9)
      return;

    if (Auswahl <= 0 || Auswahl > NrEntries)
    {
        printf ("Ungültige Eingabe: %d\n", Auswahl);
    }
    else
    {
        // Die Eingabe war gültig. Zugehörige Funktion aufrufen und der
        // Funktion den in der Menüdefinition vermerkten Wert mitgeben

        Auswahl = Auswahl - 1;

        Menu[Auswahl].Function (Menu[Auswahl].ArgumentToFunction);
    }
  }
  while (1);
}

int main()
{
  // Das Menü arbeiten lassen.
  // Die Funktion DoMenu ruft selbsttätig die zu den jeweiligen
  // Menüpunkten gehörenden Funktionen auf. DoMenu kommt erst
  // dann wieder zurück, wenn der Benutzer den Menüpunkt
  //    9: Exit
  // ausgewählt hat.

  DoMenu (ARRAY_SIZE (MainMenu), MainMenu);

  while (1)
  {}
}

Auf einem Mikrocontroller wird man die Ein/Ausgabe in der Regel nicht über printf/scanf abwickeln. Hier geht es aber um das Prinzip der Funktionszeiger und wie man mit ihnen arbeitet. Daher wurde die einfachste Art der Benutzerinteraktion gewählt. Gegebenenfalls müssen printf und scanf durch Funktionen auf dem konkreten System ersetzt werden.

Auch ist die Art und Weise, wie das Menü präsentiert bzw. die Benutzereingabe ausgewertet wird, nicht der Weisheit letzter Schluss. Anstatt den Benutzer Zahlen eingeben zu lassen, könnte man auch einen Auswahl-Balken vom Benutzer mit 2 Tasten über die Menüeinträge bewegen lassen. Oder einen Drehencoder verwenden, ...