はじめに
関数ポインタを勉強していたら、typedef
でハマってしまったというお話。間違ってたら、優しく教えてほしいな…(小声)
まずは基本から
typedef
といえば、こういう風に書くのが基本だと思います。
// 符号なし整数型を Typedef した場合
typedef unsigned int UINT;
UINT a = 10;
typedef
したことで、符号なし整数型(unsigned int
)と同じ意味を持つ新しい型名 UINT
が出来ました。1
ここから、下記のように公式を導くことができます(私の発見ではなくて、よく見かけるものだけど)。
typedef 既存の型名 新規の型名
簡単ですね!
ポインタ
typedef
といえば構造体でしょうが、ここではポインタを typedef
しておきたいと思います。ちなみに、下記は K&R にも載っている例です。2
先程の公式に従うと、このようになるでしょう。
typedef 既存の型名 新規の型名
-->
既存の型名: char *
新規の型名: STRING-->
typedef char* STRING
では、実装してみます。
// char 型へのポインタ(文字列)を typedef した場合
typedef char* STRING;
STRING str = "ABCDEFG";
見事、文字列型が出来ました!
関数ポインタ
ところで、関数もメモリに配置されているため、ポインタで指すことができます。あまり面白くないコードですが、こんな感じです。
#include <stdio.h>
// 関数ポインタを通して呼び出される関数
void func(int a);
int main() {
// 関数ポインタを宣言する
void (*funcPtr)();
// 関数ポインタに関数名(関数のアドレス)を代入する
funcPtr = func;
// 関数ポインタを間接参照し、関数を呼び出す
(*funcPtr)(1);
return 0;
}
void func(int a) {
printf("Input value is %d\n", a);
}
関数ポインタで一番わかりにくいのは、下記の部分だと思います。
void (*funcPtr)();
これは「『void
型を戻り値とする関数』へのポインタ」を宣言しています。
void *funcPtr();
こんな風に書きたくもなりますが、こう書くと、「void
型へのポインタを戻り値とする関数」を宣言したことになってしまいます。それを防ぐために、()
を付けることになったそうな…3
ちなみに、「配列名」が「配列の先頭要素のアドレス」を指しているように、「関数名」も「関数のアドレス」を指しています。なので、ポインタにアドレスを代入する際も &func
とする必要はありません。
funcPtr = func;
関数ポインタをtypedefしたい!
さて、ポインタを typedef
できたなら、関数ポインタもできるはずです。実際、関数ポインタは長くなりがちなので、typedef
することも多いらしいです。
まずは公式に当てはめて考えてみます。
typedef 既存の型名 新規の型名
-->
既存の型名: void (*)()
新規の型名: FUNCTYPE-->
typedef void (*)() FUNCTYPE
うん、なんか不格好だけど、やってみよう!
#include <stdio.h>
void func(int a);
int main() {
typedef void (*)() FUNCTYPE;
FUNCTYPE funcPtr = func;
funcPtr(1);
return 0;
}
void func(int a) {
printf("Input value is %d\n", a);
}
あれっ、エラーが出てコンパイルできないぞ…
正しい書き方はこれ
いろいろと調べると、以下の書き方が正しいみたい。
typedef void (*FUNCTYPE)();
公式への疑惑
うーん、たしかに動いたけど、これって、公式と違わない?
typedef 既存の型名 新規の型名
そう思って調べてると、こんな 文章 を見つけました。
私も typedef の話をする時には、「まず変数宣言をするふりをして、頭に typedef を付けると型になる」と説明しています。
えっ、そういうこと?
typedefと変数宣言
変数宣言を念頭に考えると、typedef
は一気にわかりやすくなります。例えば、符号なし整数型の場合。
// 0.変数を宣言する
unsigned int a;
// 1.typedef を先頭につける
typedef unsigned int a;
// 2.変数名を新しい型名にする => 完成!
typedef unsigned int UINT;
文字列型を作る場合も同じ。
// 0.変数を宣言する
char* str;
// 1.typedef を先頭につける
typedef char* str;
// 2.変数名を新しい型名にする => 完成!
typedef char* STRING;
そして、関数ポインタの場合。
// 0.変数を宣言する
void (*funcPtr)();
// 1.typedef を先頭につける
typedef void (*funcPtr)();
// 2.変数名を新しい型名にする => 完成!
typedef void (*FUNCTYPE)();
実は配列も typedef
できるのですが、これも最初に考えた公式では到底導き出せません。
// 0.変数を宣言する
int numbers[5];
// 1.typedef を先頭につける
typedef int numbers[5];
// 2.変数名を新しい型名にする => 完成!
typedef int ARRAY[5];
// 実際の使い方
ARRAY hoge = {1, 2, 3, 4, 5};
規格ではどうなってるの?
コンパイラがこういう風に解釈しているのだから、規格にも記述があるはず。ということで、K&R を探してみました。以下の場所がそれっぽい?4
宣言子中の各名前には、一つの
typedef
宣言により、通常のやり方で型が与えられる(8.6節)。その後では、そうしたtypedef
名は、その型に対応する型指定子キーワードと文法的には同じになる。
ここでいう「8.6節」には、ポインタや配列、関数などの宣言方法が書かれています。それと同じようにして型が与えられるということは、変数宣言と同じようにして typedef
もできるということでしょうか?
まとめ
よく聞く公式?
typedef 既存の型名 新規の型名
では汎用性が低いので、下記の手順を踏むようにしましょう。
0.変数を宣言する
1.typedef を先頭につける
2.変数名を新しい型名にする