1506 Stimmen

Wie funktionieren Funktionszeiger in C?

Ich habe in letzter Zeit einige Erfahrungen mit Funktionszeigern in C gemacht.

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

43 Stimmen

Auch: Eine etwas ausführlichere Analyse von C-Zeigern finden Sie unter blogs.oracle.com/ksplice/entry/the_ksplice_pointer_challenge . Auch, Programmierung von Grund auf zeigt, wie sie auf Maschinenebene funktionieren. Verstehen Das "Speichermodell" von C ist sehr nützlich, um zu verstehen, wie C-Zeiger funktionieren.

10 Stimmen

Tolle Infos. Nach dem Titel zu urteilen, hätte ich eigentlich eine Erklärung erwartet, wie "Funktionszeiger funktionieren", nicht wie sie codiert sind :)

2 Stimmen

Die folgende Antwort ist kürzer und sehr viel leichter zu verstehen: stackoverflow.com/a/142809/2188550

1752voto

Yuval Adam Punkte 155168

Funktionszeiger in C

Beginnen wir mit einer Basisfunktion, die wir zeigend auf :

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

Als erstes definieren wir einen Zeiger auf eine Funktion, die 2 int s und gibt eine int :

int (*functionPtr)(int,int);

Jetzt können wir sicher auf unsere Funktion verweisen:

functionPtr = &addInt;

Da wir nun einen Zeiger auf die Funktion haben, können wir sie verwenden:

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

Die Übergabe des Zeigers an eine andere Funktion ist im Grunde dasselbe:

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

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

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

27 Stimmen

Vielen Dank für die tollen Informationen. Könnten Sie etwas darüber sagen, wo Funktionszeiger verwendet werden oder wo sie besonders nützlich sind?

378 Stimmen

"functionPtr = &addInt;" kann auch als "functionPtr = addInt;" geschrieben werden (und wird auch oft so geschrieben), was ebenfalls gültig ist, da der Standard besagt, dass ein Funktionsname in diesem Zusammenhang in die Adresse der Funktion umgewandelt wird.

26 Stimmen

Hlovdal, in diesem Zusammenhang ist es interessant zu erklären, dass dies die Möglichkeit bietet, functionPtr = ******************addInt zu schreiben;

349voto

coobird Punkte 155882

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

Die folgenden Zeilen sind zum Beispiel in C geschrieben:

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

Ja, die -> und das Fehlen eines new Operator ist ein eindeutiges Indiz, aber es scheint zu bedeuten, dass wir den Text einer bestimmten String Klasse zu sein "hello" .

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

Wie wird dies bewerkstelligt?

El String Klasse ist eigentlich eine struct mit einer Reihe von Funktionszeigern, mit denen Methoden simuliert werden können. Im Folgenden wird eine Teildeklaration 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 man sieht, sind die Methoden der String Klasse sind eigentlich Funktionszeiger auf die deklarierte Funktion. Bei der Vorbereitung der Instanz der String El newString 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 den Aufruf der get Methode ist wie folgt definiert:

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

Eine Sache, die auffällt, ist, dass es kein Konzept einer Instanz eines Objekts gibt und Methoden, die tatsächlich Teil eines Objekts sind, so dass bei jedem Aufruf ein "Selbstobjekt" übergeben werden muss. (Und die internal ist nur eine versteckte struct was im Code-Listing zuvor ausgelassen wurde - es ist eine Möglichkeit, Informationen zu verbergen, aber das ist für Funktionszeiger nicht relevant).

Anstatt also die Möglichkeit zu haben s1->set("hello"); muss man das Objekt übergeben, an dem die Aktion durchgeführt werden soll s1->set(s1, "hello") .

Nachdem diese kleine Erklärung, dass man einen Verweis auf sich selbst einfügen muss, aus dem Weg geräumt ist, kommen wir zum nächsten Teil, der lautet Vererbung in C .

Nehmen wir an, wir wollen eine Unterklasse von String sagen wir ein ImmutableString . Um die Zeichenkette unveränderlich zu machen, muss die set Methode nicht zugänglich sein wird, während der Zugriff auf get y length und zwingen den "Konstruktor", eine 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);

Im Grunde genommen sind die verfügbaren Methoden für alle Unterklassen wieder Funktionszeiger. Diesmal ist die Deklaration für die set Methode nicht vorhanden ist, kann sie daher nicht in einer ImmutableString .

Was die Umsetzung der ImmutableString ist 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;
}

Bei der Instanziierung der ImmutableString sind die Funktionszeiger auf die get y length Methoden verweisen eigentlich auf die String.get y String.length Methode, indem Sie durch die base die eine intern gespeicherte Variable ist String Objekt.

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

Wir können auch weiterhin Polymorphismus in C .

Wenn wir zum Beispiel das Verhalten der length Methode zur Rückgabe 0 die ganze Zeit im ImmutableString Klasse aus irgendeinem Grund nur getan werden müsste, um:

  1. Fügen Sie eine Funktion hinzu, die als übergeordnete length Methode.
  2. Gehen Sie zum "Konstruktor" und setzen Sie den Funktionszeiger auf den überschreibenden length Methode.

Hinzufügen einer übergeordneten length Methode in ImmutableString kann durch Hinzufügen einer lengthOverrideMethod :

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

Dann wird der Funktionszeiger für die length Methode im Konstruktor ist mit der 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 nun ein identisches Verhalten für die length Methode in ImmutableString Klasse als die String Klasse, jetzt die length Methode bezieht sich auf das Verhalten, das in der lengthOverrideMethod Funktion.

Ich muss einschränkend hinzufügen, dass ich immer noch lerne, wie man mit einem objektorientierten Programmierstil in C schreibt, so dass es wahrscheinlich Punkte gibt, die ich nicht gut erklärt habe, oder einfach nur daneben liegen, wenn es darum geht, wie man OOP in C am besten implementiert. Aber mein Ziel war es, eine der vielen Verwendungen von Funktionszeigern zu illustrieren.

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

34 Stimmen

Diese Antwort ist furchtbar! Sie impliziert nicht nur, dass OO irgendwie von der Punktnotation abhängt, sie ermutigt auch dazu, Müll in Ihre Objekte zu packen!

34 Stimmen

Das ist zwar OO, aber nicht annähernd OO im C-Stil. Was Sie haben brokenly implementiert ist Javascript-Stil Prototyp-basierte OO. Um C++/Pascal-Stil OO zu erhalten, müssten Sie: 1. Eine const-Struktur für eine virtuelle Tabelle von jedem Klasse mit virtuellen Mitgliedern. 2. Zeiger auf diese Struktur in polymorphen Objekten haben. 3. Rufen Sie virtuelle Methoden über die virtuelle Tabelle auf, und alle anderen Methoden direkt -- normalerweise durch das Festhalten an einigen ClassName_methodName Funktionsbenennungskonvention. Nur dann haben Sie die gleichen Laufzeit- und Speicherkosten wie in C++ und Pascal.

21 Stimmen

OO mit einer Sprache zu arbeiten, die nicht dafür gedacht ist, OO zu sein, ist immer eine schlechte Idee. Wenn Sie OO wollen und noch C haben, arbeiten Sie einfach mit C++.

266voto

Lee Punkte 482

Der Leitfaden zum Entlassenwerden: Wie man Funktionszeiger in GCC auf x86-Maschinen missbraucht, indem man seinen Code von Hand kompiliert:

Diese String-Literale sind Bytes von 32-Bit-x86-Maschinencode. 0xC3 es ein x86 ret Anleitung .

Normalerweise würde man diese nicht von Hand schreiben, sondern in Assemblersprache und dann mit einem Assembler wie nasm zu einer flachen Binärdatei zusammensetzen, die Sie mit Hexdump in ein C-String-Literal umwandeln.

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

    int eax = ((int(*)())("\xc3 <- This returns the value of the EAX register"))();
  2. Eine Swap-Funktion schreiben

    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 bis 1000, der jedes Mal eine Funktion aufruft

    ((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 String-Literale in die .rodata Abschnitt (oder .rdata unter Windows), die als Teil des Textsegments verknüpft ist (zusammen mit dem Code für Funktionen).

Das Textsegment hat die Berechtigung Read+Exec, so dass die Umwandlung von Zeichenkettenliteralen in Funktionszeiger funktioniert, ohne dass man mprotect() o VirtualProtect() Systemaufrufe, wie Sie sie für dynamisch zugewiesenen Speicher benötigen. (Oder gcc -z execstack verknüpft das Programm mit Stack + Datensegment + Heap ausführbar, als ein schneller Hack).


Um diese zu disassemblieren, können Sie diese kompilieren, um die Bytes zu kennzeichnen, 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";

Kompilieren mit gcc -c -m32 foo.c und die Demontage mit objdump -D -rwC -Mintel können wir die Assemblierung abrufen und herausfinden, dass dieser Code die ABI verletzt, indem er EBX (ein aufrufgeschütztes Register) beschädigt und generell 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 wird (wahrscheinlich) in 32-Bit-Code unter Windows, Linux, OS X usw. funktionieren: Die Standardaufrufkonventionen all dieser Betriebssysteme übergeben die Argumente auf dem Stack statt effizienter in Registern. Aber EBX ist in allen normalen Aufrufkonventionen aufrufgeschützt, so dass die Verwendung als Scratch-Register ohne Speichern/Wiederherstellen leicht zum Absturz des Aufrufers führen kann.

9 Stimmen

Hinweis: Dies funktioniert nicht, wenn die Datenausführungsverhinderung aktiviert ist (z. B. unter Windows XP SP2+), da C-Strings normalerweise nicht als ausführbar markiert sind.

1 Stimmen

Funktioniert das auch unter Visual Studio? Ich erhalte:Fehler C2440: 'type cast' : kann nicht von 'const char [68]' nach 'void (__cdecl *)(int *,int *)' konvertieren. Irgendwelche Ideen?

5 Stimmen

Hallo Matt! Abhängig von der Optimierungsstufe, GCC wird oft inline-String-Konstanten in das TEXT-Segment, so dass dies auch auf neueren Version von Windows, vorausgesetzt, dass Sie nicht diese Art der Optimierung zu deaktivieren. (Wenn ich mich recht erinnere, hat die MINGW-Version zum Zeitpunkt meines Beitrags vor über zwei Jahren String-Literale auf der Standard-Optimierungsstufe eingefügt)

127voto

Nick Van Brunt Punkte 14922

Einer meiner Lieblingseinsätze für Funktionszeiger ist die Verwendung als 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);
}

11 Stimmen

Sie sollten auch einen Zeiger auf benutzerdefinierte Daten übergeben, wenn Sie irgendwie eine Ausgabe aus Iterationen extrahieren wollen (denken Sie an Closures).

3 Stimmen

Einverstanden. Alle meine Iteratoren sehen so aus: int (*cb)(void *arg, ...) . Der Rückgabewert des Iterators ermöglicht es mir auch, vorzeitig anzuhalten (falls er nicht Null ist).

27voto

Funktionszeiger sind leicht zu deklarieren, wenn man die grundlegenden Deklaratoren kennt:

  • id: ID : ID ist eine
  • Zeiger: *D : D Zeiger auf
  • Funktion: D(<parameters>) : D Funktionsaufnahme < Parameter > zurückkehrend

Während D ein weiterer Deklarator ist, der nach denselben Regeln aufgebaut ist. Am Ende, irgendwo, endet es mit ID (siehe unten für ein Beispiel), der der Name der deklarierten Entität ist. Versuchen wir, eine Funktion zu bauen, die einen Zeiger auf eine Funktion nimmt, die nichts nimmt und int zurückgibt, und einen Zeiger auf eine Funktion zurückgibt, die ein char nimmt und int zurückgibt. Mit type-defs sieht das folgendermaßen aus

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

Wie Sie sehen, ist es ziemlich einfach, sie mit Hilfe von Typedefs aufzubauen. Ohne Typendefinitionen ist es auch nicht schwer, mit den oben genannten Deklarationsregeln, die konsequent angewendet werden. Wie Sie sehen, habe ich den Teil ausgelassen, auf den der Zeiger zeigt, und das, was die Funktion zurückgibt. Das ist das, was ganz links in der Deklaration steht und nicht von Interesse ist: Es wird am Ende hinzugefügt, wenn man den Deklarator bereits aufgebaut hat. Lass uns das tun. Konsequent aufbauen, zuerst wortreich - die Struktur zeigen mit [ y ] :

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 aneinanderhängt. Die Konstruktion kann auf zwei Arten erfolgen. Die eine ist von unten nach oben, wobei man mit dem ganz rechten Ding (Blätter) beginnt und sich bis zum Bezeichner durcharbeitet. Der andere Weg ist von oben nach unten, wobei man mit dem Bezeichner beginnt und sich bis zu den Blättern vorarbeitet. Ich werde beide Wege aufzeigen.

Von unten nach oben

Die Konstruktion beginnt mit dem Ding rechts: Das, was zurückgegeben wird, also die Funktion, die char nimmt. Um die Deklaratoren zu unterscheiden, werde ich sie nummerieren:

D1(char);

Der Parameter char wurde direkt eingefügt, da er trivial ist. Hinzufügen eines Zeigers auf den Deklarator durch Ersetzen von D1 von *D2 . Beachten Sie, dass wir Klammern um Folgendes setzen müssen *D2 . Dies lässt sich feststellen, indem man die Rangfolge der *-operator und der Funktionsaufruf-Operator () . Ohne unsere Klammern würde der Compiler dies als *(D2(char p)) . Aber das wäre keine einfache Ersetzung von D1 durch *D2 mehr, natürlich. Klammern sind um Deklaratoren herum immer erlaubt. Man macht also eigentlich nichts falsch, wenn man zu viele davon hinzufügt.

(*D2)(char);

Rückgabeart ist vollständig! Ersetzen wir nun D2 durch den Funktionsdeklarator Funktionsaufnahme <parameters> zurückkehrend das ist D3(<parameters>) in der wir uns jetzt befinden.

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

Beachten Sie, dass keine Klammern erforderlich sind, da wir wollen D3 diesmal ein Funktionsdeklarator und kein Zeigerdeklarator zu sein. Großartig, jetzt fehlen nur noch die Parameter dafür. Die Parameter werden genau so wie der Rückgabetyp deklariert, nur mit char ersetzt durch void . Ich werde es also kopieren:

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

Ich habe ersetzt D2 von ID1 , da wir mit diesem Parameter fertig sind (er ist bereits ein Zeiger auf eine Funktion - kein weiterer Deklarator erforderlich). ID1 wird der Name des Parameters sein. Nun, ich habe oben gesagt, dass man am Ende den Typ hinzufügt, den all diese Deklaratoren modifizieren - denjenigen, der ganz links in jeder Deklaration erscheint. Bei Funktionen wird das der Rückgabetyp. Bei Zeigern der Typ, auf den gezeigt wird usw... Interessant ist, dass der Typ in der umgekehrten Reihenfolge erscheint, nämlich ganz rechts :) Wie auch immer, wenn man ihn ersetzt, erhält man die vollständige Deklaration. Beide Male int Natürlich.

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

Ich habe den Bezeichner der Funktion aufgerufen ID0 in diesem Beispiel.

Von oben nach unten

Dies beginnt mit dem Bezeichner ganz links in der Beschreibung des Typs und umschließt den Deklarator, während wir uns nach rechts vorarbeiten. Beginnen Sie mit Funktionsaufnahme < Parameter > zurückkehrend

ID0(<parameters>)

Der nächste Punkt in der Beschreibung (nach "Rückkehr") war Zeiger auf . Lassen Sie es uns einbeziehen:

*ID0(<parameters>)

Die nächste Sache war dann Funktionstüchtigkeit < Parameter > zurückkehrend . Der Parameter ist ein einfaches Zeichen, also fügen wir ihn gleich wieder ein, da er wirklich trivial ist.

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

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

Jetzt müssen wir nur noch die < Parameter > . Ich werde eine kurze Version der Ableitung zeigen, da ich denke, dass Sie bereits eine Vorstellung davon haben, wie man es macht.

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

Setzen Sie einfach int vor den Deklaratoren, wie wir es bei Bottom-up gemacht haben, und wir sind fertig

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

Das Schöne daran

Ist Bottom-up oder Top-down besser? Ich bin an Bottom-up gewöhnt, aber manche Leute mögen sich mit Top-down wohler fühlen. Ich denke, das ist Geschmackssache. Ü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 schöne Eigenschaft von Deklarationen in C: Die Deklaration besagt, dass, wenn diese Operatoren in einem Ausdruck mit dem Bezeichner verwendet werden, sie den Typ ganz links ergibt. Das ist auch bei Arrays der Fall.

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

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X