技術日誌

DB,Java,セキュリティ,機械学習など。興味のあることを雑多に学ぶ

C言語の復習その5。関数ポインタについて

関数ポインタは「宣言方法」と「引数、返値があっていない適当に実装されたコードを読む」のがややこしいが、綺麗に書けばそこまで難解ではない。という認識です。

関数ポインタについて

  • 宣言 void(*p)()
    • 引数付き void(*p)(char *str)
  • *pを囲ってる()が若干気持ち悪い
    • これがないと演算子の優先度の都合上、ポインタ型の変数を返す関数になってしまう。
      • int *p()これはダメ。
    • ()で囲うことで演算子の優先順位を調整している
    • あくまで返値ではなく、関数自体のポインタですよ。という理解をするとわかりやすい

引数や戻り値は暗黙の型変換が絡んで来るとややこしい

この記事を書いた当初、gccコンパイルオプションを-W -Wallと書くべきところを-w -Wallと書いていまい、 警告を抑制するという馬鹿なことをやってしまいました。

※警告を最大限に出すようにオプション(-W -Wall)を与えて*1、悪いプログラミングの癖は強制するべきだと思っています。

    //引数なし、返値なしの関数の場合
    void (*fp)(void) = func1;//この宣言代入がもっともふさわしい
    fp();
    fp(2);//エラー void型の関数の引数にintを渡したため

    //関数ポインタの型を不適切なものにする
    int(*fp2)(int a) = func1;//警告がでる

    //関数ポインタの宣言時の引数の個数にふさわしく無い呼び出しをすると、
    //fp2();//コンパイルエラー
    // error: too few arguments to function call, expected 1,
    //   have 0
    // fp2();

    //関数ポインタの宣言時の引数にふさわしい引数を与えているが、代入した関数には引数はない
    //void func1();

    //一見不正なことをしているように見えるが、そもそも引数の指定が無い関数には引数を与えて呼び出すこともできる
    func1(1);//ただし警告は出る
    // other/typeConvert.c:44:12: warning: too many arguments in call to 'func1'
    // func1(1);
    // ~~~~~  ^

    //この挙動が気に入らないなら、関数ポインタの引数としてvoidを定義すれば良い。

    //ちなみに関数ポインタの宣言で引数が決められている場合は、代入する関数の引数がvoidであっても、
    //引数を与えて呼び出すことができてしまう。
    //void voidf(void)
    void (*fp3)(int a) = voidf;//ただしこの代入は警告が出る
    // warning: incompatible pointer types initializing
    //   'void (*)(int)' with an expression of type 'void (void)'
    //   [-Wincompatible-pointer-types]
    // void (*fp3)(int a) = voidf;
    //        ^             ~~~~~

    fp3(1);

    //防ぐには代入する関数の引数をvoidにして、関数ポインタの引数もvoidに、そしてコンパイラの警告オプションを最大にすることが重要。
・・・

//代入されている関数達
void func1(){
  puts("引数なしのfunc1が呼ばれました");
}
void func2(char *str){
  puts(str);
}
int intfunc(){
  return 3;
}
int intfunc2(int i){
  int result = i+3;
  printf("%d\n",result);
  return result;
}
void voidf(void){
  puts("引数を渡すとエラーになる関数が呼ばれました");
}
  • 暗黙の型変換を期待したコードでvoidが絡んでいると失敗する
  • 大体のルールはここに記載されている。

  • 関数ポインタと、ポインタ変数に代入する関数の

    • 引数
    • 戻り値
  • をきちんと一致するように宣言・代入し、コンパイルオプションで警告を細かく出せばこの問題に頭を悩ますことはないと思います。*2

    関数ポインタの配列

  • 宣言がちょっとわかりにくいけど、基本は同じなので特に解説なし。

//宣言
int(*p3[4])(char *output);

//代入
p3[0] = func1;
p3[1] = func2;
p3[2] = intfunc;
p3[3] = intfunc2;

//誤った使用
//以下のコンパイルはすべて失敗する
// p3[0]();
// p3[1]();
// p3[2]();
// p3[3]();

//コンパイル結果
// chap5/functionp.c:65:10: error: too few arguments to function call, expected 1,
//       have 0
//    p3[0]();
//    ~~~~~ ^
// chap5/functionp.c:66:10: error: too few arguments to function call, expected 1,
//       have 0
//    p3[1]();
//    ~~~~~ ^
// chap5/functionp.c:67:10: error: too few arguments to function call, expected 1,
//       have 0
//    p3[2]();
//    ~~~~~ ^
// chap5/functionp.c:68:10: error: too few arguments to function call, expected 1,
//       have 0
//    p3[3]();
//    ~~~~~ ^


//使用
p3[0]("123");//出力結果::引数なしのfunc1が呼ばれました
p3[1]("123");//出力結果:123
p3[2]("123");
p3[3]("123");//出力結果:152838021 多分不正に数値にキャストした文字列に3を足した結果。

・・・
//代入されている関数達
void func1(){
  puts("引数なしのfunc1が呼ばれました");
}
void func2(char *str){
  puts(str);
}
int intfunc(){
  return 3;
}
int intfunc2(int i){
  int result = i+3;
  printf("%d\n",result);
  return result;

}

補足、某所でいただいたアドバイスについて

引数なし、ではなくvoid型の引数というものがある。

DCL20-C. 引数を受け付けない関数の場合も必ず void を指定する

こちらを関数ポインタの型変数に宣言すれば、 引数をつけて読んだときにエラーになります。

   //void引数と関数ポインタの実験
   
   int (*fp1)(int a);
   //int voidf(void);
   fp1 = voidf;//代入可能
   //fp1();//エラー
   fp1(1);//実行可能
   //voidf(1);//コンパイルエラー

   int (*fp2)(void);
   fp2 = voidf;
   fp2();
   //fp2(10);//コンパイルエラー

暗黙の型変換は、コンパイラ依存のものも結構あり、gccで動くからといって油断はしないこと

INT36-C. ポインタから整数への変換、整数からポインタへの変換

プログラミング学習シリーズ C言語改訂版 2 はじめて学ぶCの仕組み

プログラミング学習シリーズ C言語改訂版 2 はじめて学ぶCの仕組み

*1:JSなら全てstrictモードにするなど

*2:パズルチックなコードや他人が書いたコードで型変換のルールを調べ直す日が来るかもしれません。