わさっきhb

大学(教育研究)とか ,親馬鹿とか,和歌山とか,とか,とか.

多次元配列の要素数・再考

「1つ分の大きさ」×「いくつ分」で書いていないのは,まず,2次元配列の要素数で,例えばint b[2][3];と宣言したとき,配列変数bがint型のオブジェクトを全部でいくつ持つかというと,b[0][0], b[0][1], b[0][2], b[1][0], b[1][1], b[1][2]という順でメモリに確保されるので,3×2が自然であり,2×3と書くのは「現れる順に数字をかけた」ことを意味します.

自分の「×」の使い方 - わさっき

ちょっと不気味なので,規格や本を読み直しました.多次元配列の説明の中で,かけ算表記が容易に見つかりました.

連続した添字演算子は,多次元配列の要素を指し示す.Eが次元i×j×...×kをもつn次元配列(n≧2)である場合,E(左辺値以外として用いた場合.)は,次元j×...×kをもつ(n-1)次元配列へのポインタに型変換する.単項*演算子をこのポインタに明示的に適用するか,又は暗黙に添字付けの結果として適用する場合,その結果は,指されている(n-1)次元配列となり,それ自身を左辺値以外として用いる場合,ポインタに型変換する.このことから,配列は,行優先の順序(最後の添字が最も速く変わる.)で格納する.

例 次の宣言で定義された配列オブジェクトを考える.
int x[3][5];
ここでxは,intの3×5の配列である.より正確には,xは三つの要素オブジェクトの配列であり,その各要素は,五つのintの配列である.(略)

(JIS X 3010:2003, p.51)

... Also,
static int x3d[3][5][7]
declares a static three-dimensional array of integers, with rank 3×5×7. In complete detail, x3d is an array of three items; each item is an array of five arrays; each of the latter arrays is an array of seven integers. ...
(C Programming Language (Prentice Hall Software), p.217)

(3) 4次元以上の配列の宣言とその意味
配列宣言において,次元数が増えたとしても,基本的な考え方は,(2)の考え方と同じです.たとえば,次の宣言があるとしましょう.
int z[ 2 ][ 3 ][ 4 ][ 5 ][ 6 ][ 7 ];
これは,6次元配列です.int型の要素は,演算子 [ と ] で括った添字を6つ並べて指定します.この場合,各添字が0から始まるので,z[0][0][0][0][0][0]からz[1][2][3][4][5][6]まで,2×3×4×5×6×7=5040個のint型の要素があります.配列zは,5次元配列z[0], z[1], ..., z[6]が並んだ配列と考えることもできます.6次元以上の配列の場合も,同様に考えます.
(C言語によるプログラミング―スーパーリファレンス編, pp.280-281)

本稿では,ポインタと配列の関係をn次元の場合に一般化して説明します.今,次のような次元i×j×t×...×s×kのn次元配列Eがあるとします.要素の型をTとします.
T E[i][j][t]...[s][k];*1
上記の宣言で,...が使われていますが,これは便宜上この間にいくつかの寸法の指定が入ることを示しているだけで,これをこのままプログラムとして記述すると文法エラーとなります.
(同上, p.333)

まとめると(『スーパーリファレンス編』p.334にも書かれているのですが),こうなります.T E[i][j][t]...[s][k];と宣言したら,Eは多次元配列であり,その要素数はi×j×t×...×s×kとなり,またsizeof(E)は,sizeof(T)*i*j*t*...*s*kと等しくなるということです.各成分の要素数を抜き出して,「×」でつないだら,全体の要素数を表す式となります.
なぜそのような式になるかは,ボトムアップで説明することになります.メモリ上では,「((int型オブジェクトがk個)というのがs個)…というのがi個」として格納されるので,「1つ分の大きさ」×「いくつ分」=「全体の大きさ」を繰り返し適用すると,全体の要素数はk×s×...×i,全体のメモリに占めるサイズはsizeof(T)*k*s*...*iとなるわけですが,ここは乗算の交換法則を使わせていただきまして,それぞれi×j×...×k, sizeof(T)*i*j...*kと表記する,ということです.
脱線して,例の問いから交換法則にこだわっている人のために書いておきますと,私自身は小中高大のいずれにおいても,積の可換性を教えてはいけないという立場ではありません.例の問いは,そういう交換法則を知っていて式を立てるのでは,典型的な間違え方に基づく式と一致するので,×にされたんだよという事例です.あと,トランプ配りをベースとした乗算式の立て方は,「1つ分の大きさ」×「いくつ分」=「全体の大きさ」を学ばせた直後に教えるのは混乱を招くことが想像でき,教育上適切とは思えず,あれは除算の中で(または前段として)説明するのがよいと考えます.
ボトムアップではなく,トップダウンでi×j×...×k, sizeof(T)*i*j...*kのように表記することには,メリットがあります.多次元配列の宣言があれば,文字を機械的に取り除いて乗算演算子でつなげることで,多次元配列全体の要素数も,メモリに占めるサイズも,式として表すことができます.そのように使える,「公式」です.書くときにも有用ですし,口頭でのやりとりでもいいでしょう*2.ここを,「1つ分の大きさ」×「いくつ分」=「全体の大きさ」に忠実に適用していたら,手間がかかりますし,逆順に各要素数を書き出す際に表記ミスも発生しがちです.
おまけ:

/* 234567.c */
#include <stdio.h>

#define printf_d(x) printf("%s = %d\n", #x, (x))

int main(void)
{
  int z[ 2 ][ 3 ][ 4 ][ 5 ][ 6 ][ 7 ];

  printf_d(2 * 3 * 4 * 5 * 6 * 7);
  printf_d(sizeof(int));
  printf_d(sizeof(z));
  printf_d(sizeof(int [ 2 ][ 3 ][ 4 ][ 5 ][ 6 ][ 7 ]));
  printf_d(sizeof(int) * 2 * 3 * 4 * 5 * 6 * 7);
  printf_d(sizeof(z) / sizeof(z[0]));
  printf_d(sizeof(z[0]) / sizeof(z[0][0]));
  printf_d(sizeof(z[0][0]) / sizeof(z[0][0][0]));

  return 0;
}
$ gcc -Wall -o 234567 234567.c && ./234567
2 * 3 * 4 * 5 * 6 * 7 = 5040
sizeof(int) = 4
sizeof(z) = 20160
sizeof(int [ 2 ][ 3 ][ 4 ][ 5 ][ 6 ][ 7 ]) = 20160
sizeof(int) * 2 * 3 * 4 * 5 * 6 * 7 = 20160
sizeof(z) / sizeof(z[0]) = 2
sizeof(z[0]) / sizeof(z[0][0]) = 3
sizeof(z[0][0]) / sizeof(z[0][0][0]) = 4

sizeof(z)/sizeof(int)として全体の要素数を求めるのが包含除,sizeof(z)/sizeof(z[0])としてzがいくつの5次元配列を持つかを求めるのが等分除,でいいのかな….

*1:本文には,iからsまでのの下に,下向きブレースと「n個の添字」が書かれています.しかし何がn個なのかに注意すると,このブレースはiからkまでとすべきでしょう.

*2:「お前,そんな多次元配列宣言してるけど,それで何バイトになるか分かってんのか…(かけ算の式で確認する)…ポインタの配列を使うなりして,もっと効率良ぉでけへんもんかぃのぉ?」みたいな.