Cの関数ポインターのtypedefについて


237

引数を持つ関数へのポインタのtypedefを持つ他の人々のコードを読んだとき、私はいつも少し困惑してきました。少し前にCで記述された数値アルゴリズムを理解しようとすると、このような定義に取り掛かるのにしばらく時間がかかったことを思い出します。では、関数へのポインタの良いtypedefを作成する方法(することとしないこと)に関するヒントと考えを共有して、なぜそれらが役立つのか、他の人の作業をどのように理解するのかについて教えてください。ありがとう!


1
いくつか例を挙げていただけますか?
Artelius 2009年

2
関数ポインターのマクロの代わりに、関数ポインターのtypedefを意味しませんか?前者は見たが後者は見なかった。
dave4420 2009年

回答:


296

signal()C標準の関数を考えます。

extern void (*signal(int, void(*)(int)))(int);

完全に曖昧に明白-引数として整数を取り、何も返さない関数へのポインターと整数の2つの引数を取る関数であり、それは(signal())を引数として整数を取り、返す関数へのポインターを返します何も。

あなたが書く場合:

typedef void (*SignalHandler)(int signum);

次に、代わりに次のように宣言できますsignal()

extern  SignalHandler signal(int signum, SignalHandler handler);

これは同じことを意味しますが、通常は多少読みやすいと見なされています。関数がintandおよびa SignalHandlerを取り、a を返すことはより明確ですSignalHandler

ただし、慣れるには少し時間がかかります。できませんが、SignalHandler typedef関数定義でを使用してシグナルハンドラー関数を記述します。

私は今でも関数ポインタを次のように呼び出すことを好む古い学校にいます:

(*functionpointer)(arg1, arg2, ...);

現代の構文は単に次のものを使用します:

functionpointer(arg1, arg2, ...);

それが機能する理由がわかりfunctionpointerます。呼び出される関数ではなく、変数が初期化されている場所を探す必要があることを知りたいだけです。


サムはコメントしました:

この説明は以前に見たことがあります。そして、今の場合と同じように、私は2つのステートメント間の関連性を理解できなかったと思います。

    extern void (*signal(int, void()(int)))(int);  /*and*/

    typedef void (*SignalHandler)(int signum);
    extern SignalHandler signal(int signum, SignalHandler handler);

または、私が聞きたいのは、2つ目のバージョンを作成するために使用できる基本的な概念は何ですか?「SignalHandler」と最初のtypedefを接続する基本は何ですか?ここで説明する必要があるのは、typedefが実際にここで行っていることです。

もう一度やってみましょう。これらの最初のコードは、C標準から直接持ち上げられた-私はそれを再入力し、括弧が正しいことを確認した(修正するまでは-覚えるのは難しいCookieです)。

まず、typedefは型のエイリアスを導入することを覚えておいてください。したがって、エイリアスはSignalHandlerであり、そのタイプは次のとおりです。

整数を引数として取り、何も返さない関数へのポインター。

「何も返さない」の部分は綴られていvoidます。整数である引数は(私が信じる)自明です。次の表記は、Cが指定された引数を取り、指定された型を返す関数へのポインターをどのように綴るかを単純に(またはそうではありません)しています。

type (*function)(argtypes);

シグナルハンドラータイプを作成した後、それを使用して変数などを宣言できます。例えば:

static void alarm_catcher(int signum)
{
    fprintf(stderr, "%s() called (%d)\n", __func__, signum);
}

static void signal_catcher(int signum)
{
    fprintf(stderr, "%s() called (%d) - exiting\n", __func__, signum);
    exit(1);
}

static struct Handlers
{
    int              signum;
    SignalHandler    handler;
} handler[] =
{
    { SIGALRM,   alarm_catcher  },
    { SIGINT,    signal_catcher },
    { SIGQUIT,   signal_catcher },
};

int main(void)
{
    size_t num_handlers = sizeof(handler) / sizeof(handler[0]);
    size_t i;

    for (i = 0; i < num_handlers; i++)
    {
        SignalHandler old_handler = signal(handler[i].signum, SIG_IGN);
        if (old_handler != SIG_IGN)
            old_handler = signal(handler[i].signum, handler[i].handler);
        assert(old_handler == SIG_IGN);
    }

    ...continue with ordinary processing...

    return(EXIT_SUCCESS);
}

シグナルハンドラでの使用を回避する方法に注意してくださいprintf()

それでは、ここで何をしましたか-コードをきれいにコンパイルするために必要となる4つの標準ヘッダーを省略しますか?

最初の2つの関数は、単一の整数を取り、何も返さない関数です。それらの1つは実際にはのおかげでまったく戻りませんexit(1);が、もう1つはメッセージを印刷した後に戻ります。C標準では、シグナルハンドラ内ではほとんど実行できないことに注意してください。POSIXは許可されていることについてはもう少し寛大ですが、公式には呼び出しを許可していませんfprintf()。受信した信号番号もプリントアウトしています。ではalarm_handler()機能、値は常になりますSIGALRMそれはそれがためのハンドラであることを唯一の信号であるとして、しかしsignal_handler()得る可能性がありますSIGINTまたはSIGQUIT同じ機能を両方に使用されているため、シグナル番号として。

次に、構造体の配列を作成します。各要素は、シグナル番号とそのシグナルにインストールするハンドラーを識別します。私は3つの信号について心配することにしました。私はしばしばSIGHUPSIGPIPEまた、SIGTERMそれらが定義されているかどうか(#ifdef条件付きコンパイル)について心配しますが、それだけで状況が複雑になります。私はおそらくのsigaction()代わりにPOSIXも使用するでしょうsignal()が、それは別の問題です。私たちが始めたものに固執しましょう。

このmain()関数は、インストールされるハンドラーのリストを反復処理します。各ハンドラーについて、最初にを呼び出しsignal()て、プロセスが現在シグナルを無視しているかどうかを確認し、その間SIG_IGN、ハンドラーとしてインストールします。これにより、シグナルが無視されたままになります。シグナルが以前に無視されていなかった場合は、signal()再度呼び出して、今度は優先シグナルハンドラーをインストールします。(他の値が考えられる SIG_DFL。、信号のデフォルトのシグナルハンドラ)への最初の呼び出しので「(信号)」にハンドラを設定するSIG_IGNsignal()、以前のエラーハンドラを返し、値oldの後ifのステートメントでなければなりませんSIG_IGN-それゆえ主張。(まあ、それは可能性がありますSIG_ERR 何かが劇的にうまくいかなかった場合-しかし、私はアサートの発砲からそれについて学びます。)

その後、プログラムはその処理を行い、正常に終了します。

関数の名前は、適切なタイプの関数へのポインタと見なすことができることに注意してください。たとえば、初期化子のように、関数呼び出しの括弧を適用しない場合、関数名は関数ポインターになります。これが、pointertofunction(arg1, arg2)表記法を介して関数を呼び出すことが妥当である理由でもあります。を見ると、それは関数へのポインターであり、関数ポインターを介した関数の呼び出しであるalarm_handler(1)と見なすことalarm_handlerができalarm_handler(1)ます。

これまでのところ、SignalHandler2つのシグナルハンドラー関数が提供する適切なタイプの値が割り当てられている限り、変数は比較的簡単に使用できることを示しました。

さて、質問に戻ります-の2つの宣言はどのようsignal()に相互に関連していますか。

2番目の宣言を見てみましょう。

 extern SignalHandler signal(int signum, SignalHandler handler);

関数名と型を次のように変更した場合:

 extern double function(int num1, double num2);

これをintとa doubleを引数として取り、double値を返す関数として解釈しても問題はありません(問題がある場合は、気にしない方がいいでしょう。ただし、質問をするのは慎重にすべきです)これが問題である場合)

これでdoublesignal()関数はの代わりにSignalHandler、2番目の引数としてa を取り、その結果として1を返します。

それを次のように扱うこともできるメカニズム:

extern void (*signal(int signum, void(*handler)(int signum)))(int signum);

説明するのは難しいです-だから私はおそらくそれを台無しにするでしょう。今回はパラメーター名を指定しましたが、名前は重要ではありません。

一般に、Cでは、宣言メカニズムは次のように記述します。

type var;

そして、あなたが書くとき、varそれは与えられたの値を表しますtype。例えば:

int     i;            // i is an int
int    *ip;           // *ip is an int, so ip is a pointer to an integer
int     abs(int val); // abs(-1) is an int, so abs is a (pointer to a)
                      // function returning an int and taking an int argument

標準でtypedefstaticexternはストレージクラスと同様に、文法ではストレージクラスとして扱われます。

typedef void (*SignalHandler)(int signum);

つまり、次のようにSignalHandler呼び出されるタイプの変数(たとえば、alarm_handler)が表示された場合、

(*alarm_handler)(-1);

結果がtype voidあります- 結果はありません。そしてwith引数の(*alarm_handler)(-1);呼び出しです。alarm_handler()-1

したがって、宣言した場合:

extern SignalHandler alt_signal(void);

だということだ:

(*alt_signal)();

void値を表します。したがって:

extern void (*alt_signal(void))(int signum);

同等です。これは、をsignal()返すだけでなく、引数としてSignalHandlerintとaの両方を受け入れるため、より複雑になりSignalHandlerます。

extern void (*signal(int signum, SignalHandler handler))(int signum);

extern void (*signal(int signum, void (*handler)(int signum)))(int signum);

それでも問題が解決しない場合は、どうしたらよいかわかりません。それでも、あるレベルでは不思議ですが、その動作には慣れてきているので、25年間続ければ、そうであれば、それはあなたにとって第二の性質になります(そしてあなたが賢いなら、おそらくもう少し速くなるでしょう)。


3
この説明は以前に見たことがあります。そして、今の場合と同じように、私は2つのステートメント間の接続が得られなかったと思います:extern void(signal(int、void()(int)))(int); / * and * / typedef void(* SignalHandler)(int signum); extern SignalHandler signal(int signum、SignalHandler handler); または、私が聞きたいのは、2つ目のバージョンを作成するために使用できる基本的な概念は何ですか?「SignalHandler」と最初のtypedefを接続する基本は何ですか?ここで説明する必要があるのは、typedefが実際にここで行っていることです。Thx

6
すばらしい回答です。このスレッドに戻ってよかったです。私はすべてを理解しているとは思いませんが、いつか理解します。これが私がSOを好きな理由です。ありがとうございました。
TOTO

2
単にnitを選択するだけです。シグナルハンドラ内でprintf()やその仲間を呼び出すのは安全ではありません。printf()は再入可能ではありません(基本的に、再入可能ではないmalloc()を呼び出すことができるため)
wildplasser

4
extern void (*signal(int, void(*)(int)))(int);意味signal(int, void(*)(int))機能がに関数ポインタを返しますvoid f(int)関数ポインタを戻り値として指定たい場合、構文が複雑になります。戻り値の型を左側に配置し、引数リストを右側に配置する必要がありますが、これは定義する途中です。そして、この場合、signal()関数自体がパラメータとして関数ポインタを取り、さらに複雑になります。良い知らせは、これを読むことができれば、フォースはすでにあなたと一緒にいることです。:)。
smwikipedia 2014

1
&関数名の前で使用することについて古い学校は何ですか?それは完全に不要です。無意味です。そして、間違いなく「古い学校」ではありません。オールドスクールは関数名を単純明快に使用しています。
Jonathan Leffler、2018年

80

関数ポインターは他のポインターと同様ですが、データのアドレス(ヒープまたはスタック上)ではなく関数のアドレスを指します。他のポインターと同様に、正しく入力する必要があります。関数は、戻り値とそれらが受け入れるパラメーターのタイプによって定義されます。そのため、関数を完全に説明するには、その戻り値を含める必要があり、各パラメーターの型は受け入れ可能です。そのような定義をtypedefするときは、その定義を使用してポインタを作成および参照しやすくする「わかりやすい名前」を付けます。

たとえば、次の関数があるとします。

float doMultiplication (float num1, float num2 ) {
    return num1 * num2; }

その後、次のtypedef:

typedef float(*pt2Func)(float, float);

このdoMulitplication関数を指すために使用できます。これは、floatを返し、それぞれがfloat型の2つのパラメーターを取る関数へのポインターを定義しているだけです。この定義にはわかりやすい名前が付いていpt2Funcます。pt2Funcfloatを返し、2つのfloatを取り込む任意の関数を指すことができることに注意してください。

したがって、次のようにdoMultiplication関数を指すポインタを作成できます。

pt2Func *myFnPtr = &doMultiplication;

このポインタを使用して、次のように関数を呼び出すことができます。

float result = (*myFnPtr)(2.0, 5.1);

これは読みやすくなりますhttp : //www.newty.de/fpt/index.html


psychotik、ありがとう!それは役に立ちました。関数ポインタWebページへのリンクは非常に役立ちます。今それを読んでください。

...しかし、そのnewty.deリンクはtypedefsについてまったく話していないようです:(そのリンクは素晴らしいですが、

11
既にポインターであるpt2Func myFnPtr = &doMultiplication;代わりにpt2Func *myFnPtr = &doMultiplication;、実行したい場合がありmyFnPtrます。
タミルセルバン2015

1
pt2Funcの宣言* myFnPtr =&doMultiplication; pt2Funcの代わりにmyFnPtr =&doMultiplication; 警告をスローします。
AlphaGoku

2
@Tamilselvanは正しいです。 myFunPtrはすでに関数ポインターなので、次のように使用しますpt2Func myFnPtr = &doMultiplication;
Dustin Biser 2017

35

関数ポインタのtypedefを理解する非常に簡単な方法:

int add(int a, int b)
{
    return (a+b);
}

typedef int (*add_integer)(int, int); //declaration of function pointer

int main()
{
    add_integer addition = add; //typedef assigns a new variable i.e. "addition" to original function "add"
    int c = addition(11, 11);   //calling function via new variable
    printf("%d",c);
    return 0;
}

32

cdecl関数ポインタ宣言のような奇妙な構文を解読するための優れたツールです。それを使用してそれらを生成することもできます。

複雑な宣言を今後のメンテナンスのために(自分自身または他の人が)簡単に解析できるようにするためのヒントとしてtypedefは、小さなチャンクのsを作成し、それらの小さな断片を大きく複雑な式のビルディングブロックとして使用することをお勧めします。例えば:

typedef int (*FUNC_TYPE_1)(void);
typedef double (*FUNC_TYPE_2)(void);
typedef FUNC_TYPE_1 (*FUNC_TYPE_3)(FUNC_TYPE_2);

のではなく:

typedef int (*(*FUNC_TYPE_3)(double (*)(void)))(void);

cdecl これであなたを助けることができます:

cdecl> explain int (*FUNC_TYPE_1)(void)
declare FUNC_TYPE_1 as pointer to function (void) returning int
cdecl> explain double (*FUNC_TYPE_2)(void)
declare FUNC_TYPE_2 as pointer to function (void) returning double
cdecl> declare FUNC_TYPE_3 as pointer to function (pointer to function (void) returning double) returning pointer to function (void) returning int
int (*(*FUNC_TYPE_3)(double (*)(void )))(void )

そして、(実際には)上記のクレイジーな混乱を正確に生成した方法です。


2
こんにちはカール、非常に洞察に満ちた例と説明でした。また、cdeclの使用を示してくれてありがとう。とても有難い。

Windows用のcdeclはありますか?
ジャック

@ジャック、あなたはそれを構築できると確信しています、はい。
Carl Norum、2014

2
同様の機能をオンラインで提供するcdecl.orgもあります。Windows開発者に便利です。
zaknotzach

12
int add(int a, int b)
{
  return (a+b);
}
int minus(int a, int b)
{
  return (a-b);
}

typedef int (*math_func)(int, int); //declaration of function pointer

int main()
{
  math_func addition = add;  //typedef assigns a new variable i.e. "addition" to original function "add"
  math_func substract = minus; //typedef assigns a new variable i.e. "substract" to original function "minus"

  int c = addition(11, 11);   //calling function via new variable
  printf("%d\n",c);
  c = substract(11, 5);   //calling function via new variable
  printf("%d",c);
  return 0;
}

これの出力は:

22

6

両方の関数の宣言に同じmath_func定義子が使用されていることに注意してください。

typedefの同じアプローチをextern構造体に使用できます(他のファイルでstuructを使用します)。


5

typedefを使用して、より複雑な型、つまり関数ポインターを定義します

Cでステートマシンを定義する例を取り上げます

    typedef  int (*action_handler_t)(void *ctx, void *data);

これで、2つのポインタを取り、intを返すaction_handlerと呼ばれる型を定義しました

ステートマシンを定義する

    typedef struct
    {
      state_t curr_state;   /* Enum for the Current state */
      event_t event;  /* Enum for the event */
      state_t next_state;   /* Enum for the next state */
      action_handler_t event_handler; /* Function-pointer to the action */

     }state_element;

アクションへの関数ポインタは単純な型のように見え、typedefは主にこの目的を果たします。

すべてのイベントハンドラーは、action_handlerで定義されたタイプに準拠する必要があります。

    int handle_event_a(void *fsm_ctx, void *in_msg );

    int handle_event_b(void *fsm_ctx, void *in_msg );

参照:

リンデンによるエキスパートCプログラミング


4

これは、演習として書いた関数ポインターと関数ポインター配列の最も単純な例です。

    typedef double (*pf)(double x);  /*this defines a type pf */

    double f1(double x) { return(x+x);}
    double f2(double x) { return(x*x);}

    pf pa[] = {f1, f2};


    main()
    {
        pf p;

        p = pa[0];
        printf("%f\n", p(3.0));
        p = pa[1];
        printf("%f\n", p(3.0));
    }
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.