昨日の問題の解説です.
問題文を細切れに書いてみます.
- mainの中で
- sizeof(b)の値を答えなさい.
- sizeof(b[0])の値を答えなさい.
- sizeof(b[0][0])の値を答えなさい.
- このプログラムで,print_arrayを呼び出したときの,
- sizeof(a)の値を答えなさい.
- sizeof(a[0])の値を答えなさい.
- sizeof(a[0][0])の値を答えなさい.
- 「int a[2][3]」を「int **a」に書き換え,print_arrayを呼び出したときの,
- sizeof(a)の値を答えなさい.
- sizeof(a[0])の値を答えなさい.
- sizeof(a[0][0])の値を答えなさい.
- 以上から何が言えるかを答えなさい.
sizeofを求める
上記の9つのsizeofナニナニを求めるために,プログラムを変更します.
#include <stdio.h> #define print_sizeof(x) printf("sizeof(" #x "): %d\n", sizeof(x)) void print_array(int a[2][3]) /* int **a ではうまくいかない */ { int i, j; print_sizeof(a); print_sizeof(a[0]); print_sizeof(a[0][0]); for (i = 0; i < 2; i++) { for (j = 0; j < 3; j++) { printf("(%d,%d): %d\n", i, j, a[i][j]); } } } void print_array_star(int **a) { int i, j; print_sizeof(a); print_sizeof(a[0]); print_sizeof(a[0][0]); for (i = 0; i < 2; i++) { for (j = 0; j < 3; j++) { printf("(%d,%d): %d\n", i, j, a[i][j]); } } } int main(void) { int b[2][3] = { {1, 2, 3}, {4, 5, 6} }; print_sizeof(int); print_sizeof(int *); print_sizeof(int **); print_sizeof(b); print_sizeof(b[0]); print_sizeof(b[0][0]); print_array(b); print_array_star(b); return 0; }
コンパイル時に
______.c: In function ‘main’: ______.c:50: 警告: passing argument 1 of ‘print_array_star’ from incompatible pointer type
という警告が出ました*1.しかし実行ファイルはできていますので,実行してみると,以下の出力を得ました.
sizeof(int): 4 sizeof(int *): 4 sizeof(int **): 4 sizeof(b): 24 sizeof(b[0]): 12 sizeof(b[0][0]): 4 sizeof(a): 4 sizeof(a[0]): 12 sizeof(a[0][0]): 4 (0,0): 1 (0,1): 2 (0,2): 3 (1,0): 4 (1,1): 5 (1,2): 6 sizeof(a): 4 sizeof(a[0]): 4 sizeof(a[0][0]): 4
ということで,
- mainの中で
- sizeof(b)の値は,24
- sizeof(b[0])の値は,12
- sizeof(b[0][0])の値は,4
- このプログラムで,print_arrayを呼び出したときの,
- sizeof(a)の値は,4
- sizeof(a[0])の値は,12
- sizeof(a[0][0])の値は,4
- 「int a[2][3]」を「int **a」に書き換え,print_arrayを呼び出したときの,
- sizeof(a)の値は,4
- sizeof(a[0])の値は,4
- sizeof(a[0][0])の値は,4
と分かります.
なぜこうなったのかを検討する前に,プログラムと出力を見比べておきます.プログラムでは,mainからprint_arrayとprint_array_starを呼び出しており,それぞれは2重ループで配列の中身を出力するよう,記述されています.しかし出力では,2重ループの出力は1セットしかありません.sizeofナニナニの出力を踏まえると,print_array_starでの,2重ループの配列出力が処理されていないことが分かります.別の実行環境で動かすと「セグメントエラー」が出ます.この処理の際に,実行時エラーが発生しているのです.
配列かポインタか分類
sizeofナニナニの値に立ち返り,また配列とポインタの諸性質をもとにすると,以下のようになります.
- mainの中で
- bは,2個の「3個のintの配列」の配列
- b[0]は,3個のintの配列
- b[0][0]は,1個のintの値
- このプログラムで,print_arrayを呼び出したときの,
- aは,ポインタ
- a[0]は,3個のintの配列
- a[0][0]は,1個のintの値
- 「int a[2][3]」を「int **a」に書き換え,print_arrayを呼び出したときの,
- aは,ポインタ
- a[0]も,ポインタ
- a[0][0]もまた,ポインタ…のわけがありませんね.1個のintの値です.
2次元配列で宣言した仮引数の正体
このプログラムの,一番の要所は,print_arrayの仮引数aが何者なのかです.「aは,ポインタ」と断定しましたが,これは,関数の仮引数を配列形式で書いていたら,それはポインタ変数となるという,Cの仕様があるからです.「sizeof(a): 4」という出力は,それを裏付けています.
次に考えないといけないのは,何のポインタであるかです.sizeofは,オペランドがポインタか配列かの識別には役に立っても,何のポインタであるかは教えてくれません.
ですが今回のケースでは,print_arrayを呼び出したときの,sizeof(a[0])が大きなヒントとなっています.上述のとおり,「3個のintの配列」です.aと&a[0]が同じ値(同じポインタ型かつ同じアドレス)になることを利用すると,aは“「3個のintの配列」のポインタ”と言えます.
mainで,print_array(b)として呼び出すとき,この実引数bは,ポインタに変わります.有名なルールがあって,式の中の配列変数は,sizeofまたは単項&のオペランドである場合を除き,その先頭アドレスを指し示すポインタ値として評価されます.sizeof演算子を適用したとき,bは“2個の「3個のintの配列」の配列”でしたが,関数呼び出しの実引数においては,“「3個のintの配列」の配列の先頭アドレスを指し示すポインタ値”となります.値を取り除くと,“「3個のintの配列」のポインタ”です.ということで,print_arrayの仮引数aと型が一致し,問題なく代入できるわけです.
2次元配列と,ポインタのポインタは,型が合わない
では,mainからprint_array_star(b)を呼び出す場合は,どうなるのでしょうか.bはここでも“「3個のintの配列」のポインタ”です.それに対して,仮引数aは“「intのポインタ」のポインタ”です.ここで,ポインタ型のミスマッチが起こっており,コンパイル時の警告の「incompatible pointer type」に現れています.
しかしポインタとしての型が違っていても,「アドレス」という点では同じなので,無理矢理代入しています.そうしてみても,そのアドレスの解釈がおかしなものになるので,実行時エラーになってしまうのでした.
a[i][j]の意味〜仮引数が2次元配列のとき
print_arrayとprint_array_starの中で,仮引数であるポインタ変数aに対して,a[i][j]がどんな値となるのかを,検討してみます.まず,p[q]は*(p+q)と同じというルールを2度適用すると,a[i][j]は*(*(a + i) + j)となります.
print_arrayにおいて,aは,mainの中の配列bの先頭アドレスを指す,ポインタ変数です.“「3個のintの配列」のポインタ”ですから,a + iという式は,a[0]のアドレスを起点として,「3個のintの配列」を移動の単位としてi個移動したときの先頭アドレスとなります.これは,mainの中のbを使って,b + iと一致します.(ただしbとiのスコープが違うので,こういう式が作れるというわけではありません.bとiを組み合わせた式を以降で何度か使用しますが,それらも同様です.)
*(a + i)は,a + iのアドレスを起点とする,「3個のintの配列」となります.この段階ではまだ配列です.配列であってポインタではないのは,「print_sizeof(*(a + i));」を挿入すれば「12」と出ることや,「*(a + i) = NULL;」と代入を試みるとコンパイルエラーになることから,確認できます.
*(*(a + i) + j)を評価する際の「*(a + i)」,となると,これはポインタです.「3個のintの配列」の先頭を指し示す,intのポインタになります.そして「*(intのポインタ + j)」ですから,その先頭からintでj個分の要素だけ離れたところに格納されている,intの値となります.実体としてはb[i][j]と同じになります.そしてそこに代入ができます.すなわち,print_arrayの2重ループの中で,a[i][j]=100+i+j;と書いてみると,mainの中の配列bの中身が変わることになります.
a[i][j]の意味〜仮引数がポインタのポインタのとき
print_array_starの中の,a[i][j]すなわち*(*(a + i) + j)は,どうなるでしょうか.
まずaは,“「intのポインタ」のポインタ”でしたね.するとa + iは,a[0]のアドレスを起点として,「intのポインタ」を移動の単位としてi個移動したときの先頭アドレスとなります.sizeof(int)とsizeof(int *)がともに4なので,mainのbが「2×3個のintの配列」なのに反して「intのポインタの並び」であるかのように移動することになります.
そして*(a + i)は,b[0][i]を,実体はintなのですがこれを「intのポインタ」とみなします.print_array_starを呼び出したとき,最初に参照されるのは,a[0][0]という式におけるa[0]ですが,これは,b[0][0]の値*2すなわち1を,intのポインタなのでアドレスとみなしたものとなります.
*(*(a + i) + j)は,*(a + i)を起点として,そこからintでj個分の要素だけ離れたところに格納されている,intの値を意味します.i==0, j==0で,b[0][0]==1のとき,*((int *)1)を求めようとします.「1番地の指し示す値を参照する」というのが実行環境で認めらないため,そこで終了となった,ということです.
まとめ
配列かポインタか分類した結果を書き直すと,次のようになります.
- mainの中で
- bは,2個の「3個のintの配列」の配列
- b[0]は,3個のintの配列
- b[0][0]は,1個のintの値
- このプログラムで,print_arrayを呼び出したときの,
- aは,「3個のintの配列」のポインタで,a = b;としてよい.
- a[0]は,3個のintの配列
- a[0][0]は,1個のintの値
- 「int a[2][3]」を「int **a」に書き換え,print_arrayを呼び出したときの,
- aは,「intのポインタ」のポインタで,a = b;としてはいけない.
- a[0]は,intのポインタ
- a[0][0]は,1個のintの値
配列・ポインタの重要な性質やルールとして,上で書いたことを,ここに整理しておきます.
- 関数の仮引数を配列形式で書いていたら,それはポインタ変数となります.
- sizeofは,オペランドがポインタか配列かの識別には役に立っても,何のポインタであるかは教えてくれません.
- 式の中の配列は,sizeofまたは単項&のオペランドである場合を除き,その先頭アドレスを指し示すポインタ値として評価されます.
- ポインタとしての型が違っていても,「アドレス」という点では同じなので,無理矢理代入できます.そうしてみても,そのアドレスの解釈がおかしいと,実行時エラーになってしまいます.
- p[q]は*(p+q)と同じです.
- ポインタaと整数iを用いて,a + iと書くと,a[0]のアドレスを起点として,sizeof(a[0])を移動の単位としてiだけ移動したときの先頭アドレスとなります.
本日のエントリは,図を使わず,またごくわずかな例外を除いて型名も使用しませんでしたが,型名に基づいて検討すると,少しだけ議論が楽になるので,書いておきます.
print_array関数の仮引数を「int (*a)[3]」と書き換えても,問題なく動きます.しかし「int *a[3]」では,うまくいきません.関数の仮引数という条件のもとでは,「int a[整数値または何もなし][3]」は「int (*a)[3]」と同じであり,「int *a[整数値または何もなし]」は「int **a」に変換されるためです.