web-dev-qa-db-ja.com

Cの関数ポインタのtypedefを理解する

引数を持つ関数へのポインタのためのtypedefを持つ他の人々のコードを読んだとき、私はいつも少しつまずいていました。少し前にCで書かれた数値アルゴリズムを理解しようとしながら、そのような定義にたどり着くには少し時間がかかりました。それで、関数へのポインタに適切なtypedefを書く方法(Do'sとDo not's)についてのあなたの助言と考えを共有できますか。ありがとうございます。

211
user192712

C標準のsignal()関数を考えてみましょう。

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

完全にわかりにくい-整数と、整数を引数として受け取り、何も返さない関数へのポインタの2つの引数を取る関数であり、(signal())は整数を取る関数へのポインタを返します引数として、何も返しません。

あなたが書く場合:

typedef void (*SignalHandler)(int signum);

次に、代わりにsignal()を宣言できます:

extern  SignalHandler signal(int signum, SignalHandler handler);

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

ただし、慣れるには少し時間がかかります。できないことの1つは、関数定義でSignalHandlertypedefname__を使用してシグナルハンドラー関数を作成することです。

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

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

最新の構文は次のものを使用します。

functionpointer(arg1, arg2, ...);

私はそれがなぜ機能するのかを見ることができます-functionpointername__という関数ではなく、変数が初期化される場所を探す必要があることを知りたいだけです。


サムはコメントした:

以前にこの説明を見ました。そして、今の場合のように、私が得なかったのは、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標準からまっすぐに持ち上げられています-私はそれを再入力し、括弧が正しいことを確認しました(修正するまで-それは覚えるのが難しいクッキーです)。

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

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

「何も返さない」部分のスペルはvoidname__です。整数である引数は自明です(私は信頼しています)。次の表記法は、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()関数では、値は常にSIGALRMname__であり、それがハンドラーである唯一の信号ですが、signal_handler()は同じ関数が使用されるため、信号番号としてSIGINTname__またはSIGQUITname__を取得する場合があります両方。

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

main()関数は、インストールされるハンドラーのリストを反復処理します。ハンドラーごとに、最初にsignal()を呼び出して、プロセスが現在シグナルを無視しているかどうかを確認し、そうしている間にシグナルとしてSIG_IGNをハンドラーとしてインストールします。シグナルが以前に無視されていなかった場合、signal()を再度呼び出し、今回は優先シグナルハンドラをインストールします。 (他の値はおそらくSIG_DFL、シグナルのデフォルトのシグナルハンドラです。) 'signal()'の最初の呼び出しはハンドラをSIG_IGNに設定し、signal()は前のエラーハンドラを返します。 、oldname__ステートメントの後のifname__の値はSIG_IGNでなければなりません-したがって、アサーションです。 (まあ、何かが劇的にうまくいかなかったら、それはSIG_ERRかもしれません-しかし、私はアサートの発火からそれについて学びます。)

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

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

したがって、これまでのところ、SignalHandlername__変数は、適切なタイプの値を割り当てる必要がある場合に使用するのが比較的簡単であることを示しました-これは2つのシグナルハンドラー関数が提供するものです。

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

2番目の宣言を確認しましょう。

 extern SignalHandler signal(int signum, SignalHandler handler);

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

 extern double function(int num1, double num2);

これをintname__とdoublename__を引数として取り、doublename__の値を返す関数として解釈しても問題はありません(問題がある場合は気をつけた方がいいかもしれませんが、質問する場合は注意が必要です)それが問題である場合、これと同じくらい難しい質問)。

signal()関数は、doublename__の代わりに、SignalHandlername__を2番目の引数として取り、結果として1つを返します。

それを次のように扱うことのできる仕組み:

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

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

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

type var;

その後、varname__を記述すると、指定されたtypename__の値を表します。例えば:

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

標準では、typedefname__とstaticname__がストレージクラスであるように、externname__は文法ではストレージクラスとして扱われます。

typedef void (*SignalHandler)(int signum);

は、SignalHandlername__型の変数(alarm_handlerなど)が次のように呼び出されていることを意味します。

(*alarm_handler)(-1);

結果にはtype voidがあります-結果はありません。 (*alarm_handler)(-1);は、-1を引数とするalarm_handler()の呼び出しです。

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

extern SignalHandler alt_signal(void);

だということだ:

(*alt_signal)();

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

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

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

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

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

それでもそれがあなたを混乱させる場合、私はどのように助けるかわかりません-それはまだ私には神秘的ないくつかのレベルですが、私はそれがどのように機能するかに慣れてきたので、あなたがさらに25年間それを続けるならあなたに伝えることができますまたは、それはあなたにとって二番目の性質になります(そして賢い人ならもう少し早くなるかもしれません).

278

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

だから例えばあなたが関数を持っていると仮定します:

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

それから次のtypedef:

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

このdoMulitplication関数を指すために使用できます。これは単にfloatを返す関数へのポインタを定義するだけで、それぞれfloat型の2つのパラメータを取ります。この定義の名前はpt2Funcです。 pt2Funcは、float値を返し、2つのfloat値を取るANY関数を指すことができます。

そのため、doMultiplication関数を指すポインタを次のように作成できます。

pt2Func *myFnPtr = &doMultiplication;

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

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

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

71
psychotik

関数ポインタの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;
}
30
user2786027

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

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

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 )

そして(実際には)まさしく私が上でその狂った混乱を起こした方法です。

25
Carl Norum
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を使用)。

12

より複雑な型、すなわち関数ポインタを定義するために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 );

参考文献:

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

3
vaaz

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

    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));
    }
3
Bing Bang