🎉

C言語のポインタの解読

2024/05/11に公開

目的

この記事では、関数ポインタなどが絡んだ複雑な変数の宣言を解読するための知識をまとめています。最終的には、読者は以下の宣言の意味を理解できるようになると考えています。

int **(*(*a[10])(int))(int, int), b;

まずは基本的な宣言から

さて、複雑な型を理解するには基本が大事です。ところで、以下の変数aは何でしょうか?

int a;

Cを少しでも経験したことのある方ならばこれは簡単でしょう。もちろんこれはint型の変数です。
それでは、次の二つの変数abは何でしょうか?

int *a;
int b[10];

おそらくこれも簡単でしょう。aint *型の変数で、bは長さが10のint型の配列です。
次にこれら二つをまとめた以下の変数を考えます。

int *a[10];

この変数は何を表しているのでしょうか?これは少し複雑ですが、上で得た知識を活用するとこれは長さが10でその型がint *である配列であると理解できるでしょう。

ポインタを宣言する際の仕様

ところで、C言語では以下のように複数の変数を一行で宣言することができます。

int i, j, k;

ここで、以下の変数の宣言を考えます。

int *a, b;

この二つの変数abの型は何でしょうか?自然に考えるとどちらも型がint *であると考えてしまうかもしれませんが、実はbの型はintとなってしまいます。これはなぜかというとC言語においてポインタの部分*は型名(ここではint)ではなく変数(ここではa)にくっついているからです。つまり、上の宣言は

元となる型はintであり、変数aはその型へのポインタ、bはその型そのもの

と読む必要があるのです。つまり、上の宣言は以下のように書き換えることができます。

int *a;
int b;

配列へのポインタと関数ポインタ

閑話休題、以下の変数が存在するとき、この変数へのポインタはどのように宣言することができるのでしょうか?

int *b[10];

結論からの述べると、以下の変数はbの参照を受け取ることができます。

int *(*a)[10] = &b;

この意味を説明する前に、もう一つの類似した形式の変数の宣言を見てみましょう。

int *(*a)(int, int);

これは引数がintintで、返り値の型がint *である関数へのポインタを表しています。
そこで、この二つをよく見てみると、どちらも何かしらの型へのポインタを表しているとわかります。このことから、変数名とポインタの周りに丸かっこがついている場合はその変数がそれの周りの型へのポインタを表していることがわかります。つまり、文法的には正しくないですが、上の宣言たちは以下のように書き換えることができます。

int *[10] *a;
int *(int, int) *a;

幾分かは見やすくなったのではないのでしょうか?

そして複雑な宣言へ

さて、この記事の終わりとして冒頭で見せた宣言を解読してみましょう。

int **(*(*a[10])(int))(int, int), b;

初めに、この宣言を2つの宣言に分解します。

int **(*(*a[10])(int))(int, int);
int b;

変数bについては自明ですので、変数aについて見ていきましょう。これは以下のように書き直せます。

int **(int, int) *(*a[10])(int);

さらに、これは以下のように書き直せます。

(int **(int, int)) *(int) *a[10];

よって、この変数の型は、長さが10で型が整数を受け取って、整数を二個受け取りintへのポインタのポインタを返す関数を返す関数へのポインタである配列であるとわかります。
実際、以下のコードは型的に有効です。

int **(*(*a[10])(int))(int, int), b;
int **c = a[0](b)(b, b);

おわりに

いかがでしたでしょうか。Cのプログラムを書く上でこのような知識が必要になることはないと考えていますが、コンパイラを書く上ではこのような知識が必要になってきて、さらに自分の探した範囲ではこのような記事が見当たらなかったため今回この記事を作成しました。もしこの記事がコンパイラを自作したいという人に対しての助けとなれば幸いです。

Discussion