Menu

Funktionszeiger in C: Syntax, Callbacks und qsort

Auch Funktionen haben Adressen, und ein Funktionszeiger speichert eine davon. Diese Seite entschlüsselt die Deklarationssyntax, zeigt, wie typedef sie lesbar macht, und arbeitet sich zu Callbacks, qsort mit eigenem Vergleicher und Sprungtabellen vor.

Diese Seite enthält ausführbare Editoren - bearbeiten, ausführen und Ausgabe sofort sehen.

Übersetzter Code liegt genauso im Speicher wie Daten, eine Funktion hat also eine Adresse. Ein Funktionszeiger ist eine Variable, die eine solche speichert - und sobald du eine Funktion speichern kannst, kannst du eine als Argument übergeben, eine Tabelle davon halten oder zur Laufzeit zwischen ihnen wählen.

Das ist die Fähigkeit hinter Sortieren mit eigener Ordnung, Ereignisbehandlern, Plugin-Schnittstellen und Zustandsmaschinen. Es ist zugleich das Stück C-Syntax, bei dem ein Leser am ehesten innehält und die Augen zusammenkneift, also fangen wir dort an.

Die Deklarationssyntax

Nimm eine gewöhnliche Funktion:

int add(int a, int b);

Um einen Zeiger zu deklarieren, der ihre Adresse fassen kann, ersetze den Namen durch (*name):

int (*op)(int, int);

Lies es von innen: op ist ein Zeiger auf eine Funktion, die (int, int) nimmt und int liefert.

Die Klammern sind keine Zierde. Ohne sie bindet das * an den Rückgabetyp:

int (*f)(int);   // Zeiger auf eine Funktion, die int nimmt und int liefert
int *g(int);     // FUNKTION, die int nimmt und int* liefert - völlig anders

Hier funktioniert es von Anfang bis Ende:

Beide Aufrufformen funktionieren, weil sich ein Funktionsbezeichner automatisch in einen Zeiger umwandelt. Schreibe op(10, 3); die Schreibweise (*op)(10, 3) ist ein Überbleibsel.

Eine Anmerkung zum letzten printf: %p erwartet einen Objektzeiger, und Funktionszeiger sind formal eine eigene Familie, einen davon auszugeben ist also nicht streng portabel. Der Cast hält Compiler auf den Plattformen ruhig, auf denen es überhaupt geht; du wirst selten einen ausgeben müssen.

typedef macht es lesbar

Die Deklarationssyntax wird schnell hässlich, und typedef ist das übliche Gegenmittel:

Vergleiche int apply(BinaryOp op, int x, int y) mit der rohen Form int apply(int (*op)(int, int), int x, int y). Dieselbe Bedeutung; eine davon kannst du auf einen Blick lesen. Siehe typedef für die allgemeinen Regeln.

Callbacks: den Aufrufer das Verhalten liefern lassen

Ein Callback ist eine Funktion, die du einer anderen Funktion reichst, damit sie dich im richtigen Moment zurückrufen kann. So kann ein Stück Code die Struktur einer Aufgabe übernehmen, während der Aufrufer die Entscheidung liefert.

Ohne Funktionszeiger würdest du count_even, count_positive und count_big schreiben - drei Kopien derselben Schleife. Mit ihnen wird die Schleife einmal geschrieben und der Test ist der Parameter.

Der eigentliche Gewinn: qsort

qsort aus der Standardbibliothek ist das Funktionszeiger-Beispiel, das du tatsächlich benutzen wirst. Es sortiert ein Array von irgendetwas, und das gelingt, weil es über die Elemente nichts weiß außer ihrer Größe und einem Vergleicher, den du lieferst.

void qsort(void *base, size_t nmemb, size_t size,
           int (*compar)(const void *, const void *));

Der Vergleicher erhält zwei const void * - die Adressen zweier Elemente - und liefert eine negative Zahl, wenn das erste vor dem zweiten einsortiert wird, null bei Gleichstand, positiv, wenn es danach kommt.

Zwei Gewohnheiten aus diesem Vergleicher. Schreibe nie return x - y; - es sieht clever aus und läuft bei großen oder negativen Werten über, was ein falsches Vorzeichen und ein fehlsortiertes Array ergibt. Caste vor dem Dereferenzieren, nicht danach: *(const int *)a, denn *a auf einem void * hat nichts zu lesen.

Dieselbe Maschinerie sortiert Strukturen nach jedem Feld, das du magst:

Das Idiom (q->score > p->score) - (q->score < p->score) liefert genau -1, 0 oder 1 ohne Subtraktion und ohne Überlauf. Und strcmp hat bereits die richtige Rückgabekonvention, ein Zeichenketten-Vergleicher ist also ein Einzeiler.

Tabellen von Funktionszeigern

Ein Array von Funktionszeigern macht aus einer Kette von if/else ein Nachschlagen. So verteilen Interpreter Opcodes und so führen Menüsysteme Befehle aus.

Eine Operation hinzuzufügen heißt jetzt, eine Zeile hinzuzufügen, statt ein switch zu bearbeiten. Kombiniere das mit Strukturen, und du hast die übliche C-Plugin-Form: Eine Struktur aus Funktionszeigern ist das, was „eine Schnittstelle" in dieser Sprache bedeutet.

Dinge, die beißen

Signaturen müssen exakt passen. Der Typ eines Funktionszeigers schließt seine Parametertypen und seinen Rückgabetyp ein. Eine unpassende Funktion zuzuweisen ist eine Verletzung der Sprachregeln, und ein Aufruf über einen falsch typisierten Zeiger ist undefiniertes Verhalten, auch wenn es „funktioniert". Caste nicht die Signatur eines Vergleichers, um eine Warnung zum Schweigen zu bringen - repariere den Vergleicher.

Ein Null-Funktionszeiger ist immer noch ein Nullzeiger. Initialisiere mit NULL und prüfe vor dem Aufruf, genau wie bei Datenzeigern:

if (handler != NULL) {
    handler(event);
}

Siehe Nullzeiger, warum diese Prüfung ihr Geld wert ist.

void-Zeiger verlieren die Typsicherheit, und du bist das Typsystem. In einem qsort-Vergleicher hindert dich nichts daran, in den falschen Typ zu casten. Sortierst du ein Array von double mit compare_ints, kompiliert es sauber und produziert Unsinn.

Die Deklaratorsyntax verschachtelt sich. Eine Funktion, die einen Funktionszeiger liefert, ist int (*get_op(char c))(int, int);. Wenn du dich dabei ertappst, das zu schreiben, greif zu typedef:

typedef int (*BinaryOp)(int, int);
BinaryOp get_op(char c);           // dasselbe, lesbar

Häufig gestellte Fragen

Wie deklariert man einen Funktionszeiger in C?

Schreibe die Signatur der Funktion mit (*name) dort, wo der Funktionsname stünde: int (*op)(int, int); deklariert op als Zeiger auf eine Funktion, die zwei ints nimmt und int liefert. Die Klammern um *op sind Pflicht - ohne sie deklarierst du eine Funktion, die einen Zeiger liefert.

Wie ruft man eine Funktion über einen Zeiger auf?

Entweder op(3, 4) oder (*op)(3, 4) - beides ist erlaubt und tut dasselbe, weil sich ein Funktionsbezeichner automatisch in einen Zeiger umwandelt. Moderner C-Code benutzt die schlichte Form op(3, 4).

Was ist ein Callback in C?

Eine Funktion, die du einer anderen Funktion reichst, damit sie dich zurückrufen kann. qsort ist das klassische Beispiel: Du übergibst einen Vergleicher, und qsort ruft ihn auf, wann immer es wissen muss, welches von zwei Elementen zuerst kommt. Das ist es, was eine einzige Sortierroutine für jeden Typ arbeiten lässt.

Braucht man das & beim Nehmen der Adresse einer Funktion?

Nein. Ein Funktionsname wandelt sich bereits in einen Zeiger auf diese Funktion um, op = add; und op = &add; sind also gleichwertig. Der meiste C-Code lässt das & weg.

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S