引数を持つ関数へのポインタのtypedefを持つ他の人々のコードを読んだとき、私はいつも少し困惑してきました。少し前にCで記述された数値アルゴリズムを理解しようとすると、このような定義に取り掛かるのにしばらく時間がかかったことを思い出します。では、関数へのポインタの良いtypedefを作成する方法(することとしないこと)に関するヒントと考えを共有して、なぜそれらが役立つのか、他の人の作業をどのように理解するのかについて教えてください。ありがとう!
引数を持つ関数へのポインタのtypedefを持つ他の人々のコードを読んだとき、私はいつも少し困惑してきました。少し前にCで記述された数値アルゴリズムを理解しようとすると、このような定義に取り掛かるのにしばらく時間がかかったことを思い出します。では、関数へのポインタの良いtypedefを作成する方法(することとしないこと)に関するヒントと考えを共有して、なぜそれらが役立つのか、他の人の作業をどのように理解するのかについて教えてください。ありがとう!
回答:
signal()
C標準の関数を考えます。
extern void (*signal(int, void(*)(int)))(int);
完全に曖昧に明白-引数として整数を取り、何も返さない関数へのポインターと整数の2つの引数を取る関数であり、それは(signal()
)を引数として整数を取り、返す関数へのポインターを返します何も。
あなたが書く場合:
typedef void (*SignalHandler)(int signum);
次に、代わりに次のように宣言できますsignal()
。
extern SignalHandler signal(int signum, SignalHandler handler);
これは同じことを意味しますが、通常は多少読みやすいと見なされています。関数がint
andおよび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つの信号について心配することにしました。私はしばしばSIGHUP
、SIGPIPE
また、SIGTERM
それらが定義されているかどうか(#ifdef
条件付きコンパイル)について心配しますが、それだけで状況が複雑になります。私はおそらくのsigaction()
代わりにPOSIXも使用するでしょうsignal()
が、それは別の問題です。私たちが始めたものに固執しましょう。
このmain()
関数は、インストールされるハンドラーのリストを反復処理します。各ハンドラーについて、最初にを呼び出しsignal()
て、プロセスが現在シグナルを無視しているかどうかを確認し、その間SIG_IGN
、ハンドラーとしてインストールします。これにより、シグナルが無視されたままになります。シグナルが以前に無視されていなかった場合は、signal()
再度呼び出して、今度は優先シグナルハンドラーをインストールします。(他の値が考えられる SIG_DFL
。、信号のデフォルトのシグナルハンドラ)への最初の呼び出しので「(信号)」にハンドラを設定するSIG_IGN
とsignal()
、以前のエラーハンドラを返し、値old
の後if
のステートメントでなければなりませんSIG_IGN
-それゆえ主張。(まあ、それは可能性がありますSIG_ERR
何かが劇的にうまくいかなかった場合-しかし、私はアサートの発砲からそれについて学びます。)
その後、プログラムはその処理を行い、正常に終了します。
関数の名前は、適切なタイプの関数へのポインタと見なすことができることに注意してください。たとえば、初期化子のように、関数呼び出しの括弧を適用しない場合、関数名は関数ポインターになります。これが、pointertofunction(arg1, arg2)
表記法を介して関数を呼び出すことが妥当である理由でもあります。を見ると、それは関数へのポインターであり、関数ポインターを介した関数の呼び出しであるalarm_handler(1)
と見なすことalarm_handler
ができalarm_handler(1)
ます。
これまでのところ、SignalHandler
2つのシグナルハンドラー関数が提供する適切なタイプの値が割り当てられている限り、変数は比較的簡単に使用できることを示しました。
さて、質問に戻ります-の2つの宣言はどのようsignal()
に相互に関連していますか。
2番目の宣言を見てみましょう。
extern SignalHandler signal(int signum, SignalHandler handler);
関数名と型を次のように変更した場合:
extern double function(int num1, double num2);
これをint
とa double
を引数として取り、double
値を返す関数として解釈しても問題はありません(問題がある場合は、気にしない方がいいでしょう。ただし、質問をするのは慎重にすべきです)これが問題である場合)
これでdouble
、signal()
関数はの代わりに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
標準でtypedef
はstatic
、extern
はストレージクラスと同様に、文法ではストレージクラスとして扱われます。
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()
返すだけでなく、引数としてSignalHandler
intとaの両方を受け入れるため、より複雑になりSignalHandler
ます。
extern void (*signal(int signum, SignalHandler handler))(int signum);
extern void (*signal(int signum, void (*handler)(int signum)))(int signum);
それでも問題が解決しない場合は、どうしたらよいかわかりません。それでも、あるレベルでは不思議ですが、その動作には慣れてきているので、25年間続ければ、そうであれば、それはあなたにとって第二の性質になります(そしてあなたが賢いなら、おそらくもう少し速くなるでしょう)。
extern void (*signal(int, void(*)(int)))(int);
意味signal(int, void(*)(int))
機能がに関数ポインタを返しますvoid f(int)
。関数ポインタを戻り値として指定したい場合、構文が複雑になります。戻り値の型を左側に配置し、引数リストを右側に配置する必要がありますが、これは定義する途中です。そして、この場合、signal()
関数自体がパラメータとして関数ポインタを取り、さらに複雑になります。良い知らせは、これを読むことができれば、フォースはすでにあなたと一緒にいることです。:)。
&
関数名の前で使用することについて古い学校は何ですか?それは完全に不要です。無意味です。そして、間違いなく「古い学校」ではありません。オールドスクールは関数名を単純明快に使用しています。
関数ポインターは他のポインターと同様ですが、データのアドレス(ヒープまたはスタック上)ではなく関数のアドレスを指します。他のポインターと同様に、正しく入力する必要があります。関数は、戻り値とそれらが受け入れるパラメーターのタイプによって定義されます。そのため、関数を完全に説明するには、その戻り値を含める必要があり、各パラメーターの型は受け入れ可能です。そのような定義をtypedefするときは、その定義を使用してポインタを作成および参照しやすくする「わかりやすい名前」を付けます。
たとえば、次の関数があるとします。
float doMultiplication (float num1, float num2 ) {
return num1 * num2; }
その後、次のtypedef:
typedef float(*pt2Func)(float, float);
このdoMulitplication
関数を指すために使用できます。これは、floatを返し、それぞれがfloat型の2つのパラメーターを取る関数へのポインターを定義しているだけです。この定義にはわかりやすい名前が付いていpt2Func
ます。pt2Func
floatを返し、2つのfloatを取り込む任意の関数を指すことができることに注意してください。
したがって、次のようにdoMultiplication関数を指すポインタを作成できます。
pt2Func *myFnPtr = &doMultiplication;
このポインタを使用して、次のように関数を呼び出すことができます。
float result = (*myFnPtr)(2.0, 5.1);
pt2Func myFnPtr = &doMultiplication;
代わりにpt2Func *myFnPtr = &doMultiplication;
、実行したい場合がありmyFnPtr
ます。
myFunPtr
はすでに関数ポインターなので、次のように使用しますpt2Func myFnPtr = &doMultiplication;
関数ポインタの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;
}
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 )
そして、(実際には)上記のクレイジーな混乱を正確に生成した方法です。
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を使用します)。
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プログラミング