• 検索結果がありません。

多次元配列の定義

ドキュメント内 note.dvi (ページ 127-132)

6.16 配列とポインタ(その3・多次元配列)

6.16.1 多次元配列の定義

はじめに多次元配列の定義を行う.

int str[2][5] ;

配列を構成する演算子[]の結合規則は「左から右」であるので,str[i]は,int型の5個の要素を持つ 配列であり, strは2個の要素を持つ「int型の5個の要素を持つ配列」の配列である. 各種の教科書に は, 以下のような図が書かれていることが多い.

str[0][0] str[0][1] str[0][2] str[0][3] str[0][4]

str[1][0] str[1][1] str[1][2] str[1][3] str[1][4]

より正確にメモリ上でstrが表す2次元配列の様子を見ると,次の図のようになる.

str[0] str[1]

str[0][0] str[0][1] str[0][2] str[0][3] str[0][4] str[1][0] str[1][1] str[1][2] str[1][3] str[1][4]

多次元配列を初期化する方法は, char daytab[2][13] = {

{0,31,28,31,30,31,30,31,31,30,31,30,31}, {0,31,29,31,30,31,30,31,31,30,31,30,31}

} ;

とすれば良い.

Example 6.16.1 次の例は,N×N,N = 10の単位行列を作成している.

int i, j ;

int unit_mat[10][10] ; for(i=0;i<10;i++) {

for(j=0;j<10;j++) unit_mat[i][j] = 0 ; unit_mat[i][i] = 1 ;

}

Remark 6.16.1 多次元配列は,文法的には配列を識別子の型とする配列である. 配列の宣言では,「配列 の限界を指定する定数式がないときには,配列は不完全な型を持つ。」とあり, 配列の要素の型は完全でな ければならない. これは,多次元配列においては,最初の次元のみが省略できることを意味している. すな わち,明示的な初期化により配列を完全にすることが可能なので,

int y[][2] = { {1,2}, {2,3}, {3,4} } ;

により,この配列はint型の2つの要素を持つ配列の3つの要素を持つ配列として定義される. また,この 初期化は

int y[3][2] = { 1,2,2,3,3,4 } ; と等価である.

Remark 6.16.2 また, 次のような配列の初期化子による定義も可能である.

int y[][2] = { {1,2}, {2}, {3} } ;

この定義では,y[1],y[2]は初期化子には1つの要素しか持たないが,y[0]が2つの要素を持つため,y[1], y[2]は2つの要素を持つ配列として定義され,yは不完全な型を持つ配列となる.

しかし,これを

int y[][] = { {1,2}, {2}, {3} } ;

とは定義できない. この理由は,次のSection 6.16.2の多重配列をポインタで書換えることと関連している.

Remark 6.16.1, Remark 6.16.2の詳細については, [2, A8.6, A8.7]を参照.

6.16.2 多重配列とポインタ

1次元配列では,配列とポインタは,ほぼ同じものを示していた. すなわち, int a[3] ;

として与えられたオブジェクトに対して,a[0],*aまたはa[i],*(a+i)は,それぞれ,同じメモリ領域へ のアクセスを表し,a,&a[0]またはa+i,&a[i]も,それぞれ,同じアドレスを指し示していた. ここでは多 重配列においては,配列とポインタは(このような意味で)同じものと見なせるかどうかを考えてみよう.

はじめに,二重配列 int a[2][5] ;

を考えてみる. この時[2, A8.6.2]によれば,a[0][0],**a,またはa[i][j],*(*(a+i)+j),*(a[i]+j)は, それぞれ, 同じ領域へのアクセスを示し,a,&a[0][0],または*(a+i)+j,a[i]+j,&a[i][j]も,それぞれ 同じアドレスを示す. このことを

a[0] a[1]

a[0][0] a[0][1] a[0][2] a[0][3] a[0][4] a[1][0] a[1][1] a[1][2] a[1][3] a[1][4]

を用いて考えてみよう.

これを理解するには,次の2つのポインタ演算の差を理解する必要がある.

1. aに対して a+1が何を表すか?

2. a[0] に対してa[0]+1が何を表すか?

これに対する解答のヒントとして, sizeof(a)

sizeof(a[0]) sizeof(a[0][0])

の式の値を見てみるのがよい. これをint型が4バイトの処理系で調べると,順に40,20,4という答えが 得られる.

上の図を見ると,aは「int型の5個の要素を持つ配列」という型の配列であるので,a+iはaから「int 型の5個の要素を持つ配列」のバイト数(20バイト)のi倍だけ先を表す. つまり,*(a+i)はa[i] と 等価である.

さらに,a[0]は int型の要素を持つ配列であるので, a[0]+jはa[0] からint型のバイト数(4バイ ト)のj倍だけ先を表す. つまり, *(a[0]+j)は a[0][j]と等価である.

したがって, a[i][j] は *(a[i]+j) と等価であり, *(*(a+i)+j) と等価であることがわかる. 当然,

*(a+i)+jはa[i][j]のアドレスを示すポインタとなる.

このことから,

int y[][] = {{1,2}, {2}, {3}} ; int x[][] = {{1,2}, {2,3}, {3,4}} ;

という定義では,配列y[i]に対するインクリメントy[i]+1のインクリメントのバイト数が計算できない ことになり,このような定義が認められないことがわかる.

同様に,3次元以上の配列も int a[2][5][10] ;

と定義できる. 2次元配列と同様に, 3次元以上の場合も以下の参照はすべて同じものとなる.

a[i][j][k],*(a[i][j]+k),*(*(a[i]+j)+k),*(*(*(a+i)+j)+k).

&a[i][j][k],a[i][j]+k, *(a[i]+j)+k,*(*(a+i)+j)+k.

したがって,適切な初期化子をおくことにより, int a[][5][10] = {{

{0,1,2,3,4,5,6,7,8,9}, {1,2,3,4,5,6,7,8,9,0}, {2,3,4,5,6,7,8,9,0,1}, {3,4,5,6,7,8,9,0,1,2}, {4,5,6,7,8,9,0,1,2,3}}, {

{1,2,3,4,5,6,7,8,9,0}, {2,3,4,5,6,7,8,9,0,1}, {3,4,5,6,7,8,9,0,1,2}, {4,5,6,7,8,9,0,1,2,3}, {5,6,7,8,9,0,1,2,3,4}}} ; という定義が可能である.

6.16.3 ポインタの配列と二重ポインタ

二重配列と似た変数の定義には, int *b[2] ;

int **c が考えられる.

6.16.3.1 ポインタの配列

Section 6.14.3,および複雑な宣言をまとめた表(p. 342)で述べた通り,int *b[2]は“intへのポイン

タの2個の要素からなる配列”であるので, int a0, a1 ;

int *b[2] = {&a0, &a1} ;

とすることにより,2つのint型のオブジェクトを指し示すことができる. もし, int a0[5], a1[5] ;

int *b[2] = {a0, a1} ;

とすると, b[0] は配列 a0の先頭アドレスを示し, b[1] は配列a1の先頭アドレスを示す. したがって,

*(b+i),b[i]がともに同じアドレスを指し示しているので,*(*(b+i)+j),*(b[i]+j)は,結果として,同 じアドレスを指し示すこととなる. さらに,b[i]が配列の先頭を指し示すポインタであることを考えると,

*(b[i]+j)は b[i][j]と書換えることができ,b[i][j]も *(b[i]+j)と同じ領域へのアクセスを示すこ とになる86.

しかし, int *b[2] = {{1,2,3,4,5},{2,3,4,5,6}} ; とは初期化できない. なぜなら,この変数定義 では,これらの値を格納するメモリ領域が確保できないからである. 定義*b[2]で確保される記憶領域は, 他の変数を示すアドレス2個分に過ぎないことに注意しよう.

b[0]

b[1]

*a0 ja1

6.16.3.2 二重ポインタ

Cではどのような型のオブジェクトへのポインタも利用可能であるので, 「ポインタへのポインタ」が 利用できる. それは,

int **c ;

と定義する. このようなポインタへのポインタ(二重ポインタ)は, int a ;

int *b ; int **c ;

と定義されている時, b = &a ; c = &b ;

とすれば,**cによって,aの値を参照可能になる.

もちろん,単純にこんな利用法をするためにポインタへのポインタをつくる必要はなく,実際に利用する 場面は,「配列へのポインタ」をポイントするために用いる. すなわち,

int a0[5], a1[5] ; int *b[2] = {a0, a1} ; int **c = b ;

86理解しにくい場合には,int型の変数へのポインタpを用意し,p = b[i]と考えると良い.

とすれば,cには配列bの先頭のアドレスが格納される. したがって,c+1はint型のポインタの配列(ポ インタ)の意味で,インクリメントが行われるので,*(c+i)はb[i]と等価なアクセスを実現する. すなわ ち, この定義では,

*(*(c+i)+j),*(c[i]+j), c[i][j]は同じ参照を表し,

*(c+i)+j,c[i]+j,&c[i][j]は同じアドレスを表す.

6.16.3.3 配列へのポインタ

ポインタの配列と混乱をおこし易いものに,「配列へのポインタ」がある.

int (*b)[3] ;

と定義すると,bは先に間接演算子*と結合するので,「ポインタ」となり,「その指し示す先がint [3]

」と読むことができる. よって,int (*b)[3] は 「int型の3個の要素からなる配列へのポインタ」とな る. この時,

int a[3] = {1,2,3} ; int (*b)[3] ;

b = &a ;

とすることにより,(*b)[i]または(*b)+iは a[i]と等価なアクセスを実現する.

前に関数の戻り値の型として配列を返すことは出来ないと書いたが, 配列へのポインタを利用すること により,類似のことを行うことは可能である.

Example 6.16.2 int型の3個の要素からなる配列へのポインタを返す関数.

#include <stdio.h>

int (*foo(int n))[3]

{

static int b[3] ; int i ;

for(i=0;i<3;i++) b[i] = n+i ; return &b ;

}

int main(int argc, char **argv) {

int (*a)[3] ; int i ; a = foo(1) ; for(i=0;i<3;i++)

printf("%p\n", (*a)[i]) ; return 0 ;

}

この関数fooでは,実際に値を代入する配列 bは static宣言されている. これは, 戻り値の値(ポイン タ)が指し示す先を, 関数終了後も保持するためである.

ドキュメント内 note.dvi (ページ 127-132)