配列・ポインタ・構造体
配列
配列を使うプログラムを考えよう。
#include <stdio.h>
extern int sum(int x[], int n);
int x[] = { 3, 1, 4, 1, 5, 9, 2, 6, 5, 3 };
int main()
{
printf("%d\n", sum(x, 10));
return 0;
}
以下は,配列の要素の総和を計算する関数 sum をアセンブリ言語で記述した例だ。
section .text
global sum
; 配列の要素の総和を求める.
; int sum(int x[], int n);
; @param x 配列
; @param n xの要素数
; @return xの要素の総和
sum:
mov edx, [esp + 4] ; x
mov ecx, [esp + 8] ; n
mov eax, 0 ; 総和
loop:
add eax, [edx] ; eax += x[i]
add edx, 4 ; 次の要素の番地
dec ecx
jnz loop
ret
C言語の配列は,単に主記憶上に要素が並んでいるだけだ。 つまり,以下の記述は同じ意味と考えてよい。
C言語
int x[] = { 3, 1, 4, 1, 5, 9, 2, 6, 5, 3 };
アセンブリ言語
x: dd 3, 1, 4, 1, 5, 9, 2, 6, 5, 3
関数の引数が配列である場合,実際に渡されるのは配列の先頭要素の番地だ。 配列 x の第 i 要素 x[i] の番地は,1要素の大きさ(バイト数)を σ とすると,x + i × σ になる。
引数で渡された配列の要素数はサブルーチン側からはわからないので,上記の sum の第2引数のように,必要なら要素数も引数として渡す必要がある。
ポインタ
以下のプログラムの関数 accum のように,引数として渡された変数の値を更新する関数を考える。
#include <stdio.h>
extern void accum(int *a, int b);
int main()
{
int v = 12;
accum(&v, 34); /* vに34を加える */
printf("%d\n", v); /* 46が出力される */
return 0;
}
accumをC言語で記述した場合:
void accum(int *a, int b)
{
*a += b; /* aが指す記憶領域にbを加える */
}
仮引数 a はint型のポインタを格納する。 ポインタとは番地のこと,と考えてよい。
accum(&v, 34);
の &
は変数 v の番地を得る演算子(アドレス演算子),
*a += b;
の *
はポインタ変数 a が指す先の記憶領域を得る演算子(間接演算子)だ。
accumをアセンブリ言語で記述すると以下のようになる。
section .text
global accum
; 第1要素の変数に第2要素を加える.
; void accum(int *a, int b);
; @param a 変数の番地
; @param b 加える値
accum:
mov edx, [esp + 4] ; a (番地)
mov ecx, [esp + 8] ; b
add [edx], ecx ; aが指す番地にbを加える
ret
- 参考:
上述の配列の例の関数 sum も,accum と同じく第1引数は番地であり,それが指す先にはint型の値が格納されている。
実は,配列引数とポインタ引数は同一視される。
int sum(int x[], int n)
と書いてもint sum(int *x, int n)
と書いても同じだ。 配列の要素を表す式x[i]
は,実は*(x + i)
という式と同じだ。 x がポインタのとき,式x + i
は,x + i × σ 番地を表す(ただし,σ は x が指す先の型の大きさ)。 ポインタ x を,次の要素を指すように更新したい場合,C言語上ではx++;
を実行すればよい(下記の例を参照)。
/* 関数 sum をC言語で記述した場合 */
int sum(int *x, int n)
{
int s = 0;
for ( ; n > 0; n--) {
s += *x; /* xが指す先の値を加える */
x++; /* 次の要素の番地 */
}
return s;
}
構造体
構造体 (structure) は,「いくつかのデータ項目をひとまとめにしたデータ」だ。
#include <stdio.h>
/* 構造体型 Student の定義 */
struct Student {
int id; /* 学生番号 */
double gpa; /* GPA */
};
/* Student型の配列の定義 */
struct Student table[] = {
{ 101, 3.2 },
{ 102, 2.8 },
{ 103, 2.9 }
};
int main()
{
int i;
/* tableの中身を出力 */
for (i = 0; i < 3; i++) {
printf("%d, %f\n", table[i].id, table[i].gpa);
}
return 0;
}
上記の構造体型 Student は,int型のメンバ id とdouble型のメンバ gpa を持つ。
Student型の各変数や配列の要素(上記の table[i])は,それぞれこの2つのメンバを持ち,.id
, .gpa
という接尾辞を付けることでそのメンバにアクセスできる
(Javaのクラスと比較して言うと,構造体は「データメンバだけからなるクラス」と言ってもよい)。
$ gcc -m32 struct.c -- 実行形式ファイルを生成
$ ./a.out -- 実行
101, 3.200000
102, 2.800000
103, 2.900000
構造体の実体は,単に主記憶上にデータメンバを並べたものだ。 上記プログラムの実行可能ファイルの .data セクションを調べてみると以下のようになる。
$ nm a.out -- 実行形式ファイル中のラベルの値を出力
-- 中略 --
00004020 D table -- 配列 table の番地は0x00004020
$ readelf -x .data a.out -- .dataセクションの中身を16進数で出力
-- 中略 --
0x00004020 65000000 9a999999 99990940 66000000 e..........@f...
0x00004030 66666666 66660640 67000000 33333333 ffffff.@g...3333
0x00004040 33330740 33.@
配列 table の番地は 0x00004020 で,その番地から順に table[0], table[1], table[2] が並んでいる(通常の配列と同じ)。 各要素 table[i] は12バイトで,table[i].id が前半4バイト,table[i].gpa が後半8バイト(64ビットIEEE 754浮動小数点数)だ。