sizeof演算子のまとめ(配列の型調整)

sizeof演算子の続きです。
関数のプロトタイプ宣言で配列型を指定された引数は、関数の仮引数ではポインタ型として扱われる、というお話です。

  1 /* sizeof_func.c */
  2 #include    <stdio.h>
  3
  4 void func(char buf[]){
  5     printf("sizeof(buf)@func        = %zu\n", sizeof(buf));
  6 }
  7
  8 int main(int argv, char *argc[]){
  9
 10     char buf[] = "0123456789";
 11
 12     printf("sizeof(buf)@main        = %zu\n", sizeof(buf));
 13     func(buf);
 14
 15     return 0;
 16 }

実行結果

sizeof(buf)@main		= 11
sizeof(buf)@func		= 8

main関数内で宣言された配列変数bufのサイズは11バイトですが、関数funcの引数(char []型)にbufを指定し、関数内でのbufのサイズを参照すると、8バイト(ポインタ型のサイズ)となりました。これは、関数のプロトタイプ宣言において、引数の型を配列型に指定しても、関数内部での仮引数はポインタ型として扱われるためです(型調整)。

配列型とポインタ型の違いとして、「配列型変数は左辺値になりえないが、ポインタ型変数は左辺値になりうる」というのがあります。そこで、func内で当該の仮引数が左辺値になりうるかを確かめるために、bufを左辺値とする式を書いてみます。

  1 /* sizeof_func.c */
  2 #include    <stdio.h>
  3
  4 char *g_buf = "ABCDEFGHIJ";
  5
  6 void func(char buf[]){
  7     printf("g_buf   = %p (%s) @ %p\n\n", g_buf, g_buf, &g_buf);
  8
  9     printf("buf     = %p (%s) @ %p\n\n", buf, buf, &buf);
 10     buf = g_buf;    /* ★仮引数bufを左辺値にして代入 */
 11     printf("    buf <= g_buf\n\n");
 12     printf("buf     = %p (%s) @ %p\n", buf, buf, &buf);
 13 }
 14
 15 int main(int argv, char *argc[]){
 16
 17     char buf[] = "0123456789";
 18
 19
 20     printf("buf[]   = %s ( ---- ) @ %p\n", buf, &buf);
 21     printf("-----------------------------\n");
 22     func(buf);
 23     printf("-----------------------------\n");
 24     printf("buf[]   = %s ( ---- ) @ %p\n", buf, &buf);
 25
 26     return 0;
 27 }

実行結果

buf[] 	= 0123456789 ( ---- ) @ 0x7fffb64f7180    …(1)
-----------------------------
g_buf 	= 0x400718 (ABCDEFGHIJ) @ 0x600a80

buf 	= 0x7fffb64f7180 (0123456789) @ 0x7fffb64f7148 …(2)

    buf <= g_buf …(3)

buf 	= 0x400718 (ABCDEFGHIJ) @ 0x7fffb64f7148    …(4)
-----------------------------
buf[] 	= 0123456789 ( ---- ) @ 0x7fffb64f7180     …(5)

(1)実引数bufは、アドレス「0x7fffb64f7180」番地を格納領域とする配列型変数です。その領域には、「"0123456789"」という文字列が格納されています。

(2)関数funcに、実引数bufの格納領域のアドレス(0x7fffb64f7180)を渡しました(このアドレスは、関数main内スタックフレーム上のある場所を指すものです)。その結果、関数func内スタックフレーム上の領域(0x7fffb64f7148)に、実引数buf領域のアドレス(0x7fffb64f7180)が格納されたことになります。

(3)g_bufの値(0x400718("ABCDEFGHIJ"))を、仮引数bufの領域(0x7fffb64f7148)に格納します。

(4)(3)より、仮引数buf(領域0x7fffb64f7148)には、"ABCDEFGHIJ"のアドレス(0x400718)が格納されます。

(5)実引数buf(領域0x7fffb64f7180)には、「"0123456789"」が格納されたままとなります。

つまり、関数内での仮引数bufには、代入前は"0123456789"のアドレスが格納されていましたが、代入後は"ABCDE"のアドレスが格納されていることになります。これらはスタック上の仮引数に対する操作であるため、呼び出し元のmain関数のスタックフレーム内の領域(実引数buf)には何も影響を与えていません。そのため、main関数において、関数func呼出し前後で実引数bufの内容を表示すると、その内容は変化していません。

以上を表にまとめると、以下の通りとなります。
(変化した値の背景色を変更しています)

関数func内における「buf = g_buf」の代入前

変数名 アドレス 左記に格納された値 ポインタ値が指す領域の値
実引数buf[] 0x0x7fff01ea9bf0 "0123456789"
仮引数*buf 0x0x7fff01ea9bc8 0x0x7fff01ea9bf0 "0123456789"
変数g_buf 0x0x400748 "ABCDEFGHIJ"

関数func内における「buf = g_buf」の代入後

変数名 アドレス 左記に格納された値 ポインタ値が指す領域の値
実引数buf[] 0x0x7fff01ea9bf0 "0123456789"
仮引数*buf 0x0x7fff01ea9bc8 0x0x400748 "ABCDEFGHIJ"
変数g_buf 0x0x400748 "ABCDEFGHIJ"

というわけで、関数func内での仮引数bufへの値代入に成功したということは、仮引数bufは左辺値になりうる…つまり、関数内の配列型変数はポインタ変数に型調整されていることになります。