233
159

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Typedefの考え方

Last updated at Posted at 2016-12-27

はじめに

関数ポインタを勉強していたら、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

233
159
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
233
159

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?