配列とポインタの応用例として, 多次元配列と動的配列について, メモリマップを意識しながら理解しよう.
たとえば,3つの int 型変数からなる 1次元配列 int a[3] は, 次のように定義(宣言・初期化)される:
int a[3] = { 0, 1, 2 };
ちなみにこれは,次のような長たらしいソースと同じことでした:
int a[3]; a[0] = 0; a[1] = 1; a[2] = 2;
また,これらの配列要素 a[i](i = 0 〜 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]...ではないことに注意しよう. この多次元配列は全体的には要素数2の配列であって, その各要素が要素数3の配列となっている. ...という考え方から,Cでは, 添字(そえじ;要素番号;index)の並べ方は, 数学の「行列」と同じように, 行番号→列番号(縦方向→横方向)の順である.
そしてこの例では,各配列要素は次のように初期化されることになる: (添字の変化に注意して,上と比べてみよう.)
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;
これらの配列要素 b[i][j] (i = 0 〜 1,j = 0 〜 2) のメモリ内の配置はどうなるか?考えてみよう.
実際のメモリアドレスは1次元であるため, 2次元配列とはいっても, メモリの中では1次元的に(1列に並んで)格納されることになる. つまり,上の2次元配列 int b[2][3] は, メモリマップ的には, 次の1次元配列 int c[6] とまったく同じことになる:
int c[6] = { 0, 1, 2, 3, 4, 5 };
または:
int c[6]; // int b[2][3] c[0] = 0; // b[0][0] c[1] = 1; // b[0][1] c[2] = 2; // b[0][2] c[3] = 3; // b[1][0] c[4] = 4; // b[1][0] c[5] = 5; // b[1][0]
配列 b と c の間で要素番号の対応関係を調べてみると, 結局,W 行 H 列の2次元配列 b[H][W] の 要素 b[y][x] は, 1次元配列 c[W*H] の要素 c[y*W + x] に対応することになる. 次の図は,そのイメージである.
ところで,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次元配列の要素数については,省略できる場合でも省略せず, int func(int b[H][W], ...) と書くべき.
なお,2次元配列を1次元配列として取り扱うには, 次のようにポインタへキャストすればよい:
int b[2][3] = {{...}, {...}}; int *c; c = (int *)b; // 2次元配列 b[2][3] を1次元配列 c[6] とみなす
こうしておけば,2次元配列の要素 b[y][x] を 1次元配列の要素 c[y*3 + x] と見なして取り扱えるようになる.
これらの説明が本当であるか, 次のタスクに取り組み,確認してみよう.
上の説明の通り2次元配列 b[2][3] を 1次元配列 c[6] として取り扱えることを確かめるため, すべての要素 b[y][x] および c[y*3 + x] を表示するプログラムを作成せよ.
ソース 2d.c:
#include <stdio.h> int main(void) { int b[2][3] = {{0, 1, 2}, {3, 4, 5}}; int *c; ... c = ...; for (y = ...) { for (x = ...) { printf("b[%d][%d] = %d\t", y, x, b[y][x]); printf("c[%d*3 + %d] = %d\n", y, x, c[y*3 + x]); } } ... }
まず,次のような配列の利用方法は,ありがちな間違い. array-1.c:
#include <stdio.h> int main(void) { int n; printf("配列のサイズ > "); scanf("%d", &n); int a[n]; /* まちがい */ a[0] = 123; /* ...配列に対する何らかの処理 */ return (0); }
処理系によっては,これでも実行できる場合もある. 本科目の実験室の環境では実行できる. しかし,C言語の標準規格(本科目で対象としている ANSI C89,ISO C90)には違反している. 本実験室の環境では,この違反について,次のコンパイル方法で確認できる:
$ cc ソース.c -Wall -ansi -pedantic 0.c: 関数 ‘main’ 内: 0.c:10:9: 警告: ISO C90 は可変長の配列 ‘a’ を禁止しています ...
...残念でした. 標準Cの通常の配列は,静的配列(固定長配列)であり, 要素数を実行時には変更できない. 要素数を変えるには,ソースコードを書き換え,コンパイルし直す必要がある.
そうすると,配列を利用するプログラムの実行中に, 実際にデータを入力してみたら, 配列要素の個数が足りなくなってしまった...ということはありえる話だ. C言語では,配列要素の個数をプログラムの実行中に簡単には変えられないので, とりあえずの解決策としては,充分に大きな配列を用意しておき, その一部だけを使うことになるだろう. array-2.c:(見せかけだけの可変長配列)
#include <stdio.h> #define SIZE 1000000 /* かなり大きめに設定しておく */ int main(void) { int a[SIZE]; /* 全部で SIZE個の要素を用意 */ int n; printf("使いたい配列のサイズ > "); scanf("%d", &n); if (n >= SIZE) { perror("多過ぎ!!"); return (1); } ... /* SIZE個の内 n個だけを使用する何らかの処理 */ }
しかしこれでは, 有限なメモリ資源を無駄使いすることになってしまうだろう. また,これでも要素数が不足してしまう場合もあるだろう.
標準C言語で動的配列(可変長配列)を実現するには, 次のようなメモリ管理用ライブラリ関数を利用しよう:
戻り値は,確保された領域の先頭アドレスとなる. ただし,確保に失敗した場合には NULL となる.
かなりテキトーな使用例:(このままでは不完全・不適切...)
char *s; s = malloc(sizeof(char)*100); /* char s[100]; とほぼ同じこと */ if (s == NULL) goto ERROR;
かなりテキトーな使用例:( 〃 )
int *a; a = calloc(100, sizeof(int)); /* int a[100] = {}; とほぼ同じこと */ if (a == NULL) goto ERROR;
かなりテキトーな使用例:( 〃 )
a = malloc(...); ... free(a);
メモリの解放をしないまま,確保だけを繰り返して行くと, いつかメモリ不足に陥って,そのプログラムを中断しなければならなくなってしまう. さらに,他のアプリやシステムの動作にも影響を及ぼす場合もある. この現象は,メモリリーク(memory leak)と呼ばれており, よくある(しかも重大な)バグである. 不要になったメモリについては必ず free( ) せよ!!
これらのライブラリ関数は, ヘッダファイル stdlib.h の中で プロトタイプ宣言されている. 詳しくは,教科書 pp.253-254 を参照しよう.
なお,メモリ領域のデータサイズの指定には通常, sizeof 演算子が利用される:
結局,次のように記述するのが正解となる. array-3.c:
#include <stdio.h> #include <stdlib.h> int main(void) { int n; int *a; printf("配列のサイズ > "); scanf("%d", &n); a = (int *)malloc(sizeof(int)*n); /* int a[n] のメモリ領域を確保 */ if (a == NULL) return (1); /* 確保できなかった場合のエラー処理 */ ... free(a); /* 使い終わったらメモリ領域を解放 */ return (0); }
固定配列では変数の宣言時にメモリ領域が確保されていた.
一方,動的配列ではポインタの宣言時には配列の実体はまだ存在せず, malloc() の実行時に配列のメモリ領域が確保される.
なお,malloc( ) と calloc( ) では, この例の (int *) のように, 戻り値の型を適切にキャストしてやる必要がある. 面倒ではあるが,その代わり, これらの関数では,あらゆる型の動的配列に適用できるようになっている.
また,プログラム終了時には自動的に free( ) してもらえるが..., コンピュータ任せにはせず,プログラマが意識的に free( ) すべきだ. 特に,長期間に渡って異常終了せずに連続動作すべきシステム (例:銀行,通信,等)では,非常に重要だ. タスク2でこの関数の重要性を確認しよう.
なお,malloc( ) と free( ) の関係は,ちょうど, fopen( ) と fclose( ) の関係と同様である. 要するに,前処理(準備)と 後処理(片付け)だ.
前処理については, 準備しなければ本処理を実行できない訳で, 省略するとプログラムは動かない. 準備は絶対に必要だ.
一方,後処理については,本処理が実行できた後のことなので, 省略しても動いてしまう訳で,片付けは不要では? いや,その考えは甘え. 本処理が1回だけの場合なら後処理なしでも動くが, 複数回の場合にはうまく行かないだろう. 後処理は次回の処理の準備のための準備として必要. 片付けを怠らないこと!!
とにかく 1 MB ずつのメモリ確保 (char *)malloc(1024*1024) を free( ) せずに繰り返すプログラム malloc.c を作成し, 手元のコンピュータで確保可能なメモリ容量の最大値を確認せよ.
ヒント: わざと間違えてメモリリークを発生させてみる問題. 前期に試した fclose( ) せずに fopen( ) を繰り返すプログラム fclose1.c を参考にしよう.
誤解しないこと. 「malloc( ) できたメモリ容量 = コンピュータのメモリ容量」 ではない.
なお,一体どっちなんだー?とか,気にしないでよい. 確かなのは,メモリ容量には制限がある(=無限ではない)ということだけだ. とにかく,malloc( ) したら, 最後に必ず,free( ) せよ!!
さらに,静的配列では何MB まで使用可能か? たとえば,char a[1024*1024*...]; などとして探ってみよう. 大きなメモリ領域を使いたい場合にも, 動的配列が有効だとわかるだろう.
Tic-tac-toe(三目並べゲーム)について, 基本プログラムのソース ttt.c をダウンロードし,これを元にして, ゲーム盤を任意の大きさ n × n に変更できるように拡張せよ. ただし,標準C言語(ANSI C89)の規則に従うこと.
また,余裕がある人は,勝敗判定や人工知能(PC対ユーザ)などの処理を追加し, ゲームとしての完成度を高めてみよう.
質問 Q1〜Q5 に回答し,電子メールで提出せよ.
メールの送信形式を必ず「テキスト形式 or プレーンテキスト」>に変更せよ.