【配列とポインタ】第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