2次元配列


普通の2次元配列

まず,静的に配列を確保する場合,m 行 n 列の要素をもつ2次元配列 d は,

double d[m][n];

として宣言する(d[m,n]ではないことに注意).この場合,d は,「要素数n個の配列」を要素に持つ配列,となる.
つまり d[0] 自体が,n 個の要素を持つ配列であり,その n 個目の要素に対して d[0][n] としてアクセスするということになる.<p>

この配列を関数の引数として渡すためには,

void foo(double (*d)[n]);

あるいは

void foo(double d[][n]);

というように,「要素数n個の配列」を要素に持つ配列ですよ,として渡す必要がある$n$ の部分には具体的な数字が入る.変数ではダメ).

また,動的に確保するためには,同様にして,

double (*d)[n];
d = (double (*)[n])calloc( n * m, sizeof(double) );

として確保することになる(上と同様, n の部分には具体的な数字が入る.変数ではダメ). すなわち,まず,「n 個の要素を持つ配列,へのポインタ」を定義しておき,「n 個の要素を持つ配列」× m 個のメモリを確保する.

これよりわかるように,通常の2次元配列は,列数 n があらかじめわかっていないと,引数として渡すことも,メモリを動的確保することもできない.


行数,列数ともに未知の配列

では,行数,列数ともに未知の場合には,どうすればよいかというと,

のいずれかにより処理することになる.

1番目のケースは,本文中にあるように,2次元配列として確保するかわりに,m x n 個の要素をもった1次元配列を確保する,というものである.この場合は,i 行 j 列の要素に対して,d[n*(i-1)+(j-1)] としてアクセスすることになる.<p>

2番目のケースは具体的には次のようなものである.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
double **matrix_alloc(int row, column) {
    double **a;	/* もしくは,double *a[] */
    int i;

    a = (double**)calloc(row, sizeof(double*));
    if (a == NULL) {
        fprintf(stderr, "Cannot allocate memory.\n");
        exit(1);
    }

    for (i = 0; i < row; i++) {
        a[i] = (double *)calloc(column, sizeof(double));
        if (a[i] == NULL) {
            fprintf(stderr, "Cannot allocate memory.\n");
            exit(1);
        }
    }

    return a;
}

5行目の a = (double**)calloc(row, sizeof(double*)) で,double配列へのポインタとして,row 個の要素をもった double ** 型の配列が確保される.次に,12行目の a[i] = (double *)calloc(column, sizeof(double)) で,要素数 column の double 型配列が確保され,そのアドレスが,先に確保したポインタ配列 a に代入される.

別の言い方をすれば,m×nの行列を確保する場合,各行ごとに,要素数 m 個の配列を動的確保する(n 行あるので,n 回確保する).そして,これら n 個の行を一つに束ねるために,「要素数 m 個の配列」を n個分,要素として持つ配列,を動的に確保する(実際のプログラムでは手順が逆だけど)という作業になる.

この方式の場合,メモリ上には,要素 m 個の配列が n 個と,それらへのポインタを要素に持つ要素数 n 個の配列とが,確保されることになる.そのため,配列の大きさ次第では,メモリの無駄が大きい.しかし,通常の2次元配列の場合と同様に d[i-1][j-1] としてアクセスすることが可能である.


メモリの解放

動的に確保された2次元配列を解放するときは,メモリを確保したときと,逆の手順で一つずつ解放する必要がある.すなわち,

という手順が必要である.解放の方法は以下のコードを参考にすること.

void matrix_free(double **mat, int rows, int column) {
    int i;
    for (i = 0; i < rows; i++) {
        free(mat[i]);
    }
    free(mat);
}

2次元配列と1次元配列,どっちが良い?

構造体等で専用のタイプを設計し,それに付随する関数/マクロ群を定義してしまうのであれば,結局,どちらのデータ構造を使っても使い勝手には大差はない. ただし,現代的なコンピュータでは一般に1次元配列を使う方が全体的な計算効率が良くなる傾向になる.

安直に考えれば,1次元配列を使う方法で要素を参照する際,番地を得るために乗算 + 加算を行うのに対し, 本付録で示した2次元配列では,間接参照が1回行われるだけである.そのため2次元配列の方が,一般に高速に動作するように感じる. しかしながら,現在のコンピュータは演算速度が十分に早く,メモリ参照のコストの方が相対的に大きくなっている. 特に1次元配列は全ての要素が連続したメモリに確保されるため,順々に要素にアクセスするような一般的な使い方において, メモリ領域が断片的になっている2次元配列よりも高速に動作する.

また,2次元配列では,ポインタの配列1個分の場所が余分に必要になるため,1次元配列より多くのメモリを必要とする.これらを踏まえると,2次元配列を使うメリットは要素の書き方が直感的である以外には,ほとんどないことになる.