配列の特性

こんにちは、めのんです!

前回は「配列とポインタ」ということで配列の要素をポインタを使って間接参照することを中心に解説しました。
今回は前回のおさらいも兼ねて配列型からポインタ型への暗黙の型変換に始まって、配列が持ついろんな特性を紹介したいと思います。

配列からポインタへの暗黙の型変換

前回の最後も同じテーマで解説を行いましたが、もう少し補足説明をした方がいいと思いますので、今回も配列からポインタへの暗黙の型変換についてもう少し詳しく解説することにします。

前回は添字演算子はオペランドにポインタ型を要求するので、オペランドに配列を渡すと暗黙の型変換によってポインタ型になるというお話をしました。
でも、それだけの説明ではわかりにくかったと思います。
今回は順を追って具体的に解説していくことにします。

まずは次の例をご覧ください。

int a[] = { 1, 2, 3 };
int *p = a;

上のサンプルはごく一部のコードを抜き出したものですが、このサンプルが配列型からポインタ型への暗黙の型変換を一番わかりやすく表していると思います。

aの型はint[3]型ですのでint*型であるpの型とは異なります。
それでもCではこのような初期化子が書けてしまいます。
これは配列型からその配列の先頭要素へのポインタに暗黙期に変換されるからです。

先ほどのサンプルコードは意味としては次のコードと同じになります。

int a[] = { 1, 2, 3 };
int *p = &a[0];

Cの配列はポインタを要求するほとんどの演算子のオペランドになるとき、暗黙的に先頭要素へのポインタに変換されます。
前回解説したポインタと整数の加減算や、ポインタどうしの引き算なんかがそうです。
もちろん配列の添字演算子もそうです。

int a[] = { 1, 2, 3 };
int *p = a + 2;       // a[2]を参照
ptrdiff_t d = p - a;  // 2
printf("%d\n", a[1]); // 2を出力

配列であるaがまるでポインタであるかのように振る舞っていますね。

でも勘違いしてはいけないのは、配列はあくまでも配列であって、決してポインタではありません。
ましてやポインタ型のオブジェクトではありません。
配列からポインタの暗黙の型変換を見て配列とポインタを同一視してしまう方が結構いらっしゃるようですが、決してそんなことはありませんので注意してくださいね。

配列特有の振る舞いをする演算子

先ほど、

ポインタを要求するほとんどの演算子のオペランドになるとき、暗黙的に先頭要素へのポインタに変換されます。

と書きました。
「ほとんどの」ということは、そうではない演算子もあるということです。
ここではそういった演算子を紹介します。

アドレス演算子

「アドレス演算子」というのはオブジェクトのアドレスを取得する「&」演算子のことです。
配列に対してアドレス演算子を使った場合は、先頭要素へのポインタには暗黙的に変換されません。
ではどうなるかというと、配列型へのポインタ型の値が得られることになります。

int a[] = { 1, 2, 3 };
int (*p)[3] = &a;
printf("%d\n", (*p)[1]);  // a[1]を参照することになるので、2が出力される。

上のサンプルコードでは、配列aにアドレス演算子を使うと配列型であるint[3]型へのポインタ型int(*)[3]の値が返されることを表しています。

int(*)[3]というのパッと見わかりにくいですが、関数へのポインタ型とだいたい同じ形をしているのがわかると思います。
関数へのポインタは覚えていらっしゃいますか?
前々回の最後近くで関数へのポインタについては解説させていただきました。

int (*p)[3]と書かないといけないのは、もしint *p[3]と書いてしまうとint*型の3要素の配列の意味になってしまうからです。
ここで扱うのはint型3要素の配列へのポインタ型です。

pは配列型へのポインタ型ですから、*pと書けばその結果は配列型になります。
配列型であればほとんどの演算子のオペランドになるとき先頭要素へのポインタに変換される性質は同じです。

でも、*p[1]と書くとp[1]に対して「*」演算子を適用することになってしまいます。
p[1]はint型ですから、int型に「*」演算子を適用することはできません。
だからこんな書き方をするとコンパイルエラーになってしまいます(実際にコンパイルできないことを確認してくださいね)。

sizeof演算子

次は「sizeof」演算子です。
sizeof演算子というのはオブジェクトのバイト数を求めるための演算子です。
配列だけでなくどんなオブジェクトでもsizeof演算子のオペランドになります。
さらに、オブジェクトだけでなく型名をオペランドに取ることもできます。

具体例を見ていきますね。

char a;
int b;
int *c = &b;
int d[10];

printf("%zu\n", sizeof a);
printf("%zu\n", sizeof(char));

printf("%zu\n", sizeof b);
printf("%zu\n", sizeof(int));

printf("%zu\n", sizeof c);
printf("%zu\n", sizeof(int*));

printf("%zu\n", sizeof d);
printf("%zu\n", sizeof(int[10]));

上のサンプルコードでは、a, b, c, dの4つのオブジェクトのサイズと、各オブジェクトの型のサイズを求めています。
char型はどんな処理系でも必ず1バイトになりますので、6行目と7行目の出力結果は1になります。
注意しないといけないのは、Cでは1バイトは必ずしも8ビットではないということです。
もっともほとんどの処理系は1バイトは8ビットですのでご安心ください。

int型のサイズは処理系によって異なります。
規格上は16ビット以上ということになっていますが、みなさんが使うことになるほとんどの処理系は32ビットだと思います。
ということは、1バイトが8ビットであればint型は4バイトということになりますので、9行目と10行目の結果は4になるはずです。

ポインタ型のサイズも処理系定義です。
ざっくりいえば、32ビットの環境であればポインタも32ビット、64ビットの環境であればポインタも64ビットになることが多いようです。
みなさんの環境では12行目と13行目の結果はどうなりますか?

最後はint型10要素の配列型ですね。
これはint型のサイズに依存して結果が変わります。
もしint型のサイズが4バイトであれば、4×10ですので15行目と16行目の結果は40になるはずです。
くれぐれも間違わないでいただきたいのは、配列の要素数ではなく配列全体のバイト数が結果になるということです。

もし、この配列dの要素数を求めたいのであれば

sizeof d / sizeof d[0]

とすればOKです。
配列全体のバイト数を1要素のバイト数で割ってあげれば要素数が求まります。

こんな感じでsizeof演算子はオブジェクト(正確にいうと式)や型のサイズを求めることができます。

すでにお気付きの方もいらっしゃるかもしれませんが、オペランドがオブジェクトや式の場合はsizeofのうしろに丸括弧は必要ありませんが、オペランドが型の場合は丸括弧が必要になります。
紛らわしい場合はいつでも丸括弧でオペランドを囲むようにしましょう。
それでどちらでも問題なく使うことができます。

もうひとつ解説しないといけないことがあります。
sizeof演算子の結果は「size_t」型になります。
このsize_t型というのは前回ご紹介したptrdiff_t型と同じで本来は「stddef.h」ヘッダで定義されているのですが、標準ライブラリのほかの多くのヘッダでも定義されています。

size_t型の値をprintf関数で出力するには書式に「%zu」を使います。
16進数で出力したい場合は「%zx」を使ってくださいね。


今回の解説は以上となります。
本当は関数の引数に配列を渡すお話や、多次元配列、それから可変長配列の話もしたかったのですが長くなりすぎてしまいました。
今回はいったんここで終わりにして、別の機会にそれらの解説を行いたいと思います。