配列は添え字を使って複数の変数を効率よく扱えると前々回で説明しました。ただ、この場合は添え字で単純な位置を指定することしかできません。今回は、その添え字の指定を拡張して使用する多次元配列を使ってみましょう。
- 添え字を複数使用する
突然ですが RPG ロールプレイングゲームってご存じですよね?ゲームでは、広いマップに野原や山や町などが存在しています。これを今まで学んだ配列で処理しようとすると、少し面倒なことになります。例えば、マップとして下記の仕様を策定してみます。
横幅 W = 100 縦幅 H = 60 0 = 野原 1 = 森林 2 = 山間 3 = 海 4 = 洞窟 5 = 城下町
マップのエリアは 100×60 = 6000 になります。配列でこのサイズを指定することはできます。例えば、下記のような記述とします。
enum EMapStatus { FIELD, FOREST, MOUNTAIN, SEA, DUNGEON, TOWN }; const int c_mapWidth = 100; const int c_mapHeight = 60; EMapStatus map[c_mapWidth * c_mapHeight] = { FIELD };
新しい書き方が出てきました。const です。これは宣言した変数を読み取り専用として定数的に扱えるようにした修飾子となります。const 指定をしておけば、通常は書き込みができなくなります。将来的に C++ や C# も使ってみたいというのであれば、定数は #define ではなく const で慣れておくとよろしいかと思います。
配列の要素数指定は定数じゃないとエラーになります。試しに上記のプログラムから const を取り除くとエラーになりコンパイルできなくなります。変数で配列を取得したい場合もあるかとおもいますが、それはまだ先に説明します。
数値に意味を持たせるのであれば、enum 列挙体が便利です・・・とかなり横道に逸れましたね😓話を戻します。配列でマップを用意したはよいのですが、どの場所がどういう地形かを調べるのは、ちょっと面倒なことになります。例えば、現在のプレイヤーが X,Y の位置にいるとしたら、その場所を調べるためには
EMapStatus status = map[x + y * c_mapWidth];
こんな書き方にななります。ちゃんと動きますし、その意味では問題はないのですが、直感的ではないですよね。そこで登場するのが多次元配列です。多次元というぐらいなので、2次元でも3次元でも現実にはあり得ない4次元でも宣言することができます。マップは2次元なので、それを C言語で表現するのなら
EMapStatus map[c_mapWidth][c_mapHeight] = { FIELD };
このような記述となります。何のことはない [] を次元数に合わせて増やすだけです。これが2次元配列です。X,Y からマップの情報を取得しようと思ったら
EMapStatus status = map[x][y];
このように記述するだけです。どうでしょう?こちらのほうが感覚的に感じませんか?これは、map[y] の配列を [x] 個分持っているという宣言になります。宇宙空間のような場所の場合であれば、座標系は X,Y,Z となりますので、3次元配列が良いと思います。その場合も・・・
enum ESpace { NONE, STAR, BASE, ENEMY, PLAYER }; const int c_spaceWidth = 100; const int c_spaceHeight = 100; const int c_spaceDepth = 100; ESpace space[c_spaceWidth][c_spaceHeight][c_spaceDepth] = { NONE };
こんな書き方で出来てしまいます。
※ ネットに接続しなくてもローカルのHDDだけでバージョン管理出来るって知ってました?それを使うだけでもいつでも変更を元に戻せるメリットがあります。これを読めば使い方を理解できると思います。
- 多次元配列の初期化
既に上記で書いてしまってますが、多次元配列でも初期化子を与えることで、配列が確保される時点でその内容を初期化することが出来ます。また、正しい初期化子を与えることで、要素数の記述も省略することが出来ます。試してみましょう。
enum EMapStatus { FIELD, FOREST, MOUNTAIN, SEA, DUNGEON, TOWN }; EMapStatus map[][4] = { { MOUNTAIN, MOUNTAIN, MOUNTAIN, MOUNTAIN, }, { MOUNTAIN, FOREST, FOREST, DUNGEON, }, { MOUNTAIN, FOREST, FIELD, MOUNTAIN, }, { MOUNTAIN, TOWN, FIELD, MOUNTAIN, }, { MOUNTAIN, SEA, SEA, SEA, }, }
さきほど、初期化子を与えると省略できると説明しましたが、実は C言語では省略できるのは最上位の要素数だけです。それ以外の要素数の省略は出来ないのです。少し不便ですよね・・・。これで、初期化した場合に TOWN はどこにあるかわかりますでしょうか?
シンキングターイム!
シンキングターイム!
:
:
:
:
:
:
:
ピッピー!答え合わせです。一見、map[1][3] の位置にあるように思えますが、この場所にあるのは TOWN ではなく DUNGEON です。正解は map[3][1] の場所となります。

これは先ほどからしつこく説明しているとおり、二次元配列は(一次元)配列の集まりなのです。内側の {} が複数集まっているので、[X][Y] という並びではなく [Y][X] となっているのです。

これは先ほどからしつこく説明しているとおり、二次元配列は(一次元)配列の集まりなのです。内側の {} が複数集まっているので、[X][Y] という並びではなく [Y][X] となっているのです。
[X][Y] で配列を扱いたい場合は初期化子を Y の集まりとして記述しないといけません。ただ、その書き方はソースコード上ではとても見づらいです。そのため、初期化子でデータを与える記述とするならば、[Y][X] の並びに慣れるのが良いと思います。
※ そういえば、printf の LOCATE も y,x という並びでしたよね。
※ そういえば、printf の LOCATE も y,x という並びでしたよね。
配列の初期化子が要素数指定より足りなかった場合は、0 でクリアされるのは通常の配列と同じです。初期化子の数が配列の要素数指定より多い場合はコンパイルエラーとなります。あと、後方にカンマが付いてても、余分な配列変数確保は行われません。最後尾のカンマは無視されます。
- 文字列を複数扱う
文字を複数集めると文字列となります。その文字列をさらにまとめる事も出来ます。配列の配列になりますので、これは二次元配列となります。試してみます。
char messages[][20] = { "Battle City", "Dragon Buster", "Galaxian", };
これで3個の文字列を配列に格納できています。20個の文字型の入れ物が3つという格納となっていますので、例えば Galaxian と表示しようと思ったら、
printf(messages[2]);
このように記述すれば表示できます。これにループを組み合わせて
for (int i = 0; i < 3; ++i) { printf("%s\n", messages[i]); }
このような記述とすれば、配列に登録されたすべての文字列を画面に表示できます。・・・ん?なに?3というマジックナンバーが気になる?また難しいところに気がつきますねぇ。この場合はやはり sizeof 演算子を使います。説明のため、処理を分解して記述しますので、是非トレースして確認してみてください。
int num1 = sizeof(messages); int num2 = sizeof(messages[0]); int count = num1 / num2;
まず、sizeof(messages) で二次元配列全体のサイズが取得できます。次に sizeof(messages[0]) でまとめられている配列のサイズが取得できます。[0] は必ず存在するので 0 と添え字を使っています。配列の集まりが2次元配列という特性ですから、全体サイズをまとめられた一つ分の配列のサイズで割れば、2次元目の要素数が得られるというわけです。
まとめて記述するとこんな書き方となります。
char messages[][20] = { "Battle City", "Dragon Buster", "Galaxian", }; int count = sizeof(messages) / sizeof(messages[0]); for (int i = 0; i < count; ++i) { printf("%s\n", messages[i]); }
この辺り、C言語はいろいろ面倒ですね・・・。
バッファロー
2019-04-25
※ ローカルGitのリポジトリ保存先としてこのUSB-HDD 6TBを2台、ダブルバックアップHDDとして3年半使い続けています。全く問題なく使えてます。現時点では外部接続HDDの一番のオススメです。

- 文字列を複数入力する
複数の入れ物を用意できたので、当然そこに文字を入力する事も出来ます。論よりRUNで、いきなりコードを提示してみます。
char messages[3][20] = { 0 }; int moji = sizeof(messages[0]) / sizeof(char); int count = sizeof(messages) / sizeof(messages[0]); for (int i = 0; i < count; ++i) { do { printf("%d人目の名前を入力 : ", i + 1); rewind(stdin); scanf_s("%s", messages[i], moji); } while (messages[i][0] == 0); }
説明しなくてもわかるかもですが、注目点は scanf_s の第2引数です。scanf_s で数値入力をする際は、変数の場所を示すために & を先頭に付与していました。ところが配列では & は不要なのです。これはその名前がそのまま場所を示しているためです。配列の messages[0] とは配列名 messages の場所から 0 番目の位置という意味になります。添え字とかインデックスってのは、その場所からのオフセットだったりするんです。・・・って、これは逆にわかりづらいか。
先の messages は大きさが 20 の配列が 3個集まっています。この先頭に、"NAITOTokihiro" と入力した状態がこちらです。

messgaes の先頭から文字列が入っているのが分かるかと思います。messages は配列全体の先頭となります。messages[0] は NAITOTokihiro と入力されている行の先頭です。そして、messages[0][0] は 'N' が格納されている場所を示します。でもね…
char messages[3][20] = { "NAITOTokihiro" }; scanf_s("%c", messages[0][0], 1);
このように記述すると、実行時に例外が発生します。何故かと言うと、messages[0][0] は単なる char型の変数だからです。
messages[0][0] = 'S';
これが出来ますよね?なので場所を示しているわけではなく、それは単なる char型の変数になっているのです。変数には場所を示す & が必要なので、正しく動作させようと思ったら



コメント