C言語の関数ポインタのtypedefを理解する 質問する

C言語の関数ポインタのtypedefを理解する 質問する

引数付きの関数へのポインターの typedef がある他の人のコードを読むと、いつも少し困惑してしまいます。以前、C で書かれた数値アルゴリズムを理解しようとしていたときに、そのような定義にたどり着くまでに時間がかかったことを思い出します。関数へのポインターの適切な typedef の書き方 (すべきこととすべきでないこと)、それがなぜ便利なのか、他の人の作業を理解する方法について、ヒントや考えを共有していただけませんか? よろしくお願いします!

ベストアンサー1

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

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

まったくわかりにくいですが、これは 2 つの引数、つまり整数と、引数として整数を取り何も返さない関数へのポインターを取る関数であり、引数signal()として整数を取り何も返さない関数へのポインターを返します ( )。

次のように書くと:

typedef void (*SignalHandler)(int signum);

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

extern  SignalHandler signal(int signum, SignalHandler handler);

これは同じことを意味しますが、通常は読みやすいとみなされます。関数が と を受け取りintSignalHandlerを返すことがより明確になります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 標準からそのまま引用したものです。再入力し、括弧が正しいかどうかを確認しました (修正するまでは確認しませんでした。覚えるのが難しいものです)。

まず、 は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()は常に になりますが、同じ関数が両方に使用されるため、シグナル番号としてまたは が取得される可能性があります。SIGALRMsignal_handler()SIGINTSIGQUIT

次に、各要素がシグナル番号とそのシグナル用にインストールされるハンドラーを識別する構造体の配列を作成します。私は 3 つのシグナルについて考慮することにしました。私は 、 、SIGHUPおよびSIGPIPESIGTERM定義されているかどうか (条件付きコンパイル) についてよく考慮しますが、それは物事を複雑にするだけです。の代わりに#ifdefPOSIX を使用する可能性もありますが、それは別の問題です。最初に使用した方法に固執しましょう。sigaction()signal()

このmain()関数は、インストールするハンドラのリストを反復処理します。各ハンドラについて、最初に を呼び出して、signal()プロセスが現在シグナルを無視しているかどうかを調べ、その際に をSIG_IGNハンドラとしてインストールします。これにより、シグナルが無視されたままになります。シグナルが以前に無視されていなかった場合は、再度 を呼び出してsignal()、今度は優先シグナル ハンドラをインストールします。(もう 1 つの値は SIG_DFL、シグナルのデフォルトのシグナル ハンドラである であると考えられます。) 'signal()' の最初の呼び出しでハンドラが に設定されSIG_IGN、以前のエラー ハンドラが返されるため、ステートメントの後のsignal()の値はである必要があります。これがアサーションです。(何か劇的に間違ったことがあった場合はそうなるかもしれませんが、その場合はアサーションの発動からそれを知ることができます。)oldifSIG_IGNSIG_ERR

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

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

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

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

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

 extern SignalHandler signal(int signum, SignalHandler handler);

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

 extern double function(int num1, double num2);

intこれを、引数としてと を受け取りdouble、値を返す関数として解釈しても問題ありませんdouble(そうでしょうか? それが問題になる場合は、白状しない方がよいでしょう。ただし、それが問題になる場合は、これほど難しい質問をするのは慎重にしたほうがよいでしょう)。

doubleここで、関数はではなく を2 番目の引数としてsignal()受け取りSignalHandler、その結果として 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

標準では、は、 や がストレージ クラスであるのとtypedef同様に、文法上のストレージ クラスとして扱われます。staticextern

typedef void (*SignalHandler)(int signum);

つまり、次のように呼び出される型の変数SignalHandler(たとえば、alarm_handler) を見ると、

(*alarm_handler)(-1);

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

したがって、次のように宣言するとします。

extern SignalHandler alt_signal(void);

だということだ:

(*alt_signal)();

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

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

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

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

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

それでもまだ混乱してしまうなら、どう助けてあげたらよいかわかりません。私にとってはまだある意味では謎ですが、仕組みには慣れてきましたので、あと 25 年ほど使い続ければ、自然に身につくようになるでしょう (賢い人なら、もっと早く身につくかもしれません)。

おすすめ記事