わさっきhb

大学(教育研究)とか ,親馬鹿とか,和歌山とか,とか,とか.

2次元配列を仮引数に持つ関数(解説編)

昨日の問題の解説です.
問題文を細切れに書いてみます.

  • 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」に変換されるためです.

*1:Cygwin 1.7で実行し,コンパイラのバージョン情報は「gcc (GCC) 4.3.4 20090804 (release) 1」です.

*2:a[0]とb[0][0]が対応するのではなく,2次元配列bを,intの並びとみたときの先頭がb[0][0]になるためです.