MSBOOKS

プログラミングとか深層学習とか日常の出来事とか

【C言語入門】part8 : 配列

前回はfor文と乱数の生成についてやりました.
今回は配列についてやっていきます.

配列(1次元)

今回は新しく配列という概念を導入します.たとえば主人公が攻撃ではなく,アイテムを使用して回復するなんてときもありますよね.
f:id:msteacher:20190122231940p:plain
そんなとき所持しているアイテムの数を変数に保存したいと思って,いちいち変数を作って,item1,item2,item3…って作るのは大変です.このような同じ種類(型)の連続した変数を使うとき,1パックにまとめたもの配列といいます.使い方は以下のような感じです.
・test8-1.c

#include <stdio.h>
int main(void){
   int heroHP;
   int choose;
   int item[3] = {1, 2, 1}; //アイテムの個数を入れる配列の定義(1個,2個,1個)
   int i;
   do{
      printf("主人公のHPを入力してね:");
      scanf("%d",&heroHP);
      if(heroHP <= 0){
         printf("HPは0より大きい値で入力してね\n");
      }
   }while(heroHP <= 0);
   printf("***かばんの中身***\n");
   printf("0:回復薬, 1:大回復薬, 2:毒薬(自分用)\n個数 ");
   for(i = 0; i < 3; i++){
      printf("%d:%dコ ",i,item[i]);
   }
   printf("\n");
   do{
      printf("使うアイテムの番号を指定してね:");
      scanf("%d",&choose);
      if(choose < 0 || choose >= 3){
         printf("0~2で入力してね\n");
      }
   }while(choose < 0 || choose >= 3);
   switch(choose){
      case 0:
         printf("主人公は3回復した!\n");
         heroHP = heroHP + 3;
         break;
      case 1:
         printf("主人公は6回復した!\n");
         heroHP = heroHP + 6;
         break;
      case 2:
         printf("主人公は毒のダメージを受けた!\n");
         heroHP = heroHP - 3;
   }
   item[choose] = item[choose] - 1;
   printf("主人公のHP:%d\n",heroHP);
   printf("***かばんの中身***\n");
   printf("0:回復薬, 1:大回復薬, 2:毒薬(自分用)\n個数 ");
   for(i = 0; i < 3; i++){
      printf("%d:%d ",i,item[i]);
   }
   printf("\n");
   return 0;
}

・実行結果

C:\Users\ユーザ名\Desktop>test8-1
主人公のHPを入力してね:10
***かばんの中身***
0:回復薬, 1:大回復薬, 2:毒薬(自分用)
個数 0:1コ 1:2コ 2:1コ
使うアイテムの番号を指定してね:0
主人公は3回復した!
主人公のHP:13
***かばんの中身***
0:回復薬, 1:大回復薬, 2:毒薬(自分用)
個数 0:0 1:2 2:1

配列はitem[数値]のように書き,printfなどで出力するときはitem[1]などと書きます.今回はfor文をつかって配列の中身を全部表示したいのでitem[i](iは0~2)になっています.ここで注目してほしいのは配列の通し番号は0から始まっていることです.なので1番目の値が欲しいときはitem[0]と入力します.このitem配列をイメージ図にすると以下のようになります.変数は箱のようなイメージなので,配列は箱が連続しているイメージです.
f:id:msteacher:20190118112321p:plain
配列の定義の仕方は以下のような形です.

変数の型 変数名[配列の長さ(数)] = {1つ目の初期値, 2つめの初期値, …};
配列の1つの要素を表示する書き方は以下のようになります.
printf("%d",変数名[配列の位置]);
ちなみに配列の位置は数値だけでなく計算式でも書けます.例えばitem[10/5]とすれば,item[2]が表示されます.
今回は初期値を最初に指定しましたが,forを用いてキーボード入力から受け付けることも可能です.例えば以下のような感じです.
・test8-2.c

#include <stdio.h>
int main(void){
   int item[3];
   int i;
   printf("***かばんの中身***\n");
   printf("0:回復薬, 1:大回復薬, 2:毒薬(自分用)\n");
   printf("それぞれ何個欲しいか入力してね\n");
   for(i = 0; i < 3; i++){
      printf("%d:",i);
      scanf("%d",&item[i]);
   }
   printf("***かばんの中身***\n");
   printf("0:回復薬, 1:大回復薬, 2:毒薬(自分用)\n個数 ");
   for(i = 0; i < 3; i++){
      printf("%d:%dコ ",i,item[i]);
   }
   printf("\n");
   return 0;
}

・実行結果

C:\Users\ユーザ名\Desktop>test8-2
***かばんの中身***
0:回復薬, 1:大回復薬, 2:毒薬(自分用)
それぞれ何個欲しいか入力してね
0:4
1:5
2:0
***かばんの中身***
0:回復薬, 1:大回復薬, 2:毒薬(自分用)
個数 0:4コ 1:5コ 2:0コ

配列はたくさん変数を作らずに,まとめることができる便利なもの!っていう解釈でOKなのでいろんな場面で使って慣れていきましょう.

2次元配列

2次元配列….なんか難しそうです.でもすごく単純な話で,普通の(1次元)配列の各要素に配列を入れるイメージです.1次元の配列を縦に並べてさらにその配列の1つの要素の中に配列を入れると横に広がり,配列が縦横に広がるマップのようなものができます.縦に3マス,横に4マスあるマップを2次元配列で表すと以下の図のようになります.
f:id:msteacher:20190122231413p:plain
縦に何個,横に何個?っていう考え方をするとわかりやすいですね.2次元配列の書き方は以下のような感じです.
変数名[縦に何個目?][横に何個目?];
さて先ほどの図で赤で示した配列の添え字[?][?]の中の数字は何になるか考えてみましょう.答えは練習問題の解答例のところに載せてあります.
それでは2次元配列の具体例を見てみましょう.RPGゲームのマップをイメージしてみてください.
f:id:msteacher:20190122234849p:plain(ドット絵世界)
縦横6マスで移動できるフィールドを0端は壁(1)で囲われているものとしたとき以下のように書けます.
・test8-3.c

#include <stdio.h>
int main(void){
   int map[6][6];  //マップの定義
   int i,j;
   //マップの初期化(0:移動可能,1:壁(移動不可))
   for(i = 0 ;i < 6 ;i++){
      for(j = 0 ;j < 6 ;j++){
         if(i == 0 || i == 5 || j == 0 || j == 5){
            map[i][j] = 1;
         }
         else{
            map[i][j] = 0;
         }
      }
   }
   for(i = 0 ;i < 6 ;i++){
      for(j = 0 ;j < 6 ;j++){
         printf("%d",map[i][j]);
      }
      printf("\n");
   }
   return 0;
}

・実行結果

C:\Users\ユーザ名\Desktop>test8-3
111111
100001
100001
100001
100001
111111

マップの初期化では,for文を2重にすることで,縦と横の全要素にアクセスできるようにしています.途中のif文はiかj(縦か横)が0か5であれば画面端なので1(壁)を入れるという意味です.
ちなみにマップの定義は後からforで作るのではなく,1次元配列のときのように以下のように初期値として代入することも可能です.マップを定義するときはこのほうがビジュアル的にわかりやすそうですね.

   //マップの定義と初期化(0:移動可能,1:壁(移動不可))
   int map[6][6] = {
   {1, 1, 1, 1, 1, 1},
   {1, 0, 0, 0, 0, 1},
   {1, 0, 0, 0, 0, 1},
   {1, 0, 0, 0, 0, 1},
   {1, 0, 0, 0, 0, 1},
   {1, 1, 1, 1, 1, 1}};

これだけではマップを生成しただけで操作ができないので,主人公(@)を操作して移動できるようにしてみましょう.ただし,壁(1)は移動できないのでその処理に注意して,処理を見てみましょう.
・test8-4.c

#include <stdio.h>
int main(void){
   int map[6][6];  //マップの定義
   int i,j;
   int choose;
   int hero[2] = {1, 1};  //主人公の初期位置
   int end = 0;  //終了条件
   //マップの初期化(0:移動可能,1:壁(移動不可))
   for(i = 0 ;i < 6 ;i++){
      for(j = 0 ;j < 6 ;j++){
         if(i == 0 || i == 5 || j == 0 || j == 5)
            map[i][j] = 1;
         else
            map[i][j] = 0;
      }
   }
   /*RPGゲームメインループ処理*/
   while(end == 0){
      for(i = 0 ;i < 6 ;i++){
         for(j = 0 ;j < 6 ;j++){
            //主人公の居る位置は0でなく@を描画
            if(hero[0] == i && hero[1] == j)
               printf("@");
            else
               printf("%d",map[i][j]);
         }
         printf("\n");
      }
      printf("(↑:1,→:2,↓:3,←:4,5:END)>");
      scanf("%d",&choose);
      switch(choose){
         case 1:
            //次移動するところが壁でなければ移動する
            if(map[hero[0]-1][hero[1]] != 1)
               hero[0] -= 1;
            break;
         case 2:
            if(map[hero[0]][hero[1]+1] != 1)
               hero[1] += 1;
            break;
         case 3:
            if(map[hero[0]+1][hero[1]] != 1)
               hero[0] += 1;
            break;
         case 4:
            if(map[hero[0]][hero[1]-1] != 1)
               hero[1] -= 1;
            break;
         case 5:
            end = 1;
            break;
         default:
            printf("不正な入力です\n");
      }
   }
   return 0;
}

・実行結果

C:\Users\ユーザ名\Desktop>test8-4
111111
1@0001
100001
100001
100001
111111
(↑:1,→:2,↓:3,←:4,5:END)>2
111111
10@001
100001
100001
100001
111111
(↑:1,→:2,↓:3,←:4,5:END)>1
111111
10@001
100001
100001
100001
111111
(↑:1,→:2,↓:3,←:4,5:END)>2
111111
100@01
100001
100001
100001
111111
(↑:1,→:2,↓:3,←:4,5:END)>3
111111
100001
100@01
100001
100001
111111
(↑:1,→:2,↓:3,←:4,5:END)>5

実際主人公(@)が動いているのがわかると思います.主人公の現在地座標をhero[2]に保存しています.hero[0]がy座標,hero[1]がx座標のイメージですね.次移動するところが壁なら移動しないといった処理がすごくRPGっぽいですね.アイテムを使ったら壁を貫通できる…とかも面白いかもしれません.RPGゲームをもし原始的にC言語で作るならこんな感じになるハズです.さらに今回if文の{}を省略しましたが,if文などは中身の処理の内容が1行だけであるとき{}を省略して書くことができます.便利なので使ってみましょう.
今回マップを例にしましたが,この1つの要素を画素とすると,縦横6画素の6×6の画像とも言えます.つまりみなさんが見ている画像も2次元配列みたいなものってことですね.
配列は1次元,2次元とあるからには3次元もあります.配列の中の配列の中の配列…といった感じに説明すると大変なので省略しますが,例えば3次元は3DCGのようなものを配列で作れるといったイメージになりますね.
f:id:msteacher:20190122235036p:plain
まずは配列という概念を理解して書けるようにしていきましょう.練習問題もぜひやってみてください.
お疲れさまでした.

練習問題

・問題1-1
 テストの集計プログラムを作成する.まず10人分のテストの点数(0~100)をキーボードから入力し,整数型の配列score(長さ10)に代入し,表示するプログラムを書いてみよう(test8-5.c).
・実行結果例

C:\Users\ユーザ名\Desktop>test8-5
***テスト集計プログラム***
0番目の人のテストの点数:55
1番目の人のテストの点数:63
2番目の人のテストの点数:46
3番目の人のテストの点数:50
4番目の人のテストの点数:77
5番目の人のテストの点数:69
6番目の人のテストの点数:89
7番目の人のテストの点数:400
7番目の人のテストの点数:40
8番目の人のテストの点数:5
9番目の人のテストの点数:53

0番目の人:55点
1番目の人:63点
2番目の人:46点
3番目の人:50点
4番目の人:77点
5番目の人:69点
6番目の人:89点
7番目の人:40点
8番目の人:5点
9番目の人:53点

・問題1-2
 test8-5.cで作成したプログラムを改良する.点数の分布を示す以下のような結果を得られるプログラムを作成してみよう(test8-6.c).ただし点数分布を保存する整数型配列agg(長さ11),平均点を求めるために合計値を保存する整数型変数totalを定義する.なお,点数分布を表示するにあたって点数の高い人から低い人へ表示するときにfor文を用いるときi++と同様に,大きい値から小さい値へ1ずつ減らすものにi--(i = i - 1と同じ意味)があるので使ってみよう.

今回のテストの点数分布
[100点]:○人
[90~99点]:○人
[80~89点]:○人
[70~79点]:○人
[60~69点]:○人
[50~59点]:○人
[40~49点]:○人
[30~39点]:○人
[20~29点]:○人
[10~19点]:○人
[0~9点]:○人
平均点:○点(小数点以下2桁まで)
・実行結果例

C:\Users\ユーザ名\Desktop>test8-6
***テスト集計プログラム***
0番目の人のテストの点数:55
1番目の人のテストの点数:63
2番目の人のテストの点数:46
3番目の人のテストの点数:50
4番目の人のテストの点数:77
5番目の人のテストの点数:69
6番目の人のテストの点数:89
7番目の人のテストの点数:100
8番目の人のテストの点数:5
9番目の人のテストの点数:53

[100点]:1人
[90~99点]:0人
[80~89点]:1人
[70~79点]:1人
[60~69点]:2人
[50~59点]:3人
[40~49点]:1人
[30~39点]:0人
[20~29点]:0人
[10~19点]:0人
[0~9点]:1人
平均点:60.70点

・問題2
 test8-4.cを改良する.まずマップの大きさを7×7(縦横7マス)に拡張する.そこへ任意の地点(例えばi=3,j=3)にゴール(G)を作り,そこへたどり着いたら"GOAL!"と表示し終了するプログラム(test8-7.c)を作成してみよう.ただしゴールの座標は整数型配列goal[2]で定義する.
・実行結果例

C:\Users\ユーザ名\Desktop>test8-7
1111111
1@00001
1000001
100G001
1000001
1000001
1111111
(↑:1,→:2,↓:3,←:4)>2
1111111
10@0001
1000001
100G001
1000001
1000001
1111111
(↑:1,→:2,↓:3,←:4)>2
1111111
100@001
1000001
100G001
1000001
1000001
1111111
(↑:1,→:2,↓:3,←:4)>1
1111111
100@001
1000001
100G001
1000001
1000001
1111111
(↑:1,→:2,↓:3,←:4)>3
1111111
1000001
100@001
100G001
1000001
1000001
1111111
(↑:1,→:2,↓:3,←:4)>3
1111111
1000001
1000001
100@001
1000001
1000001
1111111
GOAL!

練習問題の解答例

・二次元配列の?の答え

map[2][1]

・問題1-1
 ・test8-5.c

#include <stdio.h>
int main(void){
   int score[10];
   int i;
   printf("***テスト集計プログラム***\n");
   for(i = 0; i < 10; i++){
      do{
         printf("%d番目の人のテストの点数:",i);
         scanf("%d",&score[i]);
      }while(score[i] < 0 || score[i] > 100);
   }
   printf("\n");
   for(i = 0; i < 10; i++){
      printf("%d番目の人:%d\n",i,score[i]);
   }
   printf("\n");
   return 0;
}

・問題1-2
 ・test8-6.c

#include <stdio.h>
int main(void){
   int score[10];
   int i;
   int agg[11];
   int total;
   //agg配列の初期化
   for(i = 0; i < 11; i++){
      agg[i] = 0;
   }
   printf("***テスト集計プログラム***\n");
   for(i = 0; i < 10; i++){
      do{
         printf("%d番目の人のテストの点数:",i);
         scanf("%d",&score[i]);
      }while(score[i] < 0 || score[i] > 100);
   }
   printf("\n");
   //i番目の人の点数の分布場所のaggを++する
   //同時に平均点を求めるためのtotalを作る
   for(i = 0; i < 10; i++){
      agg[score[i]/10]++;
      total = total + score[i];
   }
   printf("[100点]:%d\n",agg[10]);
   for(i = 9; i >= 0; i--){
      printf("[%d%d点]:%d\n",i*10,i*10+9,agg[i]);
   }
   printf("平均点:%.2f\n",total/10.0);
   return 0;
}

・問題2
 ・test8-7.c

#include <stdio.h>
int main(void){
   int map[7][7];  //マップの定義
   int i,j;
   int choose;
   int hero[2] = {1, 1};  //主人公の初期位置
   int goal[2] = {3, 3};  //ゴールの位置
   //マップの初期化(0:移動可能,1:壁(移動不可))
   for(i = 0 ;i < 7 ;i++){
      for(j = 0 ;j < 7 ;j++){
         if(i == 0 || i == 6 || j == 0 || j == 6)
            map[i][j] = 1;
         else
            map[i][j] = 0;
      }
   }
   /*RPGゲームメインループ処理*/
   while(1){
      for(i = 0 ;i < 7 ;i++){
         for(j = 0 ;j < 7 ;j++){
            //主人公の居る位置は0でなく@を描画
            if(hero[0] == i && hero[1] == j)
               printf("@");
            //ゴールの場所は0ではなくGを描画
            else if(goal[0] == i && goal[1] == j)
               printf("G");
            else
               printf("%d",map[i][j]);
         }
         printf("\n");
      }
      //終了判定
      if(goal[0] == hero[0] && goal[1] == hero[1])
         break;
      printf("(↑:1,→:2,↓:3,←:4)>");
      scanf("%d",&choose);
      switch(choose){
         case 1:
            //次移動するところが壁でなければ移動する
            if(map[hero[0]-1][hero[1]] != 1)
               hero[0] -= 1;
            break;
         case 2:
            if(map[hero[0]][hero[1]+1] != 1)
               hero[1] += 1;
            break;
         case 3:
            if(map[hero[0]+1][hero[1]] != 1)
               hero[0] += 1;
            break;
         case 4:
            if(map[hero[0]][hero[1]-1] != 1)
               hero[1] -= 1;
            break;
         default:
            printf("不正な入力です\n");
      }
   }
   printf("GOAL!\n");
   return 0;
}