C言語の型の読み方
C言語の型の読み方の解説は探すと色々出てくるが、改めて自分で説明してみることにした。
用語説明
C言語のポインタ記法に代わる記法として、ここでは次のような記法を導入する。もちろんこれはC言語にはない。
- 型
T
へのポインタをptr<T>
と書く。 - 型
T
がn
個並んだ配列をarr<T, n>
と書く。- 可変長配列を
arr<T, *>
と書く。 - サイズ指定のない配列を
arr<T,>
と書く。
- 可変長配列を
- 関数を
fun<int(int, int)>
のように書く。- ノンプロトタイプ関数を
fun<int(*)>
のように書く。
- ノンプロトタイプ関数を
変数名について
C言語の型は、構文上は変数名とセットで扱われる。対応する変数がない場所では、単に変数名部分が省略されたものとして扱われる。
前処理1: K&R関数定義の場合
関数定義 ({}
が後続する) で以下の条件に当てはまるときは、次のような処理をする。
- 引数部分が
()
の場合、ここにvoid
を入れる。 - 引数部分が
(x, y)
のように変数名のみの場合、その直後にint x, y;
のような宣言があるのでこれを処理する必要がある。(詳しい説明は省略)
ステップ1: 宣言指定子と宣言初期化子に分離する
以下に挙げるものが宣言指定子 (declaration specifier)である。
- 分類A:
typedef
extern
static
_Thread_local
/thread_local
auto
register
inline
_Noreturn
/noreturn
_Alignas
/alignas
- 分類B:
const
restrict
volatile
_Atomic
/atomic
- 分類C:
void
char
short
int
long
float
double
signed
unsigned
_Bool
/bool
_Complex
/complex
struct
union
enum
_Atomic()
/atomic()
, typedef名 (int32_t
など)
これらを左から順に取り除いていって、残った部分 (変数名、ポインタ、配列、関数、初期化など) が宣言初期化子 (declarator-initializer)である。宣言初期化子はカンマ区切りで複数あるかもしれない。
例えば、
long const unsigned static long * volatile x[30] = {0}, (*y)[20];
の場合、以下のように分けられる。
- 分類Aの宣言指定子:
static
のみ - 分類Bの宣言指定子:
const
のみ - 分類Cの宣言指定子:
unsigned
,long
,long
- 宣言初期化子その1 :
* volatile x[30] = {0}
- 宣言初期化子その2 :
(*y)[20]
宣言を分割する
1つの宣言に、複数の宣言初期化子がカンマで区切られている場合は、宣言を分割する。例えば、上の例の場合、
long const unsigned static long * volatile x[30] = {0}; long const unsigned static long (*y)[20];
と分割する。
ステップ2: 宣言指定子を並び替える
宣言指定子は3種類に分けられるので、分類する。
static const unsigned long long * volatile x[30] = {0}; static const unsigned long long (*y)[20];
ここで、「unsigned long long」という3つのキーワード(順不同だが重複は数える)の並びが、1つの型をあらわしている。わかりやすさのために、以降はアンダースコアで結ぶことにする。
static const unsigned_long_long * volatile x[30] = {0}; static const unsigned_long_long (*y)[20];
ステップ3: 宣言子を分解する
宣言子を以下のルールで分解する。
T * q d
→q ptr<T> d
T d[q n]
→q arr<T, n> d
(n
は特別な記号*
の場合や、何も書かれていない場合もある)T d(A x, B y)
→fun<T (A x, B y)> d
など- 例外1:
T d()
→fun<T (*)> d
- 例外2:
T d(void)
→fun<T ()> d
- 例外1:
T (d)
→T d
ただし、
T
は分類Aを除く宣言指定子部分である。d
は宣言子である。q
は分類Bの宣言指定子と同じもの。配列の場合はstaticも指定できるが今回は説明しない。
分類Aの宣言指定子と初期化子は変換に巻き込まない。複数のルールが適用可能なら上のほうのルールを優先する。
上記のルールは適用できなくなるまで繰り返す。宣言子についている皮を剥いでいって、剥いだ順に宣言指定子側に被せていく感じになる。つまり順番が逆になる。
例えば以下のように変形する。
static const unsigned_long_long * volatile x[30] = {0}; static volatile ptr<const unsigned_long_long> x[30] = {0}; static arr<ptr<const unsigned_long_long>, 30> x = {0}; static const unsigned_long_long (*y)[20]; static arr<const unsigned_long_long, 20> (*y); static arr<const unsigned_long_long, 20> *y; static ptr<arr<const unsigned_long_long, 20>> y;
ステップ4: 関数の引数を処理する
もし関数の型が含まれていたら、その引数もまた宣言である。そこでこの処理を再帰的に繰り返す。
関数の引数の変数名は無視できるので、削除する。
ステップ5: 配列型を変換する
関数の引数部分については、配列をポインタに降格させる。例えば、
fun<int (int, arr<arr<int, 20>, 10>)>
のような型の場合は、 arr<arr<int, 20>, 10>
が関数の引数なのでこれをポインタに降格させて、
fun<int (int, ptr<arr<int, 20>>)>
とおく。この処理は上のように、配列が関数の引数の内部に間接的に出現する場合は関係ない。
※この処理は、配列がtypedefされた型に対しても行う。
また、関数の引数に間接的に出てくる配列の大きさが定数ではない場合は、これを *
に置き換える。
ステップ6: 完成
おわり
まとめ
C言語の型の構文は、意味を素直に表現していないので混乱のもとである。しかし、「宣言子から皮を剥がしていく作業」の影響範囲をきちんと理解すれば特に怖いものではない。