はじめに
この記事では、C言語の変数宣言や関数宣言における型の解釈の仕方の基本的な部分を扱います。
とりあえずは、以下のことは扱いません。
-
const
などの修飾子の扱い - キャストにおける型の解釈 (宣言する識別子が無い場合)
宣言に利用される「演算子」
C言語の変数宣言には、以下の「演算子」が使用されます。
-
*
(ポインタを作る) -
[]
(配列を作る) -
()
(関数を作る) -
()
(優先順位を変える)
これらの「演算子」の優先順位は普通の計算に使用されるのと同じで、
優先順位を変えるカッコ→関数、配列→ポインタです。
普通の式
変数宣言の前に、普通の式の解釈を見てみましょう。
例えば、
-
*hoge
は「hoge
をデリファレンス」 -
*hoge[10]
は「hoge
の10番目の要素をデリファレンス」 -
hoge[5][10]
は「hoge
の5番めの要素の10番めの要素」 -
(*hoge)[10]
は「hoge
をデリファレンスしたものの10番目の要素」 -
hoge(1)
は「hoge
が指す関数に引数1を与えて呼び出した戻り値」 -
*hoge(1)
は「hoge
が指す関数に引数1を与えて呼び出した戻り値をデリファレンス」 -
(*hoge)(1)
は「hoge
をデリファレンスしたものが指す関数に引数1を与えて呼び出した戻り値」 -
(*hoge[10])(1)
は「hoge
の10番めの要素をデリファレンスしたものが指す関数に引数1を与えて呼び出した戻り値」 -
(*hoge(1))(2)
は「hoge
が指す関数に引数1を与えて呼び出した戻り値をデリファレンスしたものが指す関数に引数2を与えて呼び出した戻り値」
となります。
普通の式とAST
普通の式は、パーサーなどによりAST(ここでは、演算子とそのオペランドの関係を表す木)に変換することができます。
例えば、(*hoge[10])(1)
は、
() 関数呼び出し
├() 優先順位
│└* デリファレンス
│ └[] 添字
│ ├hoge 識別子
│ └10 数値リテラル
└1 数値リテラル
のように表現することができます。
普通の式の計算では、このASTの葉の方から順に計算されていきます。(この場合、添字→デリファレンス→関数)
すなわち、優先順位が高い演算子ほど先に計算されます。
変数宣言
変数宣言も、同様にASTに変換できます。
そして、宣言の左側に書かれている型を初期値として、ASTの根の方から順に操作を適用していきます。
((*hoge[10])(1)
の場合、関数→デリファレンス(ポインタ)→添字(配列))
すなわち、優先順位が高く、先に計算されるはずの操作ほど後に適用します。
例えば、
int* foo, bar, (*hoge[10])(int);
という変数宣言では、左側に書かれているint
を初期値として(int*
ではない!)それぞれ解釈が行われます。
ここでは、* foo
、bar
、(*hoge[10])(int)
の3個の変数が宣言されています。
* foo
は、「(int
型)のポインタfoo
」です。
bar
は、「(int
型)のbar
」です。
(*hoge[10])(int)
は、「(int
型)を返す関数(引数はint
型のものが1個)を指すポインタを要素とする要素数10の配列hoge
」です。
同様に、
-
*hoge[10]
は「(型)を指すポインタを要素とする要素数10の配列hoge
」 -
hoge[5][10]
は「(型)を要素とする要素数10の配列を要素とする要素数5の配列hoge
」 -
(*hoge)[10]
は「(型)を要素とする要素数10の配列を指すポインタhoge
」 -
hoge()
は「(型)を返す関数hoge
」 -
*hoge()
は「(型)を指すポインタを返す関数hoge
」 -
(*hoge)()
は「(型)を返す関数を指すポインタ」 -
(*hoge(int))(int, int)
は「(型)を返す2引数の関数を指すポインタを返す1引数の関数」
となります。
変数宣言を書く
例えば、「int
型を要素とする10要素の配列へのポインタを返す関数へのポインタsample
」を宣言してみます。
優先順位を変える「余計」なカッコを使ってもいいので、落ち着いて書いていきましょう。
ASTの葉から書く
ASTの葉は、宣言したいものを言葉で表現した時の後ろの方になります。
ASTの葉から書くと書いたもののオペランドは全て決まっているので、式の中に書かれていない部分が生じません。
前に書いたものを念のため優先順位を変えるカッコで囲み、次の「演算子」を付け足します。
- 「
sample
」
sample
- 「ポインタ
sample
」
*(sample)
- 「関数へのポインタ
sample
」
(*(sample))()
- 「ポインタを返す関数へのポインタ
sample
」
*((*(sample))())
- 「10要素の配列へのポインタを返す関数へのポインタ
sample
」
(*((*(sample))()))[10]
- 「
int
型を要素とする10要素の配列へのポインタを返す関数へのポインタsample
」
int ((*((*(sample))()))[10]);
- 「余計」なカッコを外すと
int (*(*sample)())[10];
ASTの根から書く
ASTの根は、宣言したいものを言葉で表現した時の前の方になります。
ASTの根から書くと書いた時点でオペランドが決まっていないので、式の中に書かれていない部分が生じます。
ここでは、「書かれていない部分」を@
で表現します。
この「書かれていない部分」を次の「演算子」とその(まだ書かれていない)オペランドに置き換えていきます。
- 「
int
型」
int (@);
- 「
int
型を要素とする10要素の配列」
int ((@)[10]);
- 「
int
型を要素とする10要素の配列へのポインタ」
int ((*(@))[10]);
- 「
int
型を要素とする10要素の配列へのポインタを返す関数」
int ((*((@)()))[10]);
- 「
int
型を要素とする10要素の配列へのポインタを返す関数へのポインタ」
int ((*((*(@))()))[10]);
- 「
int
型を要素とする10要素の配列へのポインタを返す関数へのポインタsample
」
int ((*((*(sample))()))[10]);
- 「余計」なカッコを外すと
int (*(*sample)())[10];
メモ
- 関数の定義も関数の宣言と同様に書けます。
- 配列や関数を返す関数を宣言または定義することはできません。
- 関数の引数の宣言も変数宣言と同様に書けます。
ただし、「配列」を宣言するとその配列の要素を指すポインタに、「関数」を宣言するとその型の関数を指すポインタに変換されます。
例えば、引数のint hoge[5][10]
は「int
型を要素とする要素数10の配列を指すポインタ」となります。 - 式の評価において、配列はその配列の先頭要素を指すポインタに、関数はその関数を指すポインタに変換されます。 (一部の演算子のオペランドとして渡される場合は変換されません)
リンク
C言語の型宣言の解説 - Qiita
この記事では演算子の優先順位やASTと関連付けて説明していますが、
リンク先の記事では独自の規則や英語と関連付けて説明しています。