多次元配列は,2次元,3次元,あるいはもっと多い次元で表わされる情報を処理するのに有用ですが,そんな配列を,ポインタを使ってどのようにアクセスすればよいかについては,人によって好みが分かれるように思えます.さらに,プログラミング授業でどのように教えるべきかになると,また厄介な問題です.ここに整理を試みてみました.
配列のポインタか,ポインタの配列か,1次元配列か
2次元配列に限定するとして,それをポインタでアクセスする方法には大きく分けて次の3つがあります.
1. 配列のポインタを使う.
int a[2][2]; int (*b)[2] = a;
2. ポインタの配列を使う.
int a[2][2]; int *b[2] = {a[0], a[1]}; /* さらに,int **c = b; とできる */
3. (1次元の)配列とみなしてアクセスする.
int a[2][2]; int *b = &a[0][0]; /* 右辺は a[0] あるいは (int *)a でもよい */ /* b[i * 2 + j] と a[i][j] は同じ */
1と2の決定的な違いを見抜けますでしょうか? 1のbはポインタ,2のは配列ですので,1のほうはb++とかできるけど,2のにはできない,というのはたしかにそうです.
しかしもっと重要な違いがあります.
1のbについて,b[0]は配列型です.なのでb[0]=なんとかという代入はできませんし,その一方で,sizeof(b[0])は,要素数が2のint配列のサイズを教えてくれます.ポインタ変数bに,適切な2次元配列のポインタ値を割り当てると,b[0]とb[1]は,bの型と値によって決まる,その2次元配列領域内のしかるべき(1次元)配列領域に対応します.型でいうと,b[0]もb[1]も,int [2]型になるので,いくつかの例外を除いて,式の中ではint *型つまりポインタ型になります.したがってb[0][0]やb[0][1]などは,2次元配列内の適切なint型要素にアクセス(参照も代入も)できることになります.
次に2のbについて,b[0]はポインタ型です.b[0]には,int *型になるようなポインタ値を代入できます.つまりポインタの指し示すところを,b[0]とb[1]とで別のものにすることもできるということです.b[0]の指し示す領域,b[1]の指し示す領域のサイズを異なるものにしても,差し支えありません.
結果を表にしてみます.(6月1日: 誤りを訂正しました.)
変数宣言 | bの型 |
bに代入 | b[0]に代入 | b[0][0]に代入 | sizeof(b) | sizeof(b[0]) | sizeof(b[0][0]) |
int (*b)[2]=a; | int (*)[2],すなわちポインタ | できる | できない | できる | sizeof(void *)と等しい | sizeof(a[0])と等しい | sizeof(int)と等しい |
int *b[2]={a[0], a[1]}; | int *[2],すなわち配列 | できない | できる | できる | sizeof(void *)*2と等しい | sizeof(void *)と等しい | sizeof(int)と等しい |
3は,1や2とまったく違うやり方です.これでうまくいくのは,2次元配列も配列なので,要素が連続してメモリ上に格納されるから,もっと言うと,上の2次元配列aにおいては,a[0][0],a[0][1],a[1][0],a[1][1]が連続して並ぶので,「((int *)a)[2]」は「a[1][0]」と同一視できるからです.
授業では
私が担当している1年生向け科目では,授業中に1を取り上げています.
2次元配列とポインタのポインタが異なることを説明するためですが,ほかに,関数で2次元配列を受け渡す際,仮引数をint a[2][2]のように書けることの根拠になります.
プログラムの例題として取り上げるかどうかは年度によりまして,昨年度は,取り上げていませんでした.
2のような宣言の仕方を,授業で取り上げた記憶はありません.それなりに実用的なデータ構造だと思いますが,このように定義してアクセスする例は,2年以降に委ねるべきかと考えます.
とはいいつつも,2のデータ構造があることは,argvを使ってコマンドライン引数を順に取り出すプログラムの中で,説明しています.
3は,授業では説明せず,昨年度のレポート課題(冬休みの宿題)で出してみました.ただし,値の格納されている配列も1次元で,それを2次元的にアクセスしています.
1〜3のどれを好むか,またどれを初学者に教えるかは,プログラマ/指導者次第だと思います.個人的には,2は,データ構造に関連して学ぶべきで,3は古臭いと思っていますが.
関連
- レポート課題を振り返る
- 2007年11月5日〜8日
- ポインタのポインタの使い道
- Cの横道,指導の真髄
- ここが変だよC言語 (続き)
- 「char *型」と言えば簡単なのに
- ポインタをどう教えるか
- 『なぞりがきC言語学習ドリル』*1のp.127に,「2次元配列を直線的(1次元配列的)に参照する」という項目とコード例があります.
*1:そのうち書評します.