sizeof演算子のまとめ(仮引数の多次元配列)

関数の引数に配列型変数を指定した場合、関数内の当該仮引数はポインタ型変数に型調整される(正確には、配列の先頭要素へのポインタ値が格納されたポインタ型変数に型調整される)。これが多次元配列になると、どうなるだろう。

二次元配列とは、T型配列のTが「配列型」の配列である。まず、二次元配列を関数の引数に指定すると、その仮引数は、その配列の先頭部分配列へのポインタ値が格納された、配列型オブジェクトへのポインタ変数に型調整される。ここまでは予想がつく。

では、そのポインタ変数を間接参照すると、どうなるだろうか。

ここでポインタ変数の間接参照のふるまいについておさらいすると、

  1. 式中においてchar型へのポインタ変数を間接参照すると、char型オブジェクトに展開される。
  2. 式中においてint型へのポインタ変数を間接参照すると、int型オブジェクトに展開される。
  3. 式中において配列型へのポインタ変数を間接参照すると、配列型オブジェクトに展開される。

ということは、先頭部分配列(配列型オブジェクト)にsizeof演算子を適用した返却値は、以下の可能性が考えられる。

  1. 仮引数の配列の要素となる配列型オブジェクトなので、仮引数の配列と同じく「その配列の先頭要素へのポインタ型変数に型調整される」というルールが適用される。その結果、sizeof演算子を適用すると、配列の先頭要素へのポインタ型変数のサイズ、つまりポインタ型整数値のサイズを返す。
  2. 仮引数の配列の要素となる部分配列だとはいっても、配列は配列なので、やはり配列として扱われる。sizeof演算子を適用すると、当該部分配列のサイズを返す。

例えば、以下の二次元配列は、

    int buf[][5] = {{100, 101, 102, 103, 104},
                    {200, 201, 202, 203, 204},
                    {300, 301, 302, 303, 304},};
[0] [1] [2] [3] [4]
buf[0] 100 101 102 103 104
buf[1] 200 201 202 203 204
buf[2] 300 301 302 303 304

というマトリクスで示される。

上記のマトリクスをふまえた上で、考えてみる。
仮引数のbufは先頭要素へのポインタ値を格納するT型オブジェクト(T=配列)へのポインタ型変数に型調整される。そして関数内の式中で「*buf」と間接参照すると、もちろん、以下の部分配列(色つき部分)に展開される。

[0] [1] [2] [3] [4]
buf[0] 100 101 102 103 104
buf[1] 200 201 202 203 204
buf[2] 300 301 302 303 304

ここからが問題である。「仮引数の配列はポインタ型に型調整される」というルールが、その構成要素である部分配列にも適用されるのならば、*bufもやはりポインタ型に型調整される。そしてsizeof(*buf)は、ポインタ型のサイズ(64ビットOSで8バイト)を返す。

あるいは、そのようなルールが適用されないのならば、sizeof(*buf)は以下マトリクスの部分配列(色つき部分)のサイズ(int*5=20バイト)を返す。

[0] [1] [2] [3] [4]
buf[0] 100 101 102 103 104
buf[1] 200 201 202 203 204
buf[2] 300 301 302 303 304

そこで、こんなコードを書いてみた。

  1 /* sizeof_func2.c */
  2 #include    <stdio.h>
  3
  4 void func(int buf[][5]){
  5
  6     printf("@func()\n");
  7     printf("sizeof(buf)   = %zu\n", sizeof(buf));   /* 先頭部分配列へのポインタのサイズ */
  8     printf("buf   = %p\n", buf);
  9     printf("buf+1 = %p\n", buf+1);                  /* bufの参照先データ型サイズを確認 */
 10     printf("sizeof(*buf)  = %zu\n", sizeof(*buf));  /* 先頭部分配列(int*5)のサイズ */
 11     printf("sizeof(**buf) = %zu\n", sizeof(**buf)); /* 先頭部分配列の先頭int型のサイズ */
 12 }
 13
 14 int main(int argc, char *argv[]){
 15
 16     int buf[][5] = {{100, 101, 102, 103, 104},
 17                     {200, 201, 202, 203, 204},
 18                     {300, 301, 302, 303, 304},};
 19
 20     printf("@main()\n");
 21     printf("sizeof(buf)   = %zu\n", sizeof(buf));   /* 配列型配列(int*5*3)のサイズ */
 22     printf("sizeof(*buf)  = %zu\n", sizeof(*buf));  /* 先頭部分配列(int*5)のサイズ */
 23     printf("sizeof(**buf) = %zu\n", sizeof(**buf)); /* 先頭部分配列の先頭int型のサイズ */
 24     printf("\n");
 25
 26     func(buf);
 27
 28     return 0;
 29 }
 27 }

実行結果

@main()
sizeof(buf)   = 60     /* 配列型配列(int*5*3)のサイズ */
sizeof(*buf)  = 20     /* 先頭部分配列(int*5)のサイズ */
sizeof(**buf) = 4      /* 先頭部分配列の先頭int型のサイズ */

@func()
sizeof(buf)   = 8      /* 先頭部分配列へのポインタのサイズ */
buf   = 0x7fffac612440
buf+1 = 0x7fffac612454 /* bufの参照先データ型サイズを確認 */
sizeof(*buf)  = 20     /* 先頭部分配列(int*5)のサイズ */
sizeof(**buf) = 4      /* 先頭部分配列の先頭int型のサイズ */

となった。

関数内のsizeof(buf)は8バイト(ポインタ型のサイズ)となっている。つまり、bufは以下の色つきの「部分配列そのもの」へのポインタ値を格納するポインタ型変数として扱われていることが分かる。このポインタ値を+1するとアドレスが20バイト進んでいることからも、int*5の配列へのポインタ型変数であることを示している。

[0] [1] [2] [3] [4]
buf[0] 100 101 102 103 104
buf[1] 200 201 202 203 204
buf[2] 300 301 302 303 304

そして、関数内のsizeof(*buf)は20バイト(int*5)となっている。このことから、*bufは以下の色つきの「部分配列そのもの」として扱われていることが分かる。

[0] [1] [2] [3] [4]
buf[0] 100 101 102 103 104
buf[1] 200 201 202 203 204
buf[2] 300 301 302 303 304

また、関数内のsizeof(**buf)は4バイト(int)なので、**bufは以下の色つきの「部分配列の先頭要素(int型オブジェクト)」として扱われていることが分かる。

[0] [1] [2] [3] [4]
buf[0] 100 101 102 103 104
buf[1] 200 201 202 203 204
buf[2] 300 301 302 303 304

以上をまとめると、

  1. 関数内の仮引数の配列は、その配列の先頭要素へのポインタ値を格納したT型オブジェクト(T=配列)へのポインタ変数に型調整される。
  2. ただし上記は最上位次元の配列のみの話である。それ以外の次元の配列は、配列そのものに展開される。

無用に難しく考えてしまった気がするけど、実装レベルで考えてみると、関数呼出し時にスタックに積むのは、「配列の先頭要素へのポインタ値」である。また、関数プロトタイプ宣言においては、仮引数の多次元配列は、最上位次元以外の次元の要素数は省略できない。つまり、最上位次元以外の要素数コンパイラに伝わる情報なので、ポインタ型扱いになるのが最上位次元のみに限った話というのも道理ではある。