1400

私は最近、C で関数ポインターを使用した経験がありました。

そこで、自分自身の質問に答えるという伝統を守りながら、このテーマについて簡単に知りたい人のために、非常に基本的なことの簡単な要約を作成することにしました。

4

11 に答える 11

1628

C の関数ポインター

参照する基本的な関数から始めましょ

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

まず、2 を受け取ってintを返す関数へのポインタを定義しましょうint:

int (*functionPtr)(int,int);

これで、関数を安全に指すことができます。

functionPtr = &addInt;

関数へのポインタができたので、それを使用してみましょう。

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

別の関数へのポインターの受け渡しは、基本的に同じです。

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

戻り値に関数ポインタを使用することもできます (遅れないようにしてください。面倒です)。

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

しかし、 a を使用する方がはるかに優れています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;
}
于 2009-05-08T15:49:59.090 に答える
324

C の関数ポインターを使用して、C でオブジェクト指向プログラミングを実行できます。

たとえば、次の行は C で記述されています。

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

はい、演算子->と the がないことは明らかですが、あるクラスのテキストを に設定していることを暗示しているように思えます。newString"hello"

関数ポインタを使用すると、C でメソッドをエミュレートできます。

これはどのように達成されますか?

このStringクラスは実際には、structメソッドをシミュレートする方法として機能する一連の関数ポインターを備えています。以下は、Stringクラスの部分宣言です。

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();

ご覧のとおり、Stringクラスのメソッドは実際には、宣言された関数への関数ポインターです。のインスタンスを準備する際に、それぞれの関数への関数ポインタを設定するために関数が呼び出されますStringnewString

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

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

    self->set(self, "");

    return self;
}

たとえばgetString、メソッドを呼び出すことによって呼び出される関数はget、次のように定義されます。

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

注目できることの 1 つは、オブジェクトのインスタンスの概念がなく、実際にはオブジェクトの一部であるメソッドを持つため、呼び出しごとに「自己オブジェクト」を渡す必要があることです。(そして、internalは、以前のコード リストから省略された単なる隠しstructオブジェクトです。これは、情報を隠蔽する方法ですが、関数ポインタには関係ありません。)

したがって、 を実行できるのではなくs1->set("hello");、 オブジェクトを渡して でアクションを実行する必要がありますs1->set(s1, "hello")

自分自身への参照を渡さなければならない小さな説明はさておき、次の部分であるC での継承に進みます。

のサブクラスを作りたいとしましょStringImmutableString。文字列を不変にするために、メソッドにはアクセスできませんが、およびへのsetアクセスを維持し、「コンストラクター」に を受け入れるように強制します。getlengthchar*

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

基本的に、すべてのサブクラスで、使用可能なメソッドは再び関数ポインターです。今回は、setメソッドの宣言が存在しないため、 で呼び出すことはできませんImmutableString

の実装に関してはImmutableString、関連する唯一のコードは「コンストラクタ」関数である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;
}

のインスタンス化では、 and メソッドImmutableStringへの関数ポインタは、内部に格納されたオブジェクトである変数を介して、実際にgetandメソッドを参照します。lengthString.getString.lengthbaseString

関数ポインタを使用すると、スーパークラスからメソッドを継承できます。

さらに、C でポリモーフィズムを続けることができます。

たとえば、何らかの理由でメソッドの動作をクラスで常にlength返すように変更したい場合は、次のようにするだけです。0ImmutableString

  1. lengthオーバーライドメソッドとして機能する関数を追加します。
  2. 「コンストラクタ」に移動し、関数ポインタをオーバーライドlengthメソッドに設定します。

にオーバーライドlengthメソッドを追加するには、次をImmutableString追加しlengthOverrideMethodます。

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

次に、コンストラクター内のメソッドの関数ポインターが次のlengthようにフックされます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;
}

lengthクラス内のメソッドがImmutableStringクラスと同じ動作をするのではなく、Stringメソッドが関数lengthで定義された動作を参照するようになりました。lengthOverrideMethod

C でオブジェクト指向プログラミング スタイルを使用して記述する方法をまだ学習中であるという免責事項を追加する必要があります。そのため、うまく説明できていない点や、OOP の最適な実装方法に関して的外れな点がある可能性があります。しかし、私の目的は、関数ポインターの多くの使用法の 1 つを説明することでした。

C でオブジェクト指向プログラミングを実行する方法の詳細については、次の質問を参照してください。

于 2009-05-08T16:32:47.773 に答える
250

解雇のガイド:コードを手動でコンパイルしてx86マシンのGCCで関数ポインターを悪用する方法:

これらの文字列リテラルは、32ビットx86マシンコードのバイトです。 x86命令0xC3です。ret

通常、これらを手動で作成することはありません。アセンブリ言語で作成してから、アセンブラを使用nasmしてフラットバイナリにアセンブルし、C文字列リテラルに16進ダンプします。

  1. EAXレジスタの現在の値を返します

    int eax = ((int(*)())("\xc3 <- This returns the value of the EAX register"))();
    
  2. スワップ関数を書く

    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. 毎回いくつかの関数を呼び出して、forループカウンターを1000に書き込みます

    ((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. 100までカウントする再帰関数を作成することもできます

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

コンパイラは、テキストセグメントの一部として(関数のコードとともに)リンクされている.rodataセクション(またはWindows)に文字列リテラルを配置することに注意してください。.rdata

テキストセグメントにはRead+Exec権限があるため、文字列リテラルを関数ポインタにキャストすることは、動的に割り当てられたメモリのようにシステムコールを必要mprotect()とせずに機能します。VirtualProtect()(またはgcc -z execstack、クイックハックとして、プログラムをスタック+データセグメント+ヒープ実行可能ファイルにリンクします。)


これらを逆アセンブルするには、これをコンパイルしてバイトにラベルを付け、逆アセンブラを使用します。

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

でコンパイルgcc -c -m32 foo.cおよび逆アセンブルするとobjdump -D -rwC -Mintel、アセンブリを取得できます。このコードは、EBX(呼び出し保存レジスタ)を壊してABIに違反し、一般に非効率的であることがわかります。

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

このマシンコードは、(おそらく)Windows、Linux、OS Xなどの32ビットコードで機能します。これらすべてのOSのデフォルトの呼び出し規約は、レジスタでより効率的にではなく、スタックで引数を渡します。ただし、EBXは通常のすべての呼び出し規約で呼び出し保存されているため、保存/復元せずにスクラッチレジスタとして使用すると、呼び出し元が簡単にクラッシュする可能性があります。

于 2011-04-09T00:51:16.587 に答える
120

関数ポインターの私のお気に入りの用途の 1 つは、安価で簡単なイテレーターとして使用することです。

#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);
}
于 2009-05-08T16:26:09.800 に答える
25

基本的な宣言子があれば、関数ポインターを簡単に宣言できるようになります。

  • id::IDID
  • ポインタ*D:: Dポインタ
  • 関数::D(<parameters>)パラメータ返すD関数が返されます<>

Dは、同じルールを使用して作成された別の宣言子です。ID最後に、どこかで、宣言されたエンティティの名前である(例については以下を参照)で終わります。何も受け取らずにintを返す関数へのポインターを取り、charを取り、intを返す関数へのポインターを返す関数を作成してみましょう。type-defsを使用すると、次のようになります

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

ご覧のとおり、typedefを使用して構築するのは非常に簡単です。typedefがなければ、一貫して適用される上記の宣言子ルールでも難しくありません。ご覧のとおり、ポインターが指す部分と、関数が返すものを見逃しました。これは宣言の左端に表示されるものであり、重要ではありません。宣言者がすでに作成されている場合は、最後に追加されます。そうしよう。一貫して構築し、最初の言葉で-とを使用して構造を示し[ます]

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

ご覧のとおり、宣言子を次々に追加することで、型を完全に記述することができます。建設は2つの方法で行うことができます。1つはボトムアップで、非常に正しいもの(葉)から始まり、識別子に至るまで進みます。もう1つの方法は、識別子から始めて、葉に向かって下がるトップダウンです。両方の方法を示します。

一気飲み

構築は右側のものから始まります。返されるものは、charを受け取る関数です。宣言者を区別するために、番号を付けます。

D1(char);

些細なことなので、charパラメータを直接挿入しました。に置き換えて、宣言子へのポインタを追加しD1ます*D2。かっこを囲む必要があることに注意してください*D2*-operatorこれは、と関数呼び出し演算子の優先順位を調べることで知ることができます()。括弧がないと、コンパイラはそれを。として読み取ります*(D2(char p))*D2もちろん、それはもはやD1の単純な置き換えではありません。宣言子の前後には常に括弧を使用できます。したがって、実際には、それらを追加しすぎても、何も問題はありません。

(*D2)(char);

返品タイプが完成しました!さて、戻り値を取るD2関数宣言関数に置き換えましょう。これは現在のところです。<parameters>D3(<parameters>)

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

今回はポインタ宣言子ではなく関数宣言子になりたい ので、括弧は必要ないことに注意してください。D3素晴らしいです。残っているのはそのパラメータだけです。パラメータは、return型を実行したのとまったく同じように実行されますが、。にchar置き換えられvoidます。だから私はそれをコピーします:

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

D2そのパラメーターが完成したので、私はに置き換えましたID1(これはすでに関数へのポインターであり、別の宣言子は必要ありません)。ID1パラメータの名前になります。さて、最後に、これらすべての宣言者が変更するタイプを追加することを上で説明しました。これは、すべての宣言の左端に表示されます。関数の場合、それが戻り型になります。タイプなどを指すポインタの場合...タイプを書き留めると興味深いですが、右端に逆の順序で表示されます:)とにかく、それを置き換えると完全な宣言が得られます。もちろん両方intの時間。

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

ID0その例では、関数の識別子を呼び出しました。

トップダウン

これは、タイプの説明の左端にある識別子から始まり、右を通り抜けるときにその宣言子をラップします。パラメータを返す関数から始めます<>

ID0(<parameters>)

説明の次のこと(「戻る」の後)は、へのポインタでした。それを取り入れましょう:

*ID0(<parameters>)

次に、パラメータを取得して<パラメータを>返す関数を取得しました。パラメータは単純なcharなので、非常に簡単なので、すぐにもう一度入力します。

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

追加した括弧に注意してください。*最初にバインドしてから、バインドする必要があるため(char)です。それ以外の場合は、関数を受け取る関数を読み取り、関数を返します<...。>いいえ、関数を返す関数は許可されていません。

ここで、<パラメータを設定する必要があります>。あなたはすでにそれを行う方法のアイデアを持っていると思うので、私は派生の短いバージョンを示します。

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

intボトムアップで行ったように、宣言者の前に置くだけで、終了します

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

いいこと

ボトムアップとトップダウンのどちらが良いですか?私はボトムアップに慣れていますが、トップダウンの方が快適な人もいます。それは好みの問題だと思います。ちなみに、その宣言ですべての演算子を適用すると、intを取得することになります。

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

これは、Cの宣言の優れたプロパティです。宣言は、これらの演算子が識別子を使用する式で使用されている場合、左端の型を生成することを表明します。アレイの場合も同様です。

この小さなチュートリアルが気に入っていただけたら幸いです。これで、関数の奇妙な宣言構文について人々が疑問に思ったときに、これにリンクできます。私はC内部をできるだけ少なくしようとしました。その中のものを自由に編集/修正してください。

于 2009-05-09T02:05:44.447 に答える
24

関数ポインターのもう 1 つの良い使い方:
バージョン間の切り替えを簡単に行う

これらは、さまざまな時期やさまざまな開発段階でさまざまな機能が必要な場合に使用すると非常に便利です。たとえば、コンソールを備えたホスト コンピューターでアプリケーションを開発していますが、ソフトウェアの最終リリースは Avnet ZedBoard に配置されます (ディスプレイとコンソール用のポートがありますが、それらは必要ありません/必要ありません)。最終リリース)。そのため、開発中はprintfステ​​ータスとエラー メッセージを表示するために使用しますが、完了したら何も出力したくありません。これが私がやったことです:

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

version.c存在する 2 つの関数プロトタイプを定義します。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;
}

関数ポインターが次のようにプロトタイプ化さversion.hれていることに注意してください。

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

アプリケーションで参照されると、まだ定義されていない指している場所から実行が開始されます。

では、で定義されているバージョンに応じて、一意の関数 (関数シグネチャが一致する関数) が割り当てられている関数version.cに注目してください。board_init()zprintfversion.h

zprintf = &printf;zprintf はデバッグ目的で printf を呼び出します

また

zprintf = &noprint;zprintf は返されるだけで、不要なコードは実行されません

コードを実行すると、次のようになります。

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

上記のコードはprintf、デバッグ モードの場合は使用し、リリース モードの場合は何もしません。これは、プロジェクト全体を調べてコードをコメントアウトまたは削除するよりもはるかに簡単です。私がする必要があるのは、バージョンを変更するversion.hことだけで、あとはコードがやってくれます!

于 2013-06-10T19:56:41.133 に答える
20

通常、関数ポインタは で定義されtypedef、パラメータおよび戻り値として使用されます。

上記の回答はすでに多くのことを説明していますが、完全な例を示します。

#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;
}
于 2014-11-10T08:50:57.897 に答える
16

最初から開始する関数には、実行を開始する場所からのメモリアドレスがあります。アセンブリ言語では、(call "function's memory address").C に戻ります 関数にメモリ アドレスがある場合、それらは C のポインターによって操作できます。したがって、C の規則により

1.最初に、関数へのポインタを宣言する必要があります 2.目的の関数のアドレスを渡します

****注->関数は同じタイプでなければなりません****

この簡単なプログラムはすべてを説明します。

#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");
}

ここに画像の説明を入力その後、マシンがそれらをどのように理解するかを見てみましょう.32ビットアーキテクチャでの上記のプログラムのマシン命令を垣間見ることができます.

赤いマークの領域は、アドレスがどのように交換され、eax に格納されているかを示しています。次に、eax の call 命令です。eax には、関数の目的のアドレスが含まれています。

于 2014-09-26T08:09:51.910 に答える