70
65

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.

C言語の型宣言の解説

Posted at

イントロ

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;
}

おわり

70
65
1

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
70
65

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?