【配列とポインタ】第1回 配列

C言語中級者にとっての鬼門と言っても過言ではない、配列とポインタ絡みのところが、いつまで立ってもモヤモヤして気持ち悪い!ので、何回かに分けて、基礎からまとめてみたいと思います。

宣言と初期化

char型配列の宣言時の初期化子には、以下例のように文字列リテラルを指定するか、各要素を順に列挙します。文字列リテラルを指定する場合、末尾にナル文字が自動的にセットされますが、要素を列挙する場合は、初期化子の末尾にナル文字を指定する必要があります。

    char arr[] = "0123456789";
    char arr2[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '\0'};

アセンブリレベルでの初期化手順を確認してみます(gcc バージョン 4.7.3)。

	char arr[] = "0123456789";
 8048451:	c7 44 24 06 30 31 32 	mov    DWORD PTR [esp+0x6],0x33323130
 8048458:	33 
 8048459:	c7 44 24 0a 34 35 36 	mov    DWORD PTR [esp+0xa],0x37363534
 8048460:	37 
 8048461:	66 c7 44 24 0e 38 39 	mov    WORD PTR [esp+0xe],0x3938
 8048468:	c6 44 24 10 00       	mov    BYTE PTR [esp+0x10],0x0
	char arr2[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '\0'};
 804846d:	c6 44 24 11 30       	mov    BYTE PTR [esp+0x11],0x30
 8048472:	c6 44 24 12 31       	mov    BYTE PTR [esp+0x12],0x31
 8048477:	c6 44 24 13 32       	mov    BYTE PTR [esp+0x13],0x32
 804847c:	c6 44 24 14 33       	mov    BYTE PTR [esp+0x14],0x33
 8048481:	c6 44 24 15 34       	mov    BYTE PTR [esp+0x15],0x34
 8048486:	c6 44 24 16 35       	mov    BYTE PTR [esp+0x16],0x35
 804848b:	c6 44 24 17 36       	mov    BYTE PTR [esp+0x17],0x36
 8048490:	c6 44 24 18 37       	mov    BYTE PTR [esp+0x18],0x37
 8048495:	c6 44 24 19 38       	mov    BYTE PTR [esp+0x19],0x38
 804849a:	c6 44 24 1a 39       	mov    BYTE PTR [esp+0x1a],0x39
 804849f:	c6 44 24 1b 00       	mov    BYTE PTR [esp+0x1b],0x0

char型配列の初期化子に、1要素ずつ指定することも、文字列リテラルで指定することもできる…というのは、ネイティブなレベルで互換性のある純粋なシンタックスシュガーではなさそうです。1要素ずつの指定は、要素数分のmov命令を発行しており、一方、文字列リテラルによる指定は、4バイト単位で分割した各データごとにmov命令を発行しています。ナル文字の入力が不要、可読性の高さ、オーバーヘッドの低さ、と、文字列リテラルによる指定の方がメリットが高いですね。

「式中の配列シンボル、または式中の文字列リテラルは、その先頭要素へのポインタに展開される」という規則がありました。"saae"は、先頭要素's'へのポインタに展開されるのでは?と思いますが、ここは文脈が「式」ではなく「宣言部」であるので、上記のルールは当てはまりません。

値の参照

配列の参照方法について、見てみます。

    /* 配列の先頭要素へのポインタを指定 */
    printf("arr        = %s\n", arr);
    printf("&arr[0]    = %s\n", &arr[0]);
    /* 要素の参照 */
    printf("arr[9]     = %c\n", arr[9]);
    printf("*(arr + 9) = %c\n", *(arr + 9));
    /* 配列の途中要素へのポインタを指定 */
    printf("arr + 5    = %s\n", (arr + 5));
    printf("&arr[5]    = %s\n", &arr[5]);

以下、実行結果。

    /* 配列の先頭要素へのポインタ */
arr        = 0123456789
&arr[0]    = 0123456789
    /* 要素の参照 */
arr[9]     = 9
*(arr + 9) = 9
    /* 配列の途中要素へのポインタ指定 */
arr + 5    = 56789
&arr[5]    = 56789

Cにおける「文字列」というものについて、厳密に言うと「指定されたchar型ポインタで示されるアドレスから、ナル文字が出現するまでの連続したchar型の配列」、となるでしょうか。したがって、printf(3)の仮引数に指定しているのは「文字列」というより、「char型配列の先頭要素へのポインタ」です。

さらに言うと、printf(3)には、単に「char型オブジェクトへのポインタ」という一つの値を指定しているだけで、それが文字列として成立するのは、(1)当該文字列が、「指定したポインタで示されるアドレス」から始まる連続したメモリ領域に格納されていること、(2)終端にナル文字が存在すること、をプログラマーが保証する限りにおいて、と言えます。

配列の先頭要素へのポインタ指定について、単にシンボルを記述する("arr")ことも、実装に忠実な表現("&arra[0]")で記述することもできますが、これらはネイティブなレベルで互換性のある純粋なシンタックスシュガーなのでしょうか。以下で、簡単なサンプルプログラムを逆アセンブルしてみました。

	ptr_chr = arr;
 80484d4:	8d 44 24 16          	lea    eax,[esp+0x16]
 80484d8:	89 44 24 10          	mov    DWORD PTR [esp+0x10],eax
	ptr_chr = &arr[0];
 80484dc:	8d 44 24 16          	lea    eax,[esp+0x16]
 80484e0:	89 44 24 10          	mov    DWORD PTR [esp+0x10],eax

と、ネイティブなレベルで互換性があるようです。コンパイラによる翻訳工程において、翻訳の手順の違いがあるのかもしれませんが(JISX3010を調べないとな…)。

また、配列中の要素(char型)を参照する、おもな記述スタイルとして、添字指定(arr[n])か、あるいはシンボル名に要素番号を加算した評価結果を間接演算子*で値に展開する(*(arr + n))の二通りがありますが、これもバイナリレベルでは相違はあるのでしょうか。

	c = arr[9];
 80484e4:	0f b6 44 24 2f       	movzx  eax,BYTE PTR [esp+0x2f]
 80484e9:	88 44 24 1f          	mov    BYTE PTR [esp+0x1f],al
	c = *(arr + 9);
 80484ed:	0f b6 44 24 2f       	movzx  eax,BYTE PTR [esp+0x2f]
 80484f2:	88 44 24 1f          	mov    BYTE PTR [esp+0x1f],al

こちらもまったく同じです。やはり、純粋なシンタックスシュガーですね。

最後に、文字列の途中から部分文字列を切り出す場合、以下のような記述をします。って、バカにするな!と怒られそうなぐらい、超基礎的なテクニックでした…。

/* 配列の途中要素へのポインタ指定 */
    printf("arr + 5    = %s\n", (arr + 5));
    printf("&arr[5]    = %s\n", &arr[5]);

実行結果

arr + 5    = 56789
&arr[5]    = 56789