配列・ポインタ・構造体

配列

配列を使うプログラムを考えよう。

#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浮動小数点数)だ。

results matching ""

    No results matching ""