Was ist der Punkt von Funktionszeigern?


94

Ich habe Probleme, die Nützlichkeit von Funktionszeigern zu erkennen. Ich denke, es kann in einigen Fällen nützlich sein (sie existieren schließlich), aber ich kann mir keinen Fall vorstellen, in dem es besser oder unvermeidlich ist, einen Funktionszeiger zu verwenden.

Können Sie ein Beispiel für die gute Verwendung von Funktionszeigern (in C oder C ++) geben?


1
In dieser verwandten SO-Frage finden Sie einige Diskussionen über Funktionszeiger .
matt

20
@itsmatt: Nicht wirklich. "Wie funktioniert ein Fernseher?" ist eine ganz andere Frage als "Was mache ich mit einem Fernseher?"
sbi

6
In C ++ würden Sie wahrscheinlich stattdessen einen Funktor ( en.wikipedia.org/wiki/Function_object#In_C_and_C.2B.2B ) verwenden.
Kennytm

11
In den alten dunklen Tagen, als C ++ zu C "kompiliert" wurde, konnte man tatsächlich sehen, wie virtuelle Methoden implementiert sind - ja, mit Funktionszeigern.
sbk

1
Sehr wichtig, wenn Sie C ++ mit einem verwalteten C ++ oder C # verwenden möchten, dh: Delegaten und Rückrufe
Maher

Antworten:


108

Die meisten Beispiele laufen auf Rückrufe : Sie rufen eine Funktion f()die Adresse einer anderen Funktion übergeben g(), und f()Anrufe g()für einige bestimmte Aufgabe. Wenn Sie stattdessen f()die Adresse von übergeben h(), f()wird h()stattdessen zurückgerufen.

Grundsätzlich ist dies eine Möglichkeit, eine Funktion zu parametrisieren : Ein Teil ihres Verhaltens ist nicht fest codiert f(), sondern in die Rückruffunktion. Anrufer können f()sich anders verhalten, indem sie verschiedene Rückruffunktionen übergeben. Ein Klassiker stammt qsort()aus der C-Standardbibliothek, die sein Sortierkriterium als Zeiger auf eine Vergleichsfunktion verwendet.

In C ++ werden dazu häufig Funktionsobjekte (auch Funktoren genannt) verwendet. Dies sind Objekte, die den Funktionsaufrufoperator überladen, sodass Sie sie aufrufen können, als wären sie eine Funktion. Beispiel:

class functor {
  public:
     void operator()(int i) {std::cout << "the answer is: " << i << '\n';}
};

functor f;
f(42);

Die Idee dahinter ist, dass ein Funktionsobjekt im Gegensatz zu einem Funktionszeiger nicht nur einen Algorithmus, sondern auch Daten enthalten kann:

class functor {
  public:
     functor(const std::string& prompt) : prompt_(prompt) {}
     void operator()(int i) {std::cout << prompt_ << i << '\n';}
  private:
     std::string prompt_;
};

functor f("the answer is: ");
f(42);

Ein weiterer Vorteil ist, dass es manchmal einfacher ist, Aufrufe an Funktionsobjekte zu inline als Aufrufe über Funktionszeiger. Dies ist ein Grund, warum das Sortieren in C ++ manchmal schneller ist als das Sortieren in C.


1
+1, siehe auch diese Antwort für ein weiteres Beispiel: stackoverflow.com/questions/1727824/…
sharptooth

Sie haben virtuelle Funktionen vergessen, im Wesentlichen sind sie auch Funktionszeiger (gekoppelt mit einer Datenstruktur, die der Compiler generiert). Darüber hinaus können Sie in reinem C diese Strukturen selbst erstellen, um objektorientierten Code zu schreiben, wie er in der VFS-Schicht (und an vielen anderen Stellen) des Linux-Kernels zu sehen ist.
Florian

2
@krynr: Virtuelle Funktionen sind Funktionszeiger nur für Compiler-Implementierer. Wenn Sie fragen müssen, wozu sie gut sind, ist es wahrscheinlich (hoffentlich!) unwahrscheinlich, dass Sie den virtuellen Funktionsmechanismus eines Compilers implementieren müssen.
sbi

@sbi: Du hast natürlich recht. Ich denke jedoch, dass es hilfreich ist zu verstehen, was in der Abstraktion vor sich geht. Wenn Sie Ihre eigene vtable in C implementieren und objektorientierten Code schreiben, haben Sie eine wirklich gute Lernerfahrung.
Florian

Brownie Punkte für die Antwort auf das Leben, das Universum und alles zusammen mit dem, was von OP verlangt wurde
simplename

41

Nun, ich benutze sie im Allgemeinen (professionell) in Sprungtabellen (siehe auch diese StackOverflow-Frage ).

Sprungtabellen werden häufig (aber nicht ausschließlich) in Finite-State-Maschinen verwendet , um sie datengesteuert zu machen. Anstelle von verschachteltem Schalter / Fall

  switch (state)
     case A:
       switch (event):
         case e1: ....
         case e2: ....
     case B:
       switch (event):
         case e3: ....
         case e1: ....

Sie können ein 2D-Array von Funktionszeigern erstellen und einfach aufrufen handleEvent[state][event]


24

Beispiele:

  1. Benutzerdefinierte Sortierung / Suche
  2. Verschiedene Muster (wie Strategie, Beobachter)
  3. Rückrufe

1
Sprungtabelle ist eine der wichtigsten Anwendungen.
Ashish

Wenn es einige Beispiele gäbe, würde dies meine Gegenstimme bekommen.
Donal Fellows

1
Strategie und Beobachter lassen sich wahrscheinlich besser mit virtuellen Funktionen implementieren, wenn C ++ verfügbar ist. Ansonsten +1.
Billy ONeal

Ich denke, dass die kluge Verwendung von Funktionszeigern den Beobachter kompakter und leichter machen kann
Andrey

@BillyONeal Nur wenn Sie sich strikt an die GoF-Definition halten, durch die Javaismen sickern. Ich würde std::sort's compParameter als Strategie beschreiben
Caleth

10

Das "klassische" Beispiel für die Nützlichkeit von Funktionszeigern ist die C-Bibliotheksfunktion qsort(), die eine schnelle Sortierung implementiert. Um für alle Datenstrukturen, die der Benutzer möglicherweise erstellt, universell zu sein, sind einige leere Zeiger auf sortierbare Daten und ein Zeiger auf eine Funktion erforderlich, die zwei Elemente dieser Datenstrukturen vergleichen kann. Dies ermöglicht es uns, die Funktion unserer Wahl für den Job zu erstellen und sogar die Vergleichsfunktion zur Laufzeit auszuwählen, z. B. zum Sortieren aufsteigend oder absteigend.


7

Stimmen Sie allen oben genannten Punkten zu, plus ... Wenn Sie eine DLL zur Laufzeit dynamisch laden, benötigen Sie Funktionszeiger, um die Funktionen aufzurufen.


1
Ich mache das die ganze Zeit, um Windows XP zu unterstützen und immer noch Windows 7-Extras zu verwenden. +1.
Billy ONeal

7

Ich werde hier gegen den Strom gehen.

In C sind Funktionszeiger die einzige Möglichkeit, die Anpassung zu implementieren, da kein OO vorhanden ist.

In C ++ können Sie entweder Funktionszeiger oder Funktoren (Funktionsobjekte) für dasselbe Ergebnis verwenden.

Die Funktoren haben aufgrund ihrer Objektnatur eine Reihe von Vorteilen gegenüber Rohfunktionszeigern, insbesondere:

  • Sie können mehrere Überlastungen der operator()
  • Sie können Status / Verweis auf vorhandene Variablen haben
  • Sie können vor Ort gebaut werden ( lambdaund bind)

Ich persönlich bevorzuge Funktoren gegenüber Funktionszeigern (trotz des Boilerplate-Codes), hauptsächlich weil die Syntax für Funktionszeiger leicht haarig werden kann (aus dem Funktionszeiger-Tutorial ):

typedef float(*pt2Func)(float, float);
  // defines a symbol pt2Func, pointer to a (float, float) -> float function

typedef int (TMyClass::*pt2Member)(float, char, char);
  // defines a symbol pt2Member, pointer to a (float, char, char) -> int function
  // belonging to the class TMyClass

Das einzige Mal, dass ich Funktionszeiger gesehen habe, bei denen Funktoren nicht verwendet werden konnten, war in Boost.Spirit. Sie haben die Syntax völlig missbraucht, um eine beliebige Anzahl von Parametern als einzelnen Vorlagenparameter zu übergeben.

 typedef SpecialClass<float(float,float)> class_type;

Da jedoch verschiedene Vorlagen und Lambdas vor der Tür stehen, bin ich mir nicht sicher, ob wir Funktionszeiger in reinem C ++ - Code schon lange verwenden werden.


Nur weil Sie Ihre Funktionszeiger nicht sehen, heißt das nicht, dass Sie sie nicht verwenden. Jedes Mal, wenn Sie eine virtuelle Funktion aufrufen, Boost verwenden bindoder Funktionszeiger verwenden (es sei denn, der Compiler kann sie wegoptimieren) function. Es ist so, als würden wir in C ++ keine Zeiger verwenden, weil wir intelligente Zeiger verwenden. Wie auch immer, ich picke nicht.
Florian

3
@ krynr: Ich werde höflich widersprechen. Was zählt, ist, was Sie sehen und eingeben , das ist die Syntax, die Sie verwenden. Es sollte Ihnen egal sein, wie alles hinter den Kulissen funktioniert: Darum geht es bei der Abstraktion .
Matthieu M.

5

In C ist die klassische Verwendung die qsort-Funktion , wobei der vierte Parameter ein Zeiger auf eine Funktion ist, mit der die Reihenfolge innerhalb der Sortierung ausgeführt wird. In C ++ würde man dazu neigen, Funktoren (Objekte, die wie Funktionen aussehen) für solche Dinge zu verwenden.


2
@KennyTM: Ich habe auf die einzige andere Instanz davon in der C-Standardbibliothek hingewiesen. Die von Ihnen zitierten Beispiele sind Teil von Bibliotheken von Drittanbietern.
Billy ONeal

5

Ich habe kürzlich Funktionszeiger verwendet, um eine Abstraktionsschicht zu erstellen.

Ich habe ein Programm in reinem C geschrieben, das auf eingebetteten Systemen läuft. Es unterstützt mehrere Hardwarevarianten. Abhängig von der Hardware, auf der ich ausgeführt werde, müssen verschiedene Versionen einiger Funktionen aufgerufen werden.

Bei der Initialisierung ermittelt das Programm, auf welcher Hardware es ausgeführt wird, und füllt die Funktionszeiger. Alle übergeordneten Routinen im Programm rufen nur die Funktionen auf, auf die durch Zeiger verwiesen wird. Ich kann Unterstützung für neue Hardwarevarianten hinzufügen, ohne die übergeordneten Routinen zu berühren.

Früher habe ich switch / case-Anweisungen verwendet, um die richtigen Funktionsversionen auszuwählen, aber dies wurde unpraktisch, als das Programm immer mehr Hardwarevarianten unterstützte. Ich musste überall Fallaussagen hinzufügen.

Ich habe auch Zwischenfunktionsebenen ausprobiert, um herauszufinden, welche Funktion verwendet werden soll, aber sie haben nicht viel geholfen. Ich musste immer noch Fallanweisungen an mehreren Stellen aktualisieren, wenn wir eine neue Variante hinzufügten. Mit den Funktionszeigern muss ich nur die Initialisierungsfunktion ändern.


3

Wie Rich oben sagte, ist es in Windows sehr üblich, dass Funktionszeiger auf eine Adresse verweisen, in der Funktionen gespeichert sind.

Wenn Sie C languageauf einer Windows-Plattform programmieren, laden Sie im Grunde genommen eine DLL-Datei in den Primärspeicher (using LoadLibrary). Um die in DLL gespeicherten Funktionen zu verwenden, müssen Sie Funktionszeiger erstellen und auf diese Adresse verweisen (using GetProcAddress).

Verweise:


2

Funktionszeiger können in C verwendet werden, um eine Schnittstelle zum Programmieren zu erstellen. Abhängig von der spezifischen Funktionalität, die zur Laufzeit benötigt wird, kann dem Funktionszeiger eine andere Implementierung zugewiesen werden.


2

Meine Hauptanwendung waren CALLBACKS: Wenn Sie Informationen zu einer Funktion speichern müssen, um sie später aufzurufen .

Angenommen, Sie schreiben Bomberman. 5 Sekunden nachdem die Person die Bombe fallen gelassen hat, sollte sie explodieren ( explode()Funktion aufrufen ).

Jetzt gibt es zwei Möglichkeiten. Eine Möglichkeit besteht darin, alle Bomben auf dem Bildschirm zu "untersuchen", um festzustellen, ob sie in der Hauptschleife explodieren können.

foreach bomb in game 
   if bomb.boomtime()
       bomb.explode()

Eine andere Möglichkeit besteht darin, einen Rückruf an Ihr Uhrensystem anzuschließen. Wenn eine Bombe gepflanzt wird, fügen Sie einen Rückruf hinzu, damit sie zur richtigen Zeit bomb.explode () aufruft .

// user placed a bomb
Bomb* bomb = new Bomb()
make callback( function=bomb.explode, time=5 seconds ) ;

// IN the main loop:
foreach callback in callbacks
    if callback.timeToRun
         callback.function()

Hier callback.function()kann jede Funktion sein , da es sich um einen Funktionszeiger handelt.


Die Frage wurde mit [C] und [C ++] markiert, nicht mit einem anderen Sprach-Tag. Das Bereitstellen von Codefragmenten in einer anderen Sprache ist daher etwas unangebracht.
cmaster

2

Verwendung des Funktionszeigers

Um Anruffunktion dynamisch basierend auf Benutzereingaben. In diesem Fall erstellen Sie eine Zuordnung von Zeichenfolge und Funktionszeiger.

#include<iostream>
#include<map>
using namespace std;
//typedef  map<string, int (*)(int x, int y) > funMap;
#define funMap map<string, int (*)(int, int)>
funMap objFunMap;

int Add(int x, int y)
{
    return x+y;
}
int Sub(int x, int y)
{
        return x-y;
}
int Multi(int x, int y)
{
        return x*y;
}
void initializeFunc()
{
        objFunMap["Add"]=Add;
        objFunMap["Sub"]=Sub;
        objFunMap["Multi"]=Multi;
}
int main()
{
    initializeFunc();

    while(1)
    {
        string func;
        cout<<"Enter your choice( 1. Add 2. Sub 3. Multi) : ";
        int no, a, b;
        cin>>no;

        if(no==1)
            func = "Add";
        else if(no==2)
            func = "Sub";
        else if(no==3)
            func = "Multi";
        else 
            break;

        cout<<"\nEnter 2 no :";
                cin>>a>>b;

        //function is called using function pointer based on user input
        //If user input is 2, and a=10, b=3 then below line will expand as "objFuncMap["Sub"](10, 3)"
        int ret = objFunMap[func](a, b);      
        cout<<ret<<endl;
    }
    return 0;
}

Auf diese Weise haben wir den Funktionszeiger in unserem tatsächlichen Buchungskreis verwendet. Sie können 'n' Anzahl von Funktionen schreiben und sie mit dieser Methode aufrufen.

AUSGABE:

    Geben Sie Ihre Wahl ein (1. Addieren 2. Sub 3. Multi): 1
    Geben Sie 2 no: 2 4 ein
    6
    Geben Sie Ihre Wahl ein (1. Addieren 2. Sub 3. Multi): 2
    Geben Sie 2 no: 10 3 ein
    7
    Geben Sie Ihre Wahl ein (1. Addieren 2. Sub 3. Multi): 3
    Geben Sie 2 no: 3 6 ein
    18

2

Eine andere Perspektive, zusätzlich zu anderen guten Antworten hier:

In C verwenden Sie nur Funktionszeiger, keine (direkten) Funktionen.

Ich meine, Sie schreiben Funktionen, aber Sie können Funktionen nicht manipulieren . Es gibt keine Laufzeitdarstellung einer Funktion als solche, die Sie verwenden können. Sie können nicht einmal "eine Funktion" aufrufen. Wenn Sie schreiben:

my_function(my_arg);

Was Sie tatsächlich sagen, ist "Rufen Sie den my_functionZeiger mit dem angegebenen Argument auf". Sie rufen über einen Funktionszeiger auf. Dieser Zerfall zum Funktionszeiger bedeutet, dass die folgenden Befehle dem vorherigen Funktionsaufruf entsprechen:

(&my_function)(my_arg);
(*my_function)(my_arg);
(**my_function)(my_arg);
(&**my_function)(my_arg);
(***my_function)(my_arg);

und so weiter (danke @LuuVinhPhuc).

Sie verwenden also bereits Funktionszeiger als Werte . Natürlich möchten Sie Variablen für diese Werte haben - und hier kommen alle Verwendungszwecke anderer Metionen ins Spiel: Polymorphismus / Anpassung (wie in qsort), Rückrufe, Sprungtabellen usw.

In C ++ sind die Dinge etwas komplizierter, da wir Lambdas und Objekte mit operator()und sogar eine std::functionKlasse haben, aber das Prinzip ist meistens immer noch dasselbe.


2
Interessanterweise noch mehr, können Sie die Funktion aufrufen , wie (&my_function)(my_arg), (*my_function)(my_arg), (**my_function)(my_arg), (&**my_function)(my_arg), (***my_function)(my_arg)... weil Funktionen abklingen zu Funktionszeiger
phuclv

1

Für OO-Sprachen, um polymorphe Aufrufe hinter den Kulissen durchzuführen (dies gilt auch für C bis zu einem gewissen Punkt, denke ich).

Darüber hinaus sind sie sehr nützlich, um einer anderen Funktion (foo) zur Laufzeit ein anderes Verhalten zu verleihen. Das macht die Funktion zu einer Funktion höherer Ordnung. Abgesehen von seiner Flexibilität macht dies den foo-Code besser lesbar, da Sie damit die zusätzliche Logik des "Wenn-Sonst" herausholen können.

Es ermöglicht viele andere nützliche Dinge in Python wie Generatoren, Verschlüsse usw.


0

Ich verwende häufig Funktionszeiger, um Mikroprozessoren mit 1-Byte-Opcodes zu emulieren. Ein Array von 256 Funktionszeigern ist der natürliche Weg, dies zu implementieren.


0

Eine Verwendung des Funktionszeigers könnte darin bestehen, dass wir den Code, in dem die Funktion aufgerufen wird, möglicherweise nicht ändern möchten (was bedeutet, dass der Aufruf möglicherweise bedingt ist und wir unter verschiedenen Bedingungen unterschiedliche Verarbeitungsarten ausführen müssen). Hier sind die Funktionszeiger sehr praktisch, da wir den Code an der Stelle, an der die Funktion aufgerufen wird, nicht ändern müssen. Wir rufen die Funktion einfach mit dem Funktionszeiger mit entsprechenden Argumenten auf. Der Funktionszeiger kann so eingestellt werden, dass er bedingt auf verschiedene Funktionen zeigt. (Dies kann irgendwo während der Initialisierungsphase erfolgen). Darüber hinaus ist das obige Modell sehr hilfreich, wenn wir nicht in der Lage sind, den Code dort zu ändern, wo er aufgerufen wird (angenommen, es handelt sich um eine Bibliotheks-API, die wir nicht ändern können). Die API verwendet einen Funktionszeiger zum Aufrufen der entsprechenden benutzerdefinierten Funktion.


0

Ich werde versuchen, hier eine etwas umfassende Liste zu geben:

  • Rückrufe : Passen Sie einige (Bibliotheks-) Funktionen mit vom Benutzer bereitgestelltem Code an. Das beste Beispiel ist qsort(), aber auch nützlich, um Ereignisse zu behandeln (wie eine Schaltfläche, die einen Rückruf aufruft, wenn darauf geklickt wird) oder um einen Thread zu starten ( pthread_create()).

  • Polymorphismus : Die vtable in einer C ++ - Klasse ist nichts anderes als eine Tabelle mit Funktionszeigern. Ein C-Programm kann auch eine vtable für einige seiner Objekte bereitstellen:

    struct Base;
    struct Base_vtable {
        void (*destruct)(struct Base* me);
    };
    struct Base {
        struct Base_vtable* vtable;
    };
    
    struct Derived;
    struct Derived_vtable {
        struct Base_vtable;
        void (*frobnicate)(struct Derived* me);
    };
    struct Derived {
        struct Base;
        int bar, baz;
    }

    Der Konstruktor von Derivedwürde dann seine vtableMitgliedsvariable mit den Implementierungen von destructund der Klasse der abgeleiteten Klasse auf ein globales Objekt setzenfrobnicate , und Code, der zum Zerstören eines Destruktors benötigt wird, struct Base*würde einfach base->vtable->destruct(base)die richtige Version des Destruktors aufrufen, unabhängig davon, auf welche abgeleitete Klasse basetatsächlich verweist .

    Ohne Funktionszeiger müsste der Polymorphismus mit einer Armee von Schalterkonstrukten wie codiert werden

    switch(me->type) {
        case TYPE_BASE: base_implementation(); break;
        case TYPE_DERIVED1: derived1_implementation(); break;
        case TYPE_DERIVED2: derived2_implementation(); break;
        case TYPE_DERIVED3: derived3_implementation(); break;
    }

    Dies wird ziemlich schnell ziemlich unhandlich.

  • Dynamisch geladener Code : Wenn ein Programm ein Modul in den Speicher lädt und versucht, seinen Code aufzurufen, muss es einen Funktionszeiger durchlaufen.

Alle Verwendungen von Funktionszeigern, die ich gesehen habe, fallen direkt in eine dieser drei breiten Klassen.

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.