本連載も今回で5回目,いよいよ中盤に差し掛かってきました。第1回から4回までは,C言語のプログラムとアセンブラのプログラムとの対比,浮動小数点数が計算を間違う理由,構造化プログラミングの必要性,変数のスコープとメモリー・セグメントの関係,スタックの動作など基本的な知識を中心に解説してきました。「こうなるのは,こういう理由か」と,目に見えぬ仕組みを理解していただけたでしょうか。

 今回からは,基本的な仕組みに加えて,プログラミングの面白さもお伝えしたいと思います。プログラミングの面白さは“考えること”にあります。どう処理すればよいかをイメージできない複雑なことを,創意工夫でスパッとコードで実現できたときの爽快感は,他の仕事では得がたいものかもしれません。例えば,大量のデータを少量のコードで扱い,処理の効率をアップさせるために知っておかなくてはならない基本的なデータ構造に「配列」があります。今回は,この「配列」にフォーカスし,配列とは何かから始め,配列を使うと何が便利なのかを知っていただきたいと思います。

同じデータが繰り返し発生するときに配列を使う

 例えば,陸上競技100メートル走の結果を記録し,誰が(何コースが)1位だったかを表示するプログラムを考えてみましょう。

 100メートル走のタイムは10.12などと小数点以下の値を持つので,float型で記録することにします。コースが8コースあるとすると,タイムを記録する変数も八つ必要になります。つまり,

float rtime1,rtime2,rtime3,rtime4,rtime5,rtime6,rtime7,rtime8;

とコースの数だけ別々の変数を定義し,

printf("1コースのタイムを入力してください:");
scanf("%f",&rtime1);
~
printf("8コースのタイムを入力してください:");
scanf("%f",&rtime8);

のようにscanf関数で各変数に,タイムを入力していくプログラムが思いつくでしょう*1。でもこれだと,変数の個数ぶん同じコードを繰り返し記述しなければならないので,冗長な感じがします。最高タイムを求めるコードも面倒なものになりそうな気がします。このように,同じ性質のデータが繰り返し発生するときに「配列」を使います。

 リスト1が配列を使ったプログラムです。(1)の「float rtime[COURSE]」が配列の宣言です。COURSEは#defineプリプロセッサ命令でマクロとして宣言されています。#defineは

#define マクロ名 置換する文字列

の形式でマクロを定義します。ソース・プログラム中のマクロ名はプログラムがコンパイルされる前に,プリプロセッサにより,「置換する文字列」に変換されます。ですから,これでfloat型のrtime[0]~rtime[7]までの八つの要素を持つ配列がメモリー上に確保されるのです。各コースに8人の選手がキチンと並んだように,float型の値を入れるメモリー領域が8個ぶん連続して確保されるわけです(図1)。

リスト1●8コースぶんのタイムを記録し,一着のコースとタイムを表示するプログラム
リスト1●8コースぶんのタイムを記録し,一着のコースとタイムを表示するプログラム

図1●1次元配列のイメージ。float rtime[8]と書くと,float型の変数を入れる箱が八つ用意される
図1●1次元配列のイメージ。float rtime[8]と書くと,float型の変数を入れる箱が八つ用意される

 配列を扱う場合,マクロ定義はとても有効です。リスト1の最初のfor文のループでは,配列の添え字「i」の値を一つずつ変化させながら,scanf関数で配列の各要素に値を代入しています(2)。このforループで繰り返す回数をコントロールするためにも,「i < COURSE」とマクロCOURSEを使っていますね。このように書いておくと,コース数が変わったときに,#defineの「置換する文字列」だけを変更すればよいので便利です。

 配列について一つ注意してほしいのが,C言語では配列の添え字(インデックス)は常に0から始まるという点です。他の言語では1から始まるように指定できる場合もありますが,C言語では常に0からです。ですから,printf関数の「%dコース…」の変換仕様%dにインデックス「i+1」を指定し,コース番号としています。rtime[0]が1コースのタイムを入れる配列の要素になるのです。ですから,for文の繰り返し判定もi < COURSEと8より小さかったらとシンプルに記述することができます。

 最高タイムを求める処理では,まずrtime[0]を変数minに代入し,それより小さい値があったらminの値を入れ替え,インデックスの値をjに記憶しています(3)。最高タイムを表示するprintf文中の変換仕様「%.2f」は浮動小数点数を小数点以下2桁まで表示することを意味します。このプログラムを実行すると図2のようになります。最高タイムのコースとタイムを表示していますね。

図2●リスト1を実行したところ
図2●リスト1を実行したところ

多次元配列を使うと,より多くのデータを扱える

 陸上競技では,複数組の予選が行われ,勝者が決勝に残り,メダルを争います。リスト1のサンプル・プログラムは,決勝で誰が一番かを決めるようなイメージでしたが,今度は,予選の処理も考えてみましょう。

 100メートル走に48名のエントリがあったので,6組に分け予選を行い,各組の最高のタイムを表示させたいとします。「それなら,1次元配列を使ったリスト1のプログラムを6回実行すれば?」――確かにそうですが,ちょっと面倒です。こんなときに便利なのが「多次元配列」です。多次元配列を使えば,6組×8コースのタイムを一つの配列として扱うことができます。

 例えば,図3のような配列を宣言するには,「float rtime[6][8]」とインデックスを二つ書きます。こうすれば,8コースぶんの1次元配列が六つ格納できる2次元配列が確保できます。

図3●多次元配列(2次元)のイメージ。float rtime[6][8]と書くと,float型の変数を入れる箱が6×8=48個用意される
図3●多次元配列(2次元)のイメージ。float rtime[6][8]と書くと,float型の変数を入れる箱が6×8=48個用意される
[画像のクリックで拡大表示]

 実際に2次元配列を実装したコードを見てみましょう(リスト2)。for文によるループを入れ子(ネスト)にして,各要素を扱っています。インデックス「i」が組に,「j」がコースに対応しています。この例では単純に各組のベストタイムを表示しているだけですが,2次元配列としてデータを表現しておけば,組(次元)ごとの処理だけではなく,配列全体を一つのデータの固まりとして扱う――例えば全体から上位3名のタイムを求めること――も可能です。

#include <stdio.h>
#define KUMI 6
#define COURSE 8
int main()
{
  float rtime[KUMI][COURSE];
  int i,j,k;
  float min;

  for (i = 0; i < KUMI; i++) {
    for (j = 0; j < COURSE; j++) {
      printf("%d組%dコースのタイムを入力してください:",
      i + 1,j + 1);
      scanf("%f",&rtime[i][j]);
    }
  }
  for (i = 0; i < KUMI; i++) {
    min = rtime[i][0];
    k = 0;
    for (j = 1; j < COURSE; j++) {
      if (rtime[i][j] < min) {
        min = rtime[i][j];
        k = j;
      }
    }
    printf("%d組の最高タイムは%dコースの%.2fです¥n",
    i+1,k+1,min);
  }
  return(0);
}
リスト2●2次元配列を使って実装したコード

 C言語では,2次元配列だけでなく,3次元,4次元と多次元の配列を扱うことができます。慣れないと扱いにくいかもしれませんが,要はインデックスが増え,for文のネストが一つ深くなるだけです。例えば,100メートル走のタイムを格納する「組×コース」の2次元配列を,100メートル走と200メートル走のタイムを格納できるように拡張しようと思ったら,

#define SYUMOKU 2

と種目の数をマクロ定義して,

float rtime[SYUMOKU][KUMI][COURSE];

で3次元配列を定義し,リスト3のようにforループの外側に,もう一つforループを追加してやればよいわけです。

for (i = 0; i < SYUMOKU; i++) {
  for (j = 0; j < KUMI; j++) {
    for (k = 0; k < COURSE; k++) {
      printf("第%d種目%d組%dコースのタイムを入力!:",
      i + 1,j + 1,k + 1);
      scanf("%f",&rtime[i][j][k]);
    }
  }
}
リスト3●2次元配列を,100メートル走と200メートル走のタイムを格納できるように拡張したコード(一部)