イントロ
C言語の型宣言ってややこしいですよね。
大体の場合は型なんてシンプルなので大丈夫なんですが、
たまにややこしいやつが出てくると悩まされることがあります。
例えば下記、見慣れてると思います。
int main(int argc, char * argv[]);
第二引数のargvですが、正しく理解していますか?
「charの配列へのポインタ」なのか、
それとも、
「charのポインタへの配列」なのか、
答えられますか?
残念な事に、僕は6年間ぐらいコレが曖昧なまま放置していました。
ですが、わかってみると本当はめちゃめちゃ簡単なんです。
単にC言語の解説の際に、なぜかそれが全然解説されていないだけなんです。(要出典)
なので、解説を書こうと思いました。
覚えることは7個だけ
覚えなきゃいけないのは以下の7個の規則です。
規則1: 「*
」は「pointer to ...
」
「*
」はポインタを表す記号です。知ってますね。
ここでは、「pointer to ...
」を覚えてください。
規則2: 「[]
」は「array of ...
」
「array of ...
」を覚えてください。
[]
の中には要素数を書けます。
規則3: 「()
」は「function returning ...
」
「function returning ...
」を覚えてください。
()
の中には関数の引数の型を書けます。
規則4: 「*」は左からかかる
A * B
という形の場合、*
は右側のB
を修飾しています。
*
を型に対する単項演算子と考えると、オペランドを右側に取るということです。
例えば、
int * a
であれば、*
はa
に掛かっています。int
には掛かっていません。
int ** b;
であれば、左の*
は右の*
に掛かっています。
右の*
は、b
に掛かっています。
規則5: 「[]
」は右からかかる
A[]
のように、[]
は右側に書かれ、左側にオペランドを取ります。
規則6: 「()
」も右からかかる
A()
のように、()
は右側に書かれ、左側にオペランドを取ります。
規則7: 「*
」の結合より、「[]
」と「()
」の結合の方が優先度が強い
* A []
のように、ある型の両側に*
と[]
がある場合には、
*
とA
の結合よりも、A
と[]
の結合の方が強いため、
まずA
と[]
が結合して「A[]
」となり、
その後で「*
」と「A[]
」が結合します。
これまでの6個の規則は、おそらくすぐに覚えられたのではないかと思います。
重要なのはこの規則7です。
ぶっちゃけ、これさえ暗記してしまえばC言語型宣言はマスターできます。
がんばって覚えましょう。
参考までに、僕は「*
は意外と遅い」と覚えています。
なんとなく、素早く見えるので。「*
」。
変数宣言の読解
上記7規則を理解したら、後は、 名前のところから 読んでいきます。
char * argv[]
を例に取りましょう。
まず、名前のところから読み始めるので、argv
に注目します。
argv
の両隣には、*
と[]
があります。
*
は右側にオペランドを取るので、一見これはargv
に掛かっています。(規則5)
[]
は左側にオペランドを取るので、一見これもargv
に掛かっています。(規則6)
ここで規則7を考慮すると、*
より[]
が優先するので、
[]
がargv
に掛かっているとわかります。
なので、「*
」は、「argv
」では無く「argv[]
」に掛かっているとわかります。
最後に、余った「char
」の部分を拾っておきます。
よって下記の順番で意味が構成されているとわかります。
「argv
」→「[]
」→「*
」→「char
」
結合順序がわかったら、次にこれを、規則1,2,3を用いて書き換えていきます。
ついでに、最初の名前の後ろにはisを付けます。
すると、下記のようになります。
argv is array of pointer to char
これをそのまま英語で解釈すれば型の読解が完了です!
日本語に訳すと、
argv は char へのポインタ の配列
となります。
関数宣言の読解
この読解法ですが、変数宣言だけでなく、関数宣言にもそのまま適用できます。
int main(int argc, char * argv[])
上記を読解します。
まず、名前の部分がmain
ですね。
次に、main
の右に()
があります。
よって、全体としては下記のとおりです。
結合: 「main」→「()」→「int」
英語: main is function returning int
和訳: main は int を返す関数
さて、()
の中身は、規則3より、関数の引数の型です。
第一引数は下記のとおりです。
結合: 「argc」→「int」
英語: argc is int
和訳: argc は int
第二引数は先述したとおりです。
結合: 「argv」→「[]」→「*」→「char」
英語: argv is array of pointer to char
和訳: argv は char へのポインタ の配列
さて、初めの全体の解釈に、引数の解釈をくっつけて、下記のようになります。
main は int を返す ( 第一引数は argc で int, 第二引数が argv で char へのポインタ の配列 ) である関数
というわけです。
関数ポインタの読解
当然関数ポインタも楽勝です。
void qsort(void *base, size_t nel, size_t width, int (*compar)(const void *, const void *));
上記を読解します。
全体は下記の通り。
qsort is function returning void
qsortの第一、第二、第三引数は下記の通り。
base is pointer to void
nel is size_t
width is size_t
第4引数compar
を丁寧に見ていきます。
compar
を囲む丸カッコがありますね。
これは式を書く時の丸カッコと同じで、演算子間の結合優先度を制御するためのものです。
なので、compar
にはまず、*
が左から掛かっていることがわかります。
「(*compar)
」に対しては、右側から()
が掛かっています。
最後にint
が残ります。
よって、compar
は下記のとおりです。
結合: 「compar」→「*」→「()」→「int」
英語: compar is pointer to function returning int
和訳: compar は int を返す関数 へのポインタ
そして、()
の中には関数の引数の型があります。
名前が省略されていますが、同じようにやります。
結合: 「*」→「const void」
英語: pointer to const void
和訳: const void へのポインタ
よって以上をまとめて、下記のとおりです。
qsortはvoidを返す(
第一引数がbaseでvoidへのポインタ、第二引数がnelでsize_t、第三引数がwidthでsize_t,
第四引数がcomparでintを返す(
第一引数がconst voidへのポインタ、第二引数がconst voidへのポインタ
)である関数へのポインタ
)である関数
関数ポインタのパターン
argv
の結合と、compar
の結合を見比べてみてください。
argv
の場合はまず[]
が結合し、次に*
が結合しています。
compar
では、*
が結合した後に()
が結合しています。
規則7の通り、*
と()
,[]
では、後者達の方が結合が強いので、
両側にそのまま書いたargv
では[]
が先に結合して、*
が後に結合しています。
一方、compar
では、句を括るための()
を使うことで、
規則7の優先順位を変更し、まず*
を結合させ、次に()
を結合させています。
この、「*
」→「()
」の順番が、「pointer to function
」となり、
関数ポインタの型記述となるのです。
関数ポインタはpointer to function
ですから、
この「*
」→「()
」の結合をさせるために、
名前と*
を丸括弧で囲むのは基本パターンとなります。
その結果、下記のようなイディオムが導かれるわけです。
関数の返り値の型 (*関数ポインタ名)(引数)
これで、関数ポインタ名のところを囲っていた()
の意味がはっきりしたと思います。
関数と関数ポインタ
関数ポインタはpointer to function
でした。
そこで、この、pointer to
を取り除いてやればfunction
、
つまり通常の関数宣言になります。
pointer to
、つまり「*
」を取り除くと、
当然、「*
」を名前に結合させるために必要だった丸括弧
も取り除かれるため、下記のようになります。
関数ポインタのパターン
関数の戻り値の型 (*関数ポインタ名)(引数)
pointer toを取り除いた結果
関数の戻り値の型 関数名(引数)
すると、必然的に関数宣言の形になっています。
この関係性を理解すると、
関数ポインタの気持ち悪さがだいぶ緩和されます。
型宣言を組み立てる
これまでのやり方を、逆向きに使えば、
型宣言を記述することが出来ます。
次の例でやってみます。
intを返す関数ポインタの要素2つの配列へのポインタを返す関数f
を宣言することにします。
関数ポインタの引数と返り値はint
, f
の引数はint
とします。
英語で考えると以下のようになります。
f is function(int) returning pointer to array(len=2) of pointer to function(int) returning int
これを、左側から順番に変換していきます。
f is
f
function(int) returning
f(int x)
pointer to
*f(int x)
補足: ()の方が*よりも結合が強いため、このまま左側に*を書き足すだけです。
array(len=2) of
(*f(int x))[2]
補足: []の方が*よりも結合が強いため、これまでの部分を丸括弧で囲っておいてから、右側に[]を書き足します。
pointer to
*(*f(int x))[2]
補足: []の方が*よりも結合が強いため、このまま左側に*を書き足すだけです。
function returning
(*(*f(int x))[2])(int)
補足: ()の方が*よりも結合が強いため、これまでの部分を丸括弧で囲っておいてから、右側に()を書き足します。
上述した、pointer to arrayの「(*)()」パターンです。
int
int (*(*f())[2])(int)
このように、関数宣言が完成します。
型の意味がわかりやすいように、サンプルコードを示します。
#include<stdio.h>
int add1(int x){
return x+1;
}
int add2(int x){
return x+2;
}
int sub1(int x){
return x-1;
}
int sub2(int x){
return x-2;
}
int (*adds[2])(int) = { &add1, &add2 };
int (*subs[2])(int) = { &sub1, &sub2 };
int (*(*f(int x))[2])(int){
if(x==1){
return & adds;
}else{
return & subs;
}
}
int main(int argc, char * argv[]){
printf("%d,%d\n",
((*f(argc))[0])(1),
((*f(argc))[1])(2));
return 0;
}