Wie funktionieren Funktionszeiger in C?


1233

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

Ausgehend von der Tradition, Ihre eigenen Fragen zu beantworten, habe ich beschlossen, eine kleine Zusammenfassung der Grundlagen für diejenigen zu erstellen, die sich schnell mit dem Thema befassen möchten.


35
Außerdem: Eine ausführliche Analyse der C-Zeiger finden Sie unter blogs.oracle.com/ksplice/entry/the_ksplice_pointer_challenge . Die Programmierung von Grund auf zeigt auch, wie sie auf Maschinenebene funktionieren. Das Verständnis des "Speichermodells" von C ist sehr nützlich, um die Funktionsweise von C-Zeigern zu verstehen.
Abbafei

8
Tolle Infos. Nach dem Titel hätte ich allerdings erwartet, wirklich eine Erklärung zu sehen, wie "Funktionszeiger" funktionieren, nicht wie sie codiert sind :)
Bogdan Alexandru

Antworten:


1478

Funktionszeiger in C.

Beginnen wir mit einer Grundfunktion, auf die wir verweisen werden :

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

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

int (*functionPtr)(int,int);

Jetzt können wir sicher auf unsere Funktion verweisen:

functionPtr = &addInt;

Nachdem wir nun einen Zeiger auf die Funktion haben, verwenden wir ihn:

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 chaotisch):

// 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 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;
}

19
Danke für die tollen Infos. Können Sie einen Einblick geben, wo Funktionszeiger verwendet werden oder was besonders nützlich ist?
Rich.Carpenter

326
"functionPtr = & addInt;" kann auch geschrieben werden (und ist es oft) als "functionPtr = addInt;" Dies gilt auch, da der Standard besagt, dass ein Funktionsname in diesem Zusammenhang in die Adresse der Funktion konvertiert wird.
Hlovdal

22
hlovdal, in diesem Zusammenhang ist es interessant zu erklären, dass dies das Schreiben von functionPtr = ****************** addInt ermöglicht;
Johannes Schaub - litb

105
@ Rich.Carpenter Ich weiß, dass dies 4 Jahre zu spät ist, aber ich denke, andere Leute könnten davon profitieren: Funktionszeiger sind nützlich, um Funktionen als Parameter an andere Funktionen zu übergeben . Ich musste aus irgendeinem Grund viel suchen, um diese Antwort zu finden. Im Grunde gibt es C Pseudo erstklassige Funktionalität.
Giant91

22
@ Rich.Carpenter: Funktionszeiger sind gut für die Laufzeit-CPU-Erkennung. Verwenden Sie mehrere Versionen einiger Funktionen, um SSE, Popcnt, AVX usw. zu nutzen. Stellen Sie beim Start Ihre Funktionszeiger auf die beste Version jeder Funktion für die aktuelle CPU ein. Rufen Sie in Ihrem anderen Code einfach den Funktionszeiger auf, anstatt überall bedingte Verzweigungen für die CPU-Funktionen zu haben. Dann können Sie eine komplizierte Logik erstellen, um zu entscheiden, ob diese CPU zwar unterstützt pshufb, aber langsam ist, sodass die frühere Implementierung immer noch schneller ist. x264 / x265 nutzen dies ausgiebig und sind Open Source.
Peter Cordes

304

Funktionszeiger in C können verwendet werden, um eine objektorientierte Programmierung in C durchzuführen.

Die folgenden Zeilen sind beispielsweise in C geschrieben:

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

Ja, das ->und das Fehlen eines newOperators ist ein totes Geschenk, aber es scheint sicher zu implizieren, dass wir den Text einer StringKlasse so einstellen "hello".

Durch die Verwendung von Funktionszeigern ist es möglich, Methoden in C zu emulieren .

Wie wird das erreicht?

Die StringKlasse besteht eigentlich aus einer structReihe von Funktionszeigern, mit denen Methoden simuliert werden können. Das Folgende ist eine Teildeklaration der StringKlasse:

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, sind die Methoden der StringKlasse tatsächlich Funktionszeiger auf die deklarierte Funktion. Bei der Vorbereitung der Instanz von Stringwird die newStringFunktion 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;
}

Die getStringFunktion, die durch Aufrufen der getMethode aufgerufen wird , ist beispielsweise 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 gibt und Methoden hat, die tatsächlich Teil eines Objekts sind, so dass bei jedem Aufruf ein "Selbstobjekt" übergeben werden muss. (Und das internalist nur ein verstecktes structElement, das zuvor in der Codeliste weggelassen wurde - es ist eine Möglichkeit, Informationen auszublenden, aber das ist für Funktionszeiger nicht relevant.)

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

Da diese kleine Erklärung einen Verweis auf sich selbst aus dem Weg räumen muss, fahren wir mit dem nächsten Teil fort, der die Vererbung in C ist .

Nehmen wir an, wir möchten eine Unterklasse bilden String, sagen wir eine ImmutableString. Um die Zeichenfolge unveränderlich zu machen, kann auf die setMethode nicht zugegriffen werden, während der Zugriff auf getund beibehalten wird length, und der "Konstruktor" wird gezwungen, Folgendes 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. Dieses Mal ist die Deklaration für die setMethode nicht vorhanden, daher kann sie nicht in a aufgerufen werden ImmutableString.

Für die Implementierung von ImmutableStringist der einzige relevante Code die "Konstruktor" -Funktion, 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 Instanziieren der verweisen ImmutableStringdie Funktionszeiger auf die Methoden getund lengthtatsächlich auf die Methode String.getund String.length, indem sie die baseVariable durchlaufen , die ein intern gespeichertes StringObjekt ist.

Durch die Verwendung eines Funktionszeigers kann die Vererbung einer Methode von einer Oberklasse erreicht werden.

Wir können den Polymorphismus in C weiter fortsetzen .

Wenn wir zum Beispiel das Verhalten der lengthMethode ändern wollten, um aus irgendeinem Grund die ganze 0Zeit in der ImmutableStringKlasse zurückzukehren, müssten wir nur Folgendes tun:

  1. Fügen Sie eine Funktion hinzu, die als übergeordnete lengthMethode dienen soll.
  2. Gehen Sie zum "Konstruktor" und setzen Sie den Funktionszeiger auf die überschreibende lengthMethode.

Das Hinzufügen einer überschreibenden lengthMethode in ImmutableStringkann durch Hinzufügen eines lengthOverrideMethod:

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

Anschließend wird der Funktionszeiger für die lengthMethode im Konstruktor an Folgendes 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;
}

Anstatt ein identisches Verhalten für die lengthMethode in der ImmutableStringKlasse wie die StringKlasse zu haben, lengthverweist die Methode jetzt auf das in der lengthOverrideMethodFunktion definierte Verhalten .

Ich muss einen Haftungsausschluss hinzufügen, dass ich noch lerne, wie man mit einem objektorientierten Programmierstil in C schreibt. Daher gibt es wahrscheinlich Punkte, die ich nicht gut erklärt habe oder die in Bezug auf die beste Implementierung von OOP möglicherweise falsch sind in C. Aber mein Ziel war es, eine von vielen Verwendungen von Funktionszeigern zu veranschaulichen.

Weitere Informationen zum Ausführen der objektorientierten Programmierung in C finden Sie in den folgenden Fragen:


22
Diese Antwort ist schrecklich! Dies impliziert nicht nur, dass OO irgendwie von der Punktnotation abhängt, sondern fördert auch das Einbringen von Müll in Ihre Objekte!
Alexei Averchenko

27
Dies ist in Ordnung, aber nicht in der Nähe des OO im C-Stil. Was Sie fehlerhaft implementiert haben, ist prototypbasiertes OO im Javascript-Stil. Um OO im C ++ / Pascal-Stil zu erhalten, müssen Sie: 1. eine const-Struktur für eine virtuelle Tabelle jeder Klasse mit virtuellen Mitgliedern haben. 2. Zeigen Sie in polymorphen Objekten auf diese Struktur. 3. Rufen Sie virtuelle Methoden über die virtuelle Tabelle und alle anderen Methoden direkt auf - normalerweise unter Einhaltung einer ClassName_methodNameFunktionsnamenskonvention. Nur dann erhalten Sie die gleichen Laufzeit- und Speicherkosten wie in C ++ und Pascal.
Stellen Sie Monica

19
OO mit einer Sprache zu arbeiten, die nicht OO sein soll, ist immer eine schlechte Idee. Wenn Sie OO möchten und immer noch C haben, arbeiten Sie einfach mit C ++.
Rbaleksandar

20
@rbaleksandar Sagen Sie das den Linux-Kernel-Entwicklern. "Immer eine schlechte Idee" ist streng Ihre Meinung, mit der ich nicht einverstanden bin.
Jonathon Reinhart

6
Ich mag diese Antwort, aber wirke keine Malloc
Katze

227

Die Anleitung zum Abfeuern: So missbrauchen Sie Funktionszeiger in GCC auf x86-Computern, indem Sie Ihren Code von Hand kompilieren:

Diese Zeichenfolgenliterale sind Bytes mit 32-Bit-x86-Maschinencode. 0xC3ist eine x86- retAnweisung .

Normalerweise würden Sie diese nicht von Hand schreiben nasm, sondern in Assemblersprache schreiben und dann einen Assembler verwenden , um sie zu einer flachen Binärdatei zusammenzusetzen, die Sie in ein C-String-Literal hexdumpen.

  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 Swap-Funktion

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

Beachten Sie, dass Compiler Zeichenfolgenliterale in den .rodataAbschnitt (oder .rdataunter Windows) einfügen, der als Teil des Textsegments verknüpft ist (zusammen mit Code für Funktionen).

Das Textsegment hat Read + Exec Erlaubnis, Stringliterale zu Funktionszeiger funktioniert , ohne dass so Casting mprotect()oder VirtualProtect()Systemaufrufe wie Sie für dynamisch zugewiesenen Speicher benötigen würde. (Oder gcc -z execstackverknüpft das Programm mit Stack + Datensegment + ausführbarer Heap-Datei als schnellen Hack.)


Um diese zu zerlegen, können Sie dies kompilieren, um den Bytes eine Bezeichnung zu geben, und einen Disassembler verwenden.

// at global scope
const char swap[] = "\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";

Beim Kompilieren gcc -c -m32 foo.cund Zerlegen mit objdump -D -rwC -Mintelkönnen wir die Assembly abrufen und feststellen, dass dieser Code den ABI verletzt, indem er EBX (ein aufruferhaltenes Register) blockiert und im Allgemeinen ineffizient ist.

00000000 <swap>:
   0:   8b 44 24 04             mov    eax,DWORD PTR [esp+0x4]   # load int *a arg from the stack
   4:   8b 5c 24 08             mov    ebx,DWORD PTR [esp+0x8]   # ebx = b
   8:   8b 00                   mov    eax,DWORD PTR [eax]       # dereference: eax = *a
   a:   8b 1b                   mov    ebx,DWORD PTR [ebx]
   c:   31 c3                   xor    ebx,eax                # pointless xor-swap
   e:   31 d8                   xor    eax,ebx                # instead of just storing with opposite registers
  10:   31 c3                   xor    ebx,eax
  12:   8b 4c 24 04             mov    ecx,DWORD PTR [esp+0x4]  # reload a from the stack
  16:   89 01                   mov    DWORD PTR [ecx],eax     # store to *a
  18:   8b 4c 24 08             mov    ecx,DWORD PTR [esp+0x8]
  1c:   89 19                   mov    DWORD PTR [ecx],ebx
  1e:   c3                      ret    

  not shown: the later bytes are ASCII text documentation
  they're not executed by the CPU because the ret instruction sends execution back to the caller

Dieser Maschinencode funktioniert (wahrscheinlich) in 32-Bit-Code unter Windows, Linux, OS X usw.: Die Standardaufrufkonventionen für alle diese Betriebssysteme übergeben Argumente auf dem Stapel, anstatt effizienter in Registern. EBX bleibt jedoch in allen normalen Anrufkonventionen aufruffähig. Wenn Sie es also als Arbeitsregister verwenden, ohne es zu speichern oder wiederherzustellen, kann der Anrufer leicht abstürzen.


8
Hinweis: Dies funktioniert nicht, wenn die Verhinderung der Datenausführung aktiviert ist (z. B. unter Windows XP SP2 +), da C-Zeichenfolgen normalerweise nicht als ausführbar markiert sind.
SecurityMatt

5
Hallo Matt! Abhängig von der Optimierungsstufe fügt GCC häufig Zeichenfolgenkonstanten in das TEXT-Segment ein, sodass dies auch bei neueren Windows-Versionen funktioniert, sofern Sie diese Art der Optimierung nicht zulassen. (IIRC, die MINGW-Version zum Zeitpunkt meines Beitrags vor über zwei Jahren, fügt String-Literale auf der Standardoptimierungsstufe ein)
Lee

10
Könnte jemand bitte erklären, was hier passiert? Was sind diese seltsam aussehenden String-Literale?
Ajay

56
@ajay Es sieht so aus, als würde er rohe Hexidezimalwerte (zum Beispiel '\ x00' ist dasselbe wie '/ 0', beide sind gleich 0) in eine Zeichenfolge schreiben, die Zeichenfolge dann in einen C-Funktionszeiger umwandeln und dann ausführen der C-Funktionszeiger, weil er der Teufel ist.
ejk314

3
Hallo FUZxxl, ich denke, es kann je nach Compiler und Betriebssystemversion variieren. Der obige Code scheint auf codepad.org einwandfrei zu funktionieren. codepad.org/FMSDQ3ME
Lee

115

Eine meiner Lieblingsanwendungen für Funktionszeiger sind billige und einfache 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);
}

7
Sie sollten auch einen Zeiger auf benutzerdefinierte Daten übergeben, wenn Sie eine Ausgabe aus Iterationen extrahieren möchten (Think Closures).
Alexei Averchenko

1
Einverstanden. Alle meine Iteratoren sehen so aus : int (*cb)(void *arg, ...). Mit dem Rückgabewert des Iterators kann ich auch vorzeitig anhalten (wenn nicht Null).
Jonathon Reinhart

24

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

  • ID: ID: ID ist ein
  • Zeiger: *D: D Zeiger auf
  • Funktion: D(<parameters>): D Funktion <Aufnahmeparameter >Rückkehr

Während D ein anderer Deklarator ist, der nach denselben Regeln erstellt wurde. Am Ende endet es irgendwo mit ID(siehe unten für ein Beispiel), dem Namen der deklarierten Entität. Versuchen wir, 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. Bei Typ-Defs ist das 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 auch mit den oben genannten Deklaratorregeln, die konsistent angewendet werden, nicht schwer. Wie Sie sehen, habe ich den Teil verpasst, 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 man den Deklarator bereits aufgebaut hat. Lass uns das tun. Bauen Sie es konsequent auf, zuerst wortreich - zeigen Sie die Struktur mit [und ]:

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

Wie Sie sehen, kann man einen Typ vollständig beschreiben, indem man Deklaratoren nacheinander anfügt. Der Bau kann auf zwei Arten erfolgen. Eine ist von unten nach oben, angefangen mit der richtigen Sache (Blätter) bis hin zur Kennung. Der andere Weg ist von oben nach unten, beginnend bei der Kennung, bis hinunter zu den Blättern. Ich werde beide Wege zeigen.

Prost

Der Bau beginnt mit dem Ding auf der rechten Seite: Das Ding, das zurückgegeben wird, ist die Funktion, die char übernimmt. Um die Deklaratoren getrennt zu halten, werde ich sie nummerieren:

D1(char);

Der char-Parameter wurde direkt eingefügt, da er trivial ist. Hinzufügen eines Zeigers zum Deklarator durch Ersetzen D1durch *D2. Beachten Sie, dass wir Klammern umschließen müssen *D2. Dies kann durch Nachschlagen der Priorität des *-operatorund des Funktionsaufrufoperators festgestellt werden (). Ohne unsere Klammern würde der Compiler es als lesen *(D2(char p)). Aber das wäre natürlich kein einfacher Ersatz für D1 *D2mehr. Um Deklaratoren herum sind immer Klammern zulässig. Sie machen also nichts falsch, wenn Sie tatsächlich zu viel davon hinzufügen.

(*D2)(char);

Rückgabetyp ist vollständig! Lassen Sie uns nun D2durch die Funktionsdeklaratorfunktion ersetzen, die die <parameters>Rückgabe übernimmt , und D3(<parameters>)die wir gerade sind.

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

Beachten Sie, dass keine Klammern erforderlich sind, da wir diesmal ein Funktionsdeklarator und kein Zeigerdeklarator sein möchten D3 . Großartig, nur noch die Parameter dafür. Der Parameter wird genauso ausgeführt wie der Rückgabetyp, nur durch charersetzt durch void. Also werde ich es kopieren:

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

Ich habe ersetzt D2durch ID1, da wir mit diesem Parameter fertig sind (es ist bereits ein Zeiger auf eine Funktion - kein weiterer Deklarator erforderlich). ID1wird der Name des Parameters sein. Nun, ich habe oben am Ende gesagt, dass man den Typ hinzufügt, den alle diese Deklaratoren ändern - den, der ganz links von jeder Deklaration erscheint. Für Funktionen wird dies zum Rückgabetyp. Für Zeiger, auf die der Typ zeigt usw. Es ist interessant, wenn der Typ aufgeschrieben wird, er wird in der entgegengesetzten Reihenfolge ganz rechts angezeigt :) Wenn Sie ihn ersetzen, erhalten Sie die vollständige Deklaration. Beide Male intnatürlich.

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

Ich habe ID0in diesem Beispiel die Kennung der Funktion aufgerufen .

Von oben nach unten

Dies beginnt bei der Kennung ganz links in der Beschreibung des Typs und umschließt diesen Deklarator, während wir uns durch die rechte Seite bewegen. Beginnen Sie mit Funktion <Aufnahmeparameter >Rückkehr

ID0(<parameters>)

Das nächste in der Beschreibung (nach "Zurückkehren") war der Zeiger auf . Lassen Sie es uns aufnehmen:

*ID0(<parameters>)

Dann war das nächste, was funktionierte, indem <Parameter >zurückgegeben wurden . Der Parameter ist ein einfaches Zeichen, daher setzen wir ihn sofort wieder ein, da er wirklich trivial ist.

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

Beachten Sie die Klammern, die wir hinzugefügt haben, da wir wieder möchten, dass zuerst die *Bindungen und dann die (char). Sonst wäre es lesen Funktion unter <Parameter >Rückkehr Funktion ... . Nein, Funktionen, die Funktionen zurückgeben, sind nicht einmal erlaubt.

Jetzt müssen wir nur noch <Parameter eingeben >. 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)

Stellen Sie es einfach intvor die Deklaratoren, wie wir es mit Bottom-up getan haben, 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 manche Leute fühlen sich mit Top-Down wohler. Es ist Geschmackssache, denke ich. Wenn Sie übrigens alle Operatoren in dieser Deklaration anwenden, erhalten Sie am Ende ein int:

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

Das ist eine nette Eigenschaft von Deklarationen in C: Die Deklaration besagt, dass, wenn diese Operatoren in einem Ausdruck unter Verwendung des Bezeichners verwendet werden, der Typ ganz links ausgegeben wird. Das ist auch für Arrays so.

Hoffe dir hat dieses kleine Tutorial gefallen! Jetzt können wir darauf verweisen, wenn sich die Leute über die seltsame Deklarationssyntax von Funktionen wundern. Ich habe versucht, so wenig C-Interna wie möglich zu verwenden. Fühlen Sie sich frei, Dinge darin zu bearbeiten / zu reparieren.


24

Eine weitere gute Verwendung für Funktionszeiger:
Schmerzloses Wechseln zwischen Versionen

Sie sind sehr praktisch, wenn Sie unterschiedliche Funktionen zu unterschiedlichen Zeiten oder in unterschiedlichen Entwicklungsphasen wünschen. Zum Beispiel entwickle ich eine Anwendung auf einem Host-Computer mit einer Konsole, aber die endgültige Version der Software wird auf einem Avnet ZedBoard (das über Ports für Displays und Konsolen verfügt, aber für die nicht benötigt / gewünscht wird) bereitgestellt endgültige Veröffentlichung). Während der Entwicklung werde ich also printfStatus- und Fehlermeldungen anzeigen, aber wenn ich fertig bin, möchte ich nicht, dass etwas gedruckt wird. 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

In version.cIch werde die 2 Funktionsprototypen definieren, die in vorhanden sindversion.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 die Funktionszeiger wird prototypisiert in version.hals

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

Wenn in der Anwendung darauf verwiesen wird, wird die Ausführung überall dort ausgeführt, wo sie zeigt, was noch definiert werden muss.

In version.c, Bekanntmachung in der board_init()Funktion , wo zprintfeine eindeutige Funktion zugeordnet (dessen Funktion Unterschrift Ursachen) in Abhängigkeit von der Version , die in definiert ist ,version.h

zprintf = &printf; zprintf ruft printf zu Debugging-Zwecken auf

oder

zprintf = &noprint; zprintf kehrt nur 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 printfim Debug-Modus verwendet oder im Release-Modus nicht ausgeführt. 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.hund der Code erledigt den Rest!


4
Sie werden viel Aufführungszeit verlieren. Stattdessen können Sie ein Makro verwenden, das einen Codeabschnitt basierend auf Debug / Release aktiviert und deaktiviert.
AlphaGoku

19

Der Funktionszeiger wird normalerweise durch definiert typedefund als Parameter- und Rückgabewert verwendet.

Die obigen Antworten haben bereits 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;
}

14

Eine der Hauptanwendungen für Funktionszeiger in C ist das Aufrufen einer zur Laufzeit ausgewählten Funktion. Zum Beispiel hat die C - Laufzeitbibliothek zwei Routinen, qsortund bsearch, die einen Zeiger auf eine Funktion übernehmen , die zwei Elemente genannt wird zu vergleichen , sortiert werden; Auf diese Weise können Sie alles nach beliebigen Kriterien sortieren bzw. suchen.

Ein sehr einfaches Beispiel: Wenn es eine aufgerufene Funktion gibt, für print(int x, int y)die möglicherweise eine Funktion aufgerufen werden muss (entweder add()oder sub()vom selben Typ), fügen wir der print()Funktion ein Funktionszeigerargument hinzu, wie unten gezeigt ::

#include <stdio.h>

int add()
{
   return (100+10);
}

int sub()
{
   return (100-10);
}

void print(int x, int y, int (*func)())
{
    printf("value is: %d\n", (x+y+(*func)()));
}

int main()
{
    int x=100, y=200;
    print(x,y,add);
    print(x,y,sub);

    return 0;
}

Die Ausgabe ist:

Wert ist: 410
Wert ist: 390


10

Die Funktion "Von Grund auf neu starten" verfügt über eine Speicheradresse, von der aus sie ausgeführt werden. In der Assemblersprache werden sie als aufgerufen (rufen Sie die Speicheradresse der Funktion auf). Kehren Sie nun zu C zurück. Wenn die Funktion eine Speicheradresse hat, können sie durch Zeiger in C.So nach den Regeln von C bearbeitet werden

1. Zuerst müssen Sie einen Zeiger auf die Funktion deklarieren. 2. Übergeben Sie die Adresse der gewünschten Funktion

**** Hinweis-> Die Funktionen sollten vom gleichen Typ sein ****

Dieses einfache Programm wird alles veranschaulichen.

#include<stdio.h>
void (*print)() ;//Declare a  Function Pointers
void sayhello();//Declare The Function Whose Address is to be passed
                //The Functions should Be of Same Type
int main()
{
 print=sayhello;//Addressof sayhello is assigned to print
 print();//print Does A call To The Function 
 return 0;
}

void sayhello()
{
 printf("\n Hello World");
}

Geben Sie hier die Bildbeschreibung einDanach können wir sehen, wie die Maschine sie versteht. Die Maschinenanweisung des obigen Programms in der 32-Bit-Architektur ist begrenzt.

Der rote Markierungsbereich zeigt an, wie die Adresse ausgetauscht und in eax gespeichert wird. Dann gibt es eine Anrufanweisung für eax. eax enthält die gewünschte Adresse der Funktion.


8

Ein Funktionszeiger ist eine Variable, die die Adresse einer Funktion enthält. Da es sich jedoch um eine Zeigervariable mit einigen eingeschränkten Eigenschaften handelt, können Sie sie wie jede andere Zeigervariable in Datenstrukturen verwenden.

Die einzige Ausnahme, die ich mir vorstellen kann, besteht darin, den Funktionszeiger so zu behandeln, dass er auf etwas anderes als einen einzelnen Wert zeigt. Das Ausführen einer Zeigerarithmetik durch Inkrementieren oder Dekrementieren eines Funktionszeigers oder Hinzufügen / Subtrahieren eines Versatzes zu einem Funktionszeiger ist nicht wirklich nützlich, da ein Funktionszeiger nur auf eine einzelne Sache zeigt, den Einstiegspunkt einer Funktion.

Die Größe einer Funktionszeigervariablen, die Anzahl der von der Variablen belegten Bytes, kann abhängig von der zugrunde liegenden Architektur variieren, z. B. x32 oder x64 oder was auch immer.

Die Deklaration für eine Funktionszeigervariable muss dieselbe Art von Informationen wie eine Funktionsdeklaration angeben, damit der C-Compiler die normalerweise durchgeführten Überprüfungen durchführen kann. Wenn Sie in der Deklaration / Definition des Funktionszeigers keine Parameterliste angeben, kann der C-Compiler die Verwendung von Parametern nicht überprüfen. Es gibt Fälle, in denen diese mangelnde Überprüfung hilfreich sein kann. Denken Sie jedoch daran, dass ein Sicherheitsnetz entfernt wurde.

Einige Beispiele:

int func (int a, char *pStr);    // declares a function

int (*pFunc)(int a, char *pStr);  // declares or defines a function pointer

int (*pFunc2) ();                 // declares or defines a function pointer, no parameter list specified.

int (*pFunc3) (void);             // declares or defines a function pointer, no arguments.

Die ersten beiden Erklärungen sind insofern etwas ähnlich:

  • funcist eine Funktion, die ein intund ein nimmt und ein char *zurückgibtint
  • pFuncist ein Funktionszeiger, dem die Adresse einer Funktion zugewiesen ist, die ein intund ein nimmt und ein char *zurückgibtint

Von oben könnten wir also eine Quellzeile haben, in der die Adresse der Funktion func()der Funktionszeigervariablen pFuncwie in zugewiesen wird pFunc = func;.

Beachten Sie die Syntax, die mit einer Funktionszeigerdeklaration / -definition verwendet wird, in der Klammern verwendet werden, um die Vorrangregeln für natürliche Operatoren zu überwinden.

int *pfunc(int a, char *pStr);    // declares a function that returns int pointer
int (*pFunc)(int a, char *pStr);  // declares a function pointer that returns an int

Verschiedene Verwendungsbeispiele

Einige Beispiele für die Verwendung eines Funktionszeigers:

int (*pFunc) (int a, char *pStr);    // declare a simple function pointer variable
int (*pFunc[55])(int a, char *pStr); // declare an array of 55 function pointers
int (**pFunc)(int a, char *pStr);    // declare a pointer to a function pointer variable
struct {                             // declare a struct that contains a function pointer
    int x22;
    int (*pFunc)(int a, char *pStr);
} thing = {0, func};                 // assign values to the struct variable
char * xF (int x, int (*p)(int a, char *pStr));  // declare a function that has a function pointer as an argument
char * (*pxF) (int x, int (*p)(int a, char *pStr));  // declare a function pointer that points to a function that has a function pointer as an argument

Sie können Parameterlisten variabler Länge bei der Definition eines Funktionszeigers verwenden.

int sum (int a, int b, ...);
int (*psum)(int a, int b, ...);

Oder Sie können überhaupt keine Parameterliste angeben. Dies kann nützlich sein, verhindert jedoch, dass der C-Compiler die bereitgestellte Argumentliste überprüfen kann.

int  sum ();      // nothing specified in the argument list so could be anything or nothing
int (*psum)();
int  sum2(void);  // void specified in the argument list so no parameters when calling this function
int (*psum2)(void);

Casts im C-Stil

Sie können Casts im C-Stil mit Funktionszeigern verwenden. Beachten Sie jedoch, dass ein C-Compiler bei Überprüfungen möglicherweise nachlässig ist oder eher Warnungen als Fehler bereitstellt.

int sum (int a, char *b);
int (*psplsum) (int a, int b);
psplsum = sum;               // generates a compiler warning
psplsum = (int (*)(int a, int b)) sum;   // no compiler warning, cast to function pointer
psplsum = (int *(int a, int b)) sum;     // compiler error of bad cast generated, parenthesis are required.

Funktionszeiger mit Gleichheit vergleichen

Sie können mithilfe einer ifAnweisung überprüfen, ob ein Funktionszeiger einer bestimmten Funktionsadresse entspricht, obwohl ich nicht sicher bin, wie nützlich dies wäre. Andere Vergleichsoperatoren scheinen noch weniger nützlich zu sein.

static int func1(int a, int b) {
    return a + b;
}

static int func2(int a, int b, char *c) {
    return c[0] + a + b;
}

static int func3(int a, int b, char *x) {
    return a + b;
}

static char *func4(int a, int b, char *c, int (*p)())
{
    if (p == func1) {
        p(a, b);
    }
    else if (p == func2) {
        p(a, b, c);      // warning C4047: '==': 'int (__cdecl *)()' differs in levels of indirection from 'char *(__cdecl *)(int,int,char *)'
    } else if (p == func3) {
        p(a, b, c);
    }
    return c;
}

Ein Array von Funktionszeigern

Und wenn Sie ein Array von Funktionszeigern haben möchten, von denen jedes der Elemente in der Argumentliste Unterschiede aufweist, können Sie einen Funktionszeiger definieren, dessen Argumentliste nicht angegeben ist ( voidwas nicht bedeutet, dass keine Argumente, sondern nur nicht angegeben sind) Möglicherweise werden Warnungen vom C-Compiler angezeigt. Dies funktioniert auch für einen Funktionszeigerparameter auf eine Funktion:

int(*p[])() = {       // an array of function pointers
    func1, func2, func3
};
int(**pp)();          // a pointer to a function pointer


p[0](a, b);
p[1](a, b, 0);
p[2](a, b);      // oops, left off the last argument but it compiles anyway.

func4(a, b, 0, func1);
func4(a, b, 0, func2);  // warning C4047: 'function': 'int (__cdecl *)()' differs in levels of indirection from 'char *(__cdecl *)(int,int,char *)'
func4(a, b, 0, func3);

    // iterate over the array elements using an array index
for (i = 0; i < sizeof(p) / sizeof(p[0]); i++) {
    func4(a, b, 0, p[i]);
}
    // iterate over the array elements using a pointer
for (pp = p; pp < p + sizeof(p)/sizeof(p[0]); pp++) {
    (*pp)(a, b, 0);          // pointer to a function pointer so must dereference it.
    func4(a, b, 0, *pp);     // pointer to a function pointer so must dereference it.
}

C-Stil namespaceVerwenden von Global structmit Funktionszeigern

Mit dem staticSchlüsselwort können Sie eine Funktion angeben, deren Name der Dateibereich ist, und diese dann einer globalen Variablen zuweisen, um etwas Ähnliches wie die namespaceFunktionalität von C ++ bereitzustellen.

Definieren Sie in einer Header-Datei eine Struktur, die unser Namespace ist, zusammen mit einer globalen Variablen, die sie verwendet.

typedef struct {
   int (*func1) (int a, int b);             // pointer to function that returns an int
   char *(*func2) (int a, int b, char *c);  // pointer to function that returns a pointer
} FuncThings;

extern const FuncThings FuncThingsGlobal;

Dann in der C-Quelldatei:

#include "header.h"

// the function names used with these static functions do not need to be the
// same as the struct member names. It's just helpful if they are when trying
// to search for them.
// the static keyword ensures these names are file scope only and not visible
// outside of the file.
static int func1 (int a, int b)
{
    return a + b;
}

static char *func2 (int a, int b, char *c)
{
    c[0] = a % 100; c[1] = b % 50;
    return c;
}

const FuncThings FuncThingsGlobal = {func1, func2};

Dies wird dann verwendet, indem der vollständige Name der globalen Strukturvariablen und der Mitgliedsname angegeben werden, um auf die Funktion zuzugreifen. Der constModifikator wird global verwendet, damit er nicht versehentlich geändert werden kann.

int abcd = FuncThingsGlobal.func1 (a, b);

Anwendungsbereiche von Funktionszeigern

Eine DLL-Bibliothekskomponente könnte etwas Ähnliches wie der C-Stil- namespaceAnsatz tun, bei dem eine bestimmte Bibliotheksschnittstelle von einer Factory-Methode in einer Bibliotheksschnittstelle angefordert wird, die die Erstellung von structenthaltenen Funktionszeigern unterstützt. Diese Bibliotheksschnittstelle lädt die angeforderte DLL-Version und erstellt sie eine Struktur mit den erforderlichen Funktionszeigern und gibt die Struktur dann zur Verwendung an den anfordernden Aufrufer zurück.

typedef struct {
    HMODULE  hModule;
    int (*Func1)();
    int (*Func2)();
    int(*Func3)(int a, int b);
} LibraryFuncStruct;

int  LoadLibraryFunc LPCTSTR  dllFileName, LibraryFuncStruct *pStruct)
{
    int  retStatus = 0;   // default is an error detected

    pStruct->hModule = LoadLibrary (dllFileName);
    if (pStruct->hModule) {
        pStruct->Func1 = (int (*)()) GetProcAddress (pStruct->hModule, "Func1");
        pStruct->Func2 = (int (*)()) GetProcAddress (pStruct->hModule, "Func2");
        pStruct->Func3 = (int (*)(int a, int b)) GetProcAddress(pStruct->hModule, "Func3");
        retStatus = 1;
    }

    return retStatus;
}

void FreeLibraryFunc (LibraryFuncStruct *pStruct)
{
    if (pStruct->hModule) FreeLibrary (pStruct->hModule);
    pStruct->hModule = 0;
}

und dies könnte wie folgt verwendet werden:

LibraryFuncStruct myLib = {0};
LoadLibraryFunc (L"library.dll", &myLib);
//  ....
myLib.Func1();
//  ....
FreeLibraryFunc (&myLib);

Der gleiche Ansatz kann verwendet werden, um eine abstrakte Hardwareschicht für Code zu definieren, der ein bestimmtes Modell der zugrunde liegenden Hardware verwendet. Funktionszeiger werden von einer Fabrik mit hardwarespezifischen Funktionen gefüllt, um die hardwarespezifische Funktionalität bereitzustellen, die die im abstrakten Hardwaremodell angegebenen Funktionen implementiert. Dies kann verwendet werden, um eine abstrakte Hardwareschicht bereitzustellen, die von Software verwendet wird, die eine Factory-Funktion aufruft, um die spezifische Hardwarefunktionsschnittstelle abzurufen, und dann die bereitgestellten Funktionszeiger verwendet, um Aktionen für die zugrunde liegende Hardware auszuführen, ohne Implementierungsdetails über das spezifische Ziel zu kennen .

Funktionszeiger zum Erstellen von Delegaten, Handlern und Rückrufen

Sie können Funktionszeiger verwenden, um bestimmte Aufgaben oder Funktionen zu delegieren. Das klassische Beispiel in C ist der Vergleichsdelegat-Funktionszeiger, der mit den Standard-C-Bibliotheksfunktionen verwendet wird qsort()und bsearch()die Sortierreihenfolge zum Sortieren einer Liste von Elementen oder zum Durchführen einer binären Suche über eine sortierte Liste von Elementen bereitstellt. Der Vergleichsfunktionsdelegierte gibt den Kollatierungsalgorithmus an, der bei der Sortierung oder der binären Suche verwendet wird.

Eine andere Verwendung ähnelt der Anwendung eines Algorithmus auf einen C ++ Standard Template Library-Container.

void * ApplyAlgorithm (void *pArray, size_t sizeItem, size_t nItems, int (*p)(void *)) {
    unsigned char *pList = pArray;
    unsigned char *pListEnd = pList + nItems * sizeItem;
    for ( ; pList < pListEnd; pList += sizeItem) {
        p (pList);
    }

    return pArray;
}

int pIncrement(int *pI) {
    (*pI)++;

    return 1;
}

void * ApplyFold(void *pArray, size_t sizeItem, size_t nItems, void * pResult, int(*p)(void *, void *)) {
    unsigned char *pList = pArray;
    unsigned char *pListEnd = pList + nItems * sizeItem;
    for (; pList < pListEnd; pList += sizeItem) {
        p(pList, pResult);
    }

    return pArray;
}

int pSummation(int *pI, int *pSum) {
    (*pSum) += *pI;

    return 1;
}

// source code and then lets use our function.
int intList[30] = { 0 }, iSum = 0;

ApplyAlgorithm(intList, sizeof(int), sizeof(intList) / sizeof(intList[0]), pIncrement);
ApplyFold(intList, sizeof(int), sizeof(intList) / sizeof(intList[0]), &iSum, pSummation);

Ein anderes Beispiel ist der GUI-Quellcode, in dem ein Handler für ein bestimmtes Ereignis registriert wird, indem ein Funktionszeiger bereitgestellt wird, der tatsächlich aufgerufen wird, wenn das Ereignis eintritt. Das Microsoft MFC-Framework mit seinen Nachrichtenzuordnungen verwendet etwas Ähnliches, um Windows-Nachrichten zu verarbeiten, die an ein Fenster oder einen Thread gesendet werden.

Asynchrone Funktionen, die einen Rückruf erfordern, ähneln einem Ereignishandler. Der Benutzer der asynchronen Funktion ruft die asynchrone Funktion auf, um eine Aktion zu starten, und stellt einen Funktionszeiger bereit, den die asynchrone Funktion nach Abschluss der Aktion aufruft. In diesem Fall ist das Ereignis die asynchrone Funktion, die ihre Aufgabe erfüllt.


0

Da Funktionszeiger häufig Rückrufe eingegeben werden, möchten Sie vielleicht einen Blick auf haben Typ sicher Rückrufe . Gleiches gilt für Einstiegspunkte usw. von Funktionen, die keine Rückrufe sind.

C ist ziemlich launisch und verzeihend zugleich :)

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.