C

Typedefの考え方

More than 1 year has passed since last update.


はじめに

関数ポインタを勉強していたら、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.変数名を新しい型名にする






  1. 新しい が出来たわけではなく、型名 が出来ただけであることに注意。 



  2. こんな例を載せるぐらいなら文字列型を用意しておいてくれよ、と思わなくもないが(笑) 



  3. 演算子の優先順位が原因なのですが、あれを覚える気力が湧かない… 



  4. 『プログラミング言語C』第2版、p.276