09 月 26 日(月)

高度な配列操作

前回の課題では, 画像データの記録のために説明なしに2次元配列を利用した. 今回は,配列のちょっと高度な利用方法について勉強しよう.


多次元配列

まずは基本の1次元配列

たとえば,3つの int 型変数からなる 1次元配列 int a[3] は, 次のように定義される:

int a[3] = { 0, 1, 2 };

ちなみにこれは,次のような長たらしいソースと同じことだった:

int a[3];
a[0] = 0;
a[1] = 1;
a[2] = 2;

配列の配列で多次元配列

Cでは,配列の配列(2次元配列)とか, さらにその配列(3次元配列)などの多次元配列も定義できる. たとえば,3つint 変数からなる1次元配列が さらに2つ集まってできた2次元配列 int b[2][3] は,次のように定義される:

int b[2][3] = {
	{ 0, 1, 2 },
	{ 3, 4, 5 },
};

ここで,○[3] が2つだからといって, ○[3][2] ではないことに注意しよう. 添字(そえじ;要素番号)の並べ方は, 数学の「行列」と同じように, 行番号,列番号の順(縦方向,横方向の順)である.

なお,わざわざ説明するまでもないかもしれないが, この例では,配列の各要素は次のように初期化されることになる: (添字の変化に注意して,上と比べてみよう.)

int b[2][3];
b[0][0] = 0;	// 0 行目開始
b[0][1] = 1;
b[0][2] = 2;
b[1][0] = 3;	// 1 行目開始
b[1][1] = 4;
b[1][2] = 5;

多次元配列のメモリマップ(多次元配列と1次元配列の相互変換)

ところで,2次元配列とはいっても, 実際のメモリアドレスは1次元なので, メモリの中では1次元的に(1列に並んで)格納されることになる. つまり,上の2次元配列 int b[2][3] は, メモリマップ的には, 次の1次元配列 int c[6] とまったく同じことになる:

int c[6] = { 0, 1, 2, 3, 4, 5 };

または:

int c[6];
c[0] = 0;
c[1] = 1;
c[2] = 2;
c[3] = 3;
c[4] = 4;
c[5] = 5;
ここで,配列要素の格納順序に注意しよう. 配列 b[2][3] と配列 c[6] の初期化コードを比較せよ.

配列 bc の 要素番号の対応関係を調べてみると, 結局,2次元配列 b[H][W] の要素 b[y][x] は, 1次元配列 c[W*H] の要素 c[y*W + x] に対応することになる. Fig.1 は,そのイメージ図だ. このことを下の練習問題 (1) で確認する.

Fig.1. 2次元配列(H行W列)とメモリマップ(1次元配列 W×H要素)

ところで,1次元配列 a[N] では, 要素数 N を省略できる場合があった. int a[] = { ... } とか, int func(int a[]) のように.

ところが,2次元配列 b[H][W] の場合, 横方向の要素数 W は, 各要素のアドレスの計算(y*W + x)に必要となるので, 省略できない. つまり,int func(int b[][]) とするのは間違い.

ただし,縦方向の要素数 H については省略してよい. つまり,最低限, int func(int b[][W]) などとしなければならない だがこれでは,非対称で不格好だ.

なお,2次元配列を1次元配列として取り扱うには, 次のようにポインタへキャストすればよい:

int b[2][3] = {{...}};
int *c;

c = (int *)b;

こうしておけば,2次元配列の要素 b[y][x] を 1次元配列の要素 c[y*3 + x] と見なして取り扱えるようになる.

ポインタと配列の関係について思い出そう.

練習問題 (1)

上の説明の通り2次元配列 b[2][3] を 1次元配列 c[6] として取り扱えることを確かめるため, すべての要素 b[y][x] および c[y*3 + x] を表示するプログラムを作成せよ.

ヒント:すべての要素について,たとえば次のように表示すればよい. (二重ループで xy を変化しながら, b[y][x] 等を表示する.)

int b[.][.] = {{...}};
int *c;
...
for (y = ...) {
	for (x = ...) {
		printf("b[%d][%d] = %d ,   ", y, x, b[y][x]);
		printf("c[%d*3 + %d] = %d\n", y, x, c[y*3 + x]);
	}
}

動的配列

配列を利用するプログラムの実行中に, 実際にデータを入力してみたら, 配列要素の個数が足りなくなってしまった...ということはありえる話だ. Cでは,配列要素の個数をプログラムの実行中に簡単には変えられないので, とりあえずの解決策としては,充分に大きな配列を用意しておき, その一部だけを使うことになる. しかしこれでは, 有限なメモリ資源を無駄使いすることになってしまうし, それでもやはり,足りなくなってしまうこともあるだろう. そこで登場するのが次のようなメモリ管理関数である:

これらの関数は,ヘッダファイル stdlib.h の中で プロトタイプ宣言されている. 詳しくは,K&R の pp.203-204 を参照しよう.

メモリ領域のデータサイズの指定には通常, sizeof 演算子が利用される:

malloc( ) などで確保されたメモリ領域は, 配列と同じように使える上, 要素数を実行時に変えられるので, 動的配列(可変長配列) と呼ばれている. 一方,普通の配列は 静的配列(固定長配列) である.

動的配列と静的配列の違いの実例を示しておく. まず,次のような配列宣言は間違い:

int  n;

printf("配列のサイズ > "); 
scanf("%d", &n);

int a[n];	// まちがい

これは,処理系によっては,エラーにならない場合もあるが, C言語の標準規格に違反している. 次のコンパイル方法で確認できる:

$ cc ソース.c -Wall -pedantic

次のようにするのが正解:

int  n;
int *p;

printf("配列のサイズ > "); 
scanf("%d", &n);

p = (int *)malloc(sizeof(int)*n);	// int p[n] のメモリ領域を確保
if (p == NULL) ...			// 確保できなかった場合のエラー処理

.
.	 // 配列 p に関する処理 
.

free(p);				// 使い終わったらメモリ領域を解放

固定配列では変数の宣言時にメモリ領域が確保されていた.

一方,動的配列ではポインタの宣言時には配列の実体はまだ存在せず, malloc() の実行時に配列のメモリ領域が確保される.

なお,malloc( )calloc( ) では, 上の例の (int *) ように戻り値の型を適切にキャストしてやる必要がある.


練習問題 (2)

1 MB ずつのメモリ確保 (char *)malloc(1024*1024)free( ) せずに繰り返すプログラムを作成し, 手元のコンピュータで確保可能なメモリ容量の最大値を確認せよ.

1 MB(メガバイト) = 1 KB × 1 KB, 1 KB(キロバイト)= 1024 バイト.

ヒント: わざと間違えてメモリリークを発生させてみる問題. 前期にやった no-fclose.cfclose( ) せずに fopen( ) を繰り返すプログラム)も参考に.

誤解しないこと. 「malloc( ) できたメモリ容量 コンピュータのメモリ容量」 ではない.

なお,一体どっちなんだー?とか,気にしないでよい. 確かなのは,メモリ容量には制限がある(=無限ではない)ということだけだ. malloc( ) したら,最後に必ず,free( ) すること.


練習問題 (3)

Tic-tac-too の基本プログラムを元にして, ゲーム盤を任意の大きさ n × n に変えられるように拡張せよ. ただし,動的配列を使うこと.

また,余裕がある人は,勝敗判定などの処理を追加し, ゲームとしての体裁を整えてみよう.


(c) 2016, yanagawa@kushiro-ct.ac.jp