Frage Wie funktionieren Funktionszeiger in C?


Ich hatte in letzter Zeit einige Erfahrung mit Funktionszeigern in C.

Um die eigenen Fragen beantworten zu können, habe ich beschlossen, eine kurze Zusammenfassung der Grundlagen für diejenigen zu erstellen, die einen schnellen Einstieg in das Thema benötigen.


1006
2018-05-08 15:49


Ursprung


Antworten:


Funktionszeiger in C

Beginnen wir mit einer Grundfunktion, die wir sein werden zeigt auf:

int addInt(int n, int m) {
    return n+m;
}

Als erstes definieren wir einen Zeiger auf eine Funktion, die 2 empfängt ints und gibt ein zurück int:

int (*functionPtr)(int,int);

Jetzt können wir sicher auf unsere Funktion hinweisen:

functionPtr = &addInt;

Nun, da wir einen Zeiger auf die Funktion haben, verwenden wir sie:

int sum = (*functionPtr)(2, 3); // sum == 5

Das Übergeben des Zeigers an eine andere Funktion ist im Grunde dasselbe:

int add2to3(int (*functionPtr)(int, int)) {
    return (*functionPtr)(2, 3);
}

Wir können Funktionszeiger auch in Rückgabewerten verwenden (versuchen Sie mitzuhalten, es wird unordentlich):

// this is a function called functionFactory which receives parameter n
// and returns a pointer to another function which receives two ints
// and it returns another int
int (*functionFactory(int n))(int, int) {
    printf("Got parameter %d", n);
    int (*functionPtr)(int,int) = &addInt;
    return functionPtr;
}

Aber es ist viel schöner, ein zu verwenden typedef:

typedef int (*myFuncDef)(int, int);
// note that the typedef name is indeed myFuncDef

myFuncDef functionFactory(int n) {
    printf("Got parameter %d", n);
    myFuncDef functionPtr = &addInt;
    return functionPtr;
}

1245
2018-05-08 15:49



Funktionszeiger in C können zur objektorientierten Programmierung in C verwendet werden.

Zum Beispiel sind die folgenden Zeilen in C geschrieben:

String s1 = newString();
s1->set(s1, "hello");

Ja das -> und das Fehlen eines new Der Operator ist ein toter Werbegeschenk, aber er scheint zu implizieren, dass wir den Text von einigen setzen String Klasse zu sein "hello".

Mit Funktionszeigern, Es ist möglich, Methoden in C zu emulieren.

Wie wird das erreicht?

Das String Klasse ist eigentlich ein struct mit einer Reihe von Funktionszeigern, die Methoden simulieren. Das Folgende ist eine teilweise Erklärung der String Klasse:

typedef struct String_Struct* String;

struct String_Struct
{
    char* (*get)(const void* self);
    void (*set)(const void* self, char* value);
    int (*length)(const void* self);
};

char* getString(const void* self);
void setString(const void* self, char* value);
int lengthString(const void* self);

String newString();

Wie zu sehen ist, die Methoden der String Klasse sind Funktionszeiger auf die deklarierte Funktion. Bei der Vorbereitung der Instanz der String, das newString Funktion wird aufgerufen, um die Funktionszeiger auf ihre jeweiligen Funktionen einzurichten:

String newString()
{
    String self = (String)malloc(sizeof(struct String_Struct));

    self->get = &getString;
    self->set = &setString;
    self->length = &lengthString;

    self->set(self, "");

    return self;
}

Zum Beispiel, die getString Funktion, die durch Aufruf der aufgerufen wird get Methode ist wie folgt definiert:

char* getString(const void* self_obj)
{
    return ((String)self_obj)->internal->value;
}

Eine Sache, die bemerkt werden kann, ist, dass es kein Konzept einer Instanz eines Objekts und Methoden gibt, die tatsächlich ein Teil eines Objekts sind, so dass ein "Selbstobjekt" bei jedem Aufruf übergeben werden muss. (Und das internal ist nur ein versteckter struct was in der Code-Liste früher weggelassen wurde - es ist eine Möglichkeit, Informationen zu verstecken, aber das ist nicht relevant für Funktionszeiger.)

Also, anstatt zu können s1->set("hello");, man muss das Objekt übergeben, um die Aktion auszuführen s1->set(s1, "hello").

Mit dieser kleinen Erklärung, die einen Verweis auf sich selbst aus dem Weg räumen muss, gehen wir zum nächsten Teil über Vererbung in C.

Nehmen wir an, wir wollen eine Unterklasse von machen String, sag ein ImmutableString. Um die Zeichenfolge unveränderlich zu machen, wird die set Methode wird nicht zugänglich sein, während der Zugriff auf get und length, und erzwinge den "Konstruktor", a zu akzeptieren char*:

typedef struct ImmutableString_Struct* ImmutableString;

struct ImmutableString_Struct
{
    String base;

    char* (*get)(const void* self);
    int (*length)(const void* self);
};

ImmutableString newImmutableString(const char* value);

Grundsätzlich sind die verfügbaren Methoden für alle Unterklassen wieder Funktionszeiger. Diesmal ist die Erklärung für die set Methode ist nicht vorhanden, daher kann es nicht in a aufgerufen werden ImmutableString.

Was die Umsetzung der ImmutableString, der einzige relevante Code ist die Funktion "Konstruktor", die newImmutableString:

ImmutableString newImmutableString(const char* value)
{
    ImmutableString self = (ImmutableString)malloc(sizeof(struct ImmutableString_Struct));

    self->base = newString();

    self->get = self->base->get;
    self->length = self->base->length;

    self->base->set(self->base, (char*)value);

    return self;
}

Beim Instantiieren der ImmutableString, die Funktionszeiger auf die get und length Methoden beziehen sich eigentlich auf die String.get und String.length Methode, indem Sie durch die base Variable, die intern gespeichert ist String Objekt.

Die Verwendung eines Funktionszeigers kann die Vererbung einer Methode aus einer Oberklasse erzielen.

Wir können weitermachen Polymorphismus in C.

Wenn wir zum Beispiel das Verhalten der length Methode, um zurückzukehren 0 die ganze Zeit in der ImmutableString Klasse aus irgendeinem Grund, alles, was getan werden müsste, ist:

  1. Fügen Sie eine Funktion hinzu, die als Überschreibung dienen soll length Methode.
  2. Gehen Sie zum "Konstruktor" und setzen Sie den Funktionszeiger auf das Überschreiben length Methode.

Hinzufügen eines Überschreibens length Methode in ImmutableString kann durchgeführt werden, indem ein hinzugefügt wird lengthOverrideMethod:

int lengthOverrideMethod(const void* self)
{
    return 0;
}

Dann ist der Funktionszeiger für die length Methode im Konstruktor ist an die angeschlossen lengthOverrideMethod:

ImmutableString newImmutableString(const char* value)
{
    ImmutableString self = (ImmutableString)malloc(sizeof(struct ImmutableString_Struct));

    self->base = newString();

    self->get = self->base->get;
    self->length = &lengthOverrideMethod;

    self->base->set(self->base, (char*)value);

    return self;
}

Jetzt, anstatt ein identisches Verhalten für die length Methode in ImmutableString Klasse als die String Klasse, jetzt die length Methode bezieht sich auf das Verhalten in der definiert lengthOverrideMethod Funktion.

Ich muss einen Disclaimer hinzufügen, dass ich immer noch lerne, wie man mit einem objektorientierten Programmierstil in C schreibt, also gibt es wahrscheinlich Punkte, die ich nicht gut erklärt habe, oder die in der Frage, wie man OOP am besten implementieren kann, abweichen in C. Aber mein Ziel war es, einen von vielen Verwendungen von Funktionszeigern zu veranschaulichen.

Weitere Informationen zur objektorientierten Programmierung in C finden Sie in den folgenden Fragen:


267



Der Leitfaden zum gefeuert werden: Wie man Funktionszeiger in GCC auf x86-Maschinen missbrauchen, indem Sie Ihren Code von Hand kompilieren:

  1. Gibt den aktuellen Wert im EAX-Register zurück

    int eax = ((int(*)())("\xc3 <- This returns the value of the EAX register"))();
    
  2. Schreiben Sie eine Tauschfunktion

    int a = 10, b = 20;
    ((void(*)(int*,int*))"\x8b\x44\x24\x04\x8b\x5c\x24\x08\x8b\x00\x8b\x1b\x31\xc3\x31\xd8\x31\xc3\x8b\x4c\x24\x04\x89\x01\x8b\x4c\x24\x08\x89\x19\xc3 <- This swaps the values of a and b")(&a,&b);
    
  3. Schreiben Sie einen For-Loop-Zähler an 1000 und rufen Sie jedes Mal eine Funktion auf

    ((int(*)())"\x66\x31\xc0\x8b\x5c\x24\x04\x66\x40\x50\xff\xd3\x58\x66\x3d\xe8\x03\x75\xf4\xc3")(&function); // calls function with 1->1000
    
  4. Sie können sogar eine rekursive Funktion schreiben, die zu 100 zählt

    const char* lol = "\x8b\x5c\x24\x4\x3d\xe8\x3\x0\x0\x7e\x2\x31\xc0\x83\xf8\x64\x7d\x6\x40\x53\xff\xd3\x5b\xc3\xc3 <- Recursively calls the function at address lol.";
    i = ((int(*)())(lol))(lol);
    

187



Eine meiner Lieblingsverwendungen für Funktionszeiger ist, wie billig und einfach Iteratoren -

#include <stdio.h>
#define MAX_COLORS  256

typedef struct {
    char* name;
    int red;
    int green;
    int blue;
} Color;

Color Colors[MAX_COLORS];


void eachColor (void (*fp)(Color *c)) {
    int i;
    for (i=0; i<MAX_COLORS; i++)
        (*fp)(&Colors[i]);
}

void printColor(Color* c) {
    if (c->name)
        printf("%s = %i,%i,%i\n", c->name, c->red, c->green, c->blue);
}

int main() {
    Colors[0].name="red";
    Colors[0].red=255;
    Colors[1].name="blue";
    Colors[1].blue=255;
    Colors[2].name="black";

    eachColor(printColor);
}

95



Funktionszeiger werden einfach deklariert, sobald Sie die grundlegenden Deklaratoren haben:

  • Ich würde: ID: ID ist ein
  • Zeiger: *D: D Zeiger auf
  • Funktion: D(<parameters>): D Funktionsaufnahme <Parameter> Rückkehr

Während D ein anderer Deklarator ist, der mit denselben Regeln erstellt wurde. Irgendwann endet es mit ID (siehe unten für ein Beispiel), welches der Name der deklarierten Entität ist. Lassen Sie uns versuchen, eine Funktion zu erstellen, die einen Zeiger auf eine Funktion nimmt, die nichts nimmt und int zurückgibt, und einen Zeiger auf eine Funktion zurückgibt, die ein Zeichen nimmt und int zurückgibt. Mit type-defs ist es so

typedef int ReturnFunction(char);
typedef int ParameterFunction(void);
ReturnFunction *f(ParameterFunction *p);

Wie Sie sehen, ist es ziemlich einfach, es mit typedefs aufzubauen. Ohne typedefs ist es nicht schwer, die obigen Deklaratorregeln konsistent anzuwenden. Wie Sie sehen, verpasste ich den Teil, auf den der Zeiger zeigt, und das, was die Funktion zurückgibt. Das steht ganz links in der Deklaration und ist nicht von Interesse: Es wird am Ende hinzugefügt, wenn der Deklarator bereits erstellt wurde. Lass uns das tun. Aufbauend konsequent, zuerst wortreich - zeigt die Struktur mit [ und ]:

function taking 
    [pointer to [function taking [void] returning [int]]] 
returning
    [pointer to [function taking [char] returning [int]]]

Wie Sie sehen, können Sie einen Typ vollständig beschreiben, indem Sie nacheinander Deklaratoren anhängen. Die Konstruktion kann auf zwei Arten erfolgen. Der eine ist von unten nach oben, beginnt mit der richtigen Sache (Blätter) und arbeitet sich bis zum Identifikator vor. Der andere Weg ist von oben nach unten, beginnend bei der Kennung, bis hinab zu den Blättern. Ich werde beide Wege zeigen.

Prost

Die Konstruktion beginnt mit dem Ding auf der rechten Seite: Die Sache ist zurückgekehrt, das ist die Funktion, die char nimmt. Um die Deklaratoren eindeutig zu machen, nummeriere ich sie:

D1(char);

Der Char-Parameter wurde direkt eingefügt, da es trivial ist. Hinzufügen eines Zeigers zum Deklarator durch Ersetzen D1 durch *D2. Beachten Sie, dass wir Klammern umschließen müssen *D2. Das kann man erkennen, wenn man den Vorrang der *-operator und der Funktionsaufruf-Operator (). Ohne unsere Klammern würde der Compiler es als lesen *(D2(char p)). Aber das wäre kein einfacher Ersatz für D1 *D2 mehr natürlich. Runde Klammern sind immer um Deklaratoren erlaubt. Du machst also nichts falsch, wenn du zu viel davon hinzufügst.

(*D2)(char);

Rückgabetyp ist abgeschlossen! Jetzt ersetzen wir D2 durch den Funktionsdeklarator Funktionsaufnahme <parameters> Rückkehr, welches ist D3(<parameters>) wo wir jetzt sind.

(*D3(<parameters>))(char)

Beachten Sie, dass keine Klammern erforderlich sind, da wir wollen  D3 diesmal ein Funktion-Deklarator und kein Pointer-Deklarator sein. Großartig, nur noch übrig bleiben die Parameter dafür. Der Parameter ist genauso wie der Rückgabetyp, nur mit char ersetzt durch void. Also ich werde es kopieren:

(*D3(   (*ID1)(void)))(char)

Ich habe ersetzt D2 durch ID1, da wir mit diesem Parameter fertig sind (es ist bereits ein Zeiger auf eine Funktion - keine Notwendigkeit für einen anderen Deklarator). ID1 wird der Name des Parameters sein. Jetzt, oben habe ich am Ende gesagt, fügt man den Typ hinzu, den alle diese Deklaratoren modifizieren - den, der ganz links von jeder Deklaration erscheint. Für Funktionen wird dies zum Rückgabetyp. Für Zeiger, die auf Typ usw. zeigen ... Es ist interessant, wenn der Typ aufgeschrieben wird, wird es in der umgekehrten Reihenfolge angezeigt, ganz rechts :) Wie auch immer, es ersetzt die vollständige Deklaration. Beide Male int Na sicher.

int (*ID0(int (*ID1)(void)))(char)

Ich habe die Kennung der Funktion aufgerufen ID0 in diesem Beispiel.

Von oben nach unten

Dies beginnt bei der Kennung ganz links in der Beschreibung des Typs und umschließt den Deklarator, wenn wir unseren Weg durch die Rechte gehen. Beginnen mit Funktionsaufnahme <Parameter> Rückkehr

ID0(<parameters>)

Die nächste Sache in der Beschreibung (nach "Rückkehr") war Zeiger auf. Lass es uns einbeziehen:

*ID0(<parameters>)

Dann war das nächste Functon nehmen <Parameter> Rückkehr. Der Parameter ist ein einfaches Zeichen, also fügen wir es sofort wieder ein, da es wirklich trivial ist.

(*ID0(<parameters>))(char)

Beachten Sie die Klammern, die wir hinzugefügt haben, da wir das wieder wollen * bindet zuerst, und dann das (char). Sonst würde es lesen Funktionsaufnahme <Parameter> Rückgabe der Funktion .... Nein, Funktionen, die Funktionen zurückgeben, sind nicht einmal erlaubt.

Jetzt müssen wir nur setzen <Parameter>. Ich werde eine kurze Version der Ableitung zeigen, da ich denke, dass Sie bereits jetzt die Idee haben, wie es geht.

pointer to: *ID1
... function taking void returning: (*ID1)(void)

Einfach gesagt int vor den Deklaratoren haben wir es mit Bottom-Up gemacht, und wir sind fertig

int (*ID0(int (*ID1)(void)))(char)

Das Schöne

Ist Bottom-Up oder Top-Down besser? Ich bin es gewohnt, von unten nach oben zu gehen, aber einige Leute können sich besser mit Top-Down fühlen. Es ist eine Frage des Geschmacks, denke ich. Übrigens, wenn Sie alle Operatoren in dieser Deklaration anwenden, erhalten Sie am Ende einen int:

int v = (*ID0(some_function_pointer))(some_char);

Das ist eine nette Eigenschaft von Deklarationen in C: Die Deklaration behauptet, dass, wenn diese Operatoren in einem Ausdruck verwendet werden, der den Bezeichner verwendet, der Typ ganz links entsteht. So ist es auch für Arrays.

Hoffe, dass dir dieses kleine Tutorial gefallen hat! Jetzt können wir uns darauf beziehen, wenn sich die Leute über die seltsame Deklarationssyntax von Funktionen wundern. Ich habe versucht, so wenig wie möglich interne C. zu setzen. Fühlen Sie sich frei, Sachen darin zu redigieren / zu reparieren.


23



Eine weitere gute Verwendung für Funktionszeiger:
Zwischen den Versionen schmerzfrei wechseln

Sie sind sehr praktisch, wenn Sie verschiedene Funktionen zu unterschiedlichen Zeiten oder in unterschiedlichen Entwicklungsphasen verwenden möchten. Zum Beispiel entwickle ich eine Anwendung auf einem Host - Computer, der eine Konsole hat, aber die endgültige Version der Software wird auf ein Avnet ZedBoard gesetzt (das Ports für Displays und Konsolen hat, aber für die endgültige Freigabe). Also während der Entwicklung werde ich verwenden printf um Status- und Fehlermeldungen anzuzeigen, aber wenn ich fertig bin, möchte ich nichts drucken. Folgendes habe ich getan:

version.h

// First, undefine all macros associated with version.h
#undef DEBUG_VERSION
#undef RELEASE_VERSION
#undef INVALID_VERSION


// Define which version we want to use
#define DEBUG_VERSION       // The current version
// #define RELEASE_VERSION  // To be uncommented when finished debugging

#ifndef __VERSION_H_      /* prevent circular inclusions */
    #define __VERSION_H_  /* by using protection macros */
    void board_init();
    void noprintf(const char *c, ...); // mimic the printf prototype
#endif

// Mimics the printf function prototype. This is what I'll actually 
// use to print stuff to the screen
void (* zprintf)(const char*, ...); 

// If debug version, use printf
#ifdef DEBUG_VERSION
    #include <stdio.h>
#endif

// If both debug and release version, error
#ifdef DEBUG_VERSION
#ifdef RELEASE_VERSION
    #define INVALID_VERSION
#endif
#endif

// If neither debug or release version, error
#ifndef DEBUG_VERSION
#ifndef RELEASE_VERSION
    #define INVALID_VERSION
#endif
#endif

#ifdef INVALID_VERSION
    // Won't allow compilation without a valid version define
    #error "Invalid version definition"
#endif

Im version.c Ich werde die 2 Funktionsprototypen definieren, die in version.h

Version.c

#include "version.h"

/*****************************************************************************/
/**
* @name board_init
*
* Sets up the application based on the version type defined in version.h.
* Includes allowing or prohibiting printing to STDOUT.
*
* MUST BE CALLED FIRST THING IN MAIN
*
* @return    None
*
*****************************************************************************/
void board_init()
{
    // Assign the print function to the correct function pointer
    #ifdef DEBUG_VERSION
        zprintf = &printf;
    #else
        // Defined below this function
        zprintf = &noprintf;
    #endif
}

/*****************************************************************************/
/**
* @name noprintf
*
* simply returns with no actions performed
*
* @return   None
*
*****************************************************************************/
void noprintf(const char* c, ...)
{
    return;
}

Beachten Sie, wie der Funktionszeiger in Prototyp erstellt wird version.h wie

void (* zprintf)(const char *, ...);

Wenn es in der Anwendung referenziert wird, wird es ausgeführt, wo immer es hinzeigt, was noch zu definieren ist.

Im version.cBeachten Sie in der board_init()Funktion wo zprintf wird eine eindeutige Funktion zugewiesen (deren Funktionssignatur übereinstimmt), abhängig von der Version, die in definiert ist version.h

zprintf = &printf zprintf ruft printf zum Debuggen auf

oder

zprintf = &noprint zprintf kehrt einfach zurück und führt keinen unnötigen Code aus

Das Ausführen des Codes sieht folgendermaßen aus:

mainProg.c

#include "version.h"
#include <stdlib.h>
int main()
{
    // Must run board_init(), which assigns the function
    // pointer to an actual function
    board_init();

    void *ptr = malloc(100); // Allocate 100 bytes of memory
    // malloc returns NULL if unable to allocate the memory.

    if (ptr == NULL)
    {
        zprintf("Unable to allocate memory\n");
        return 1;
    }

    // Other things to do...
    return 0;
}

Der obige Code wird verwendet printf wenn im Debug-Modus, oder nichts tun, wenn im Release-Modus. Dies ist viel einfacher, als das gesamte Projekt durchzugehen und Code zu kommentieren oder zu löschen. Alles, was ich tun muss, ist die Version zu ändern version.h und der Code wird den Rest erledigen!


21



Der Funktionszeiger wird normalerweise von typedef definiert und als Parameter und Rückgabewert verwendet.

Obenstehende Antworten haben schon viel erklärt, ich gebe nur ein vollständiges Beispiel:

#include <stdio.h>

#define NUM_A 1
#define NUM_B 2

// define a function pointer type
typedef int (*two_num_operation)(int, int);

// an actual standalone function
static int sum(int a, int b) {
    return a + b;
}

// use function pointer as param,
static int sum_via_pointer(int a, int b, two_num_operation funp) {
    return (*funp)(a, b);
}

// use function pointer as return value,
static two_num_operation get_sum_fun() {
    return &sum;
}

// test - use function pointer as variable,
void test_pointer_as_variable() {
    // create a pointer to function,
    two_num_operation sum_p = &sum;
    // call function via pointer
    printf("pointer as variable:\t %d + %d = %d\n", NUM_A, NUM_B, (*sum_p)(NUM_A, NUM_B));
}

// test - use function pointer as param,
void test_pointer_as_param() {
    printf("pointer as param:\t %d + %d = %d\n", NUM_A, NUM_B, sum_via_pointer(NUM_A, NUM_B, &sum));
}

// test - use function pointer as return value,
void test_pointer_as_return_value() {
    printf("pointer as return value:\t %d + %d = %d\n", NUM_A, NUM_B, (*get_sum_fun())(NUM_A, NUM_B));
}

int main() {
    test_pointer_as_variable();
    test_pointer_as_param();
    test_pointer_as_return_value();

    return 0;
}

13