C言語のきほん|2次元配列を二重ループで処理する

2次元配列は、表のようにデータをまとめて扱える便利な仕組みです。
たとえば、複数人分の売上データ、数日分の気温、クラスの成績一覧など、「行」と「列」で整理できる情報をひとまとめにできます。

ただし、2次元配列の中身を順番に取り出して処理したいときは、1次元配列のときのように for文を1つ使うだけでは足りません。
そこで活躍するのが、for文の二重ループです。

外側のループで行を順番に進め、内側のループで列を順番に進めることで、2次元配列の全要素にきちんとアクセスできます。
この考え方を身につけると、各行の合計、各列の合計、平均値の計算などがとても自然に書けるようになります。

ここでは、元の「学生の点数」の例を別のわかりやすい題材に置き換えて、2次元配列を二重ループで処理する流れを丁寧に見ていきます。
今回は、3人の店員が4日間に記録した販売個数を2次元配列で管理し、次の内容を求めるプログラムにしてみます。

  • 各店員の合計販売数と平均販売数
  • 各日の合計販売数と平均販売数

二重ループの役割が見えやすい題材なので、2次元配列にまだ慣れていない方にも理解しやすいはずです。

2次元配列とは何か

2次元配列は、1次元配列を縦横に広げたようなものです。
たとえば、今回の販売データは次のような表で考えられます。

店員\日1日目2日目3日目4日目
1人目12151114
2人目10181316
3人目9141215

この表をC言語では、たとえば次のような2次元配列で表せます。

int sales[3][4] = {
    {12, 15, 11, 14},
    {10, 18, 13, 16},
    {9, 14, 12, 15}
};

このとき、sales[行][列] という形で要素を取り出します。

要素意味
sales[0][0]1人目の1日目の販売数
sales[0][1]1人目の2日目の販売数
sales[1][2]2人目の3日目の販売数
sales[2][3]3人目の4日目の販売数

つまり、最初の添字が行、次の添字が列を表しています。

二重ループが必要になる理由

2次元配列は、行ごとに列をたどることで全体を順番に処理できます。
そのため、for文を2つ組み合わせて使います。

基本形は次の通りです。

for (int i = 0; i < 行数; i++) {
    for (int j = 0; j < 列数; j++) {
        /* 配列[i][j] を処理する */
    }
}

この形では、

  • 外側の for文 が行を進める
  • 内側の for文 が列を進める

という役割になります。

今回の販売データなら、外側で店員を順に見て、内側で各日を順に見ていく流れになります。

サンプルプログラム

元の点数管理プログラムを、販売個数の集計プログラムに変更した例です。
表示メッセージも日本語でわかりやすくし、コメントもすべて日本語にしています。

ファイル名:10_9_1.c

#include <stdio.h>

#define STAFF_COUNT 3   // 店員数
#define DAY_COUNT 4     // 日数

int main(void)
{
    // 販売個数を2次元配列で管理
    int sales[STAFF_COUNT][DAY_COUNT] = {
        {12, 15, 11, 14},   // 1人目の販売個数
        {10, 18, 13, 16},   // 2人目の販売個数
        {9, 14, 12, 15}     // 3人目の販売個数
    };

    int staff_totals[STAFF_COUNT] = {0};         // 各店員の合計販売数
    double staff_averages[STAFF_COUNT] = {0.0};  // 各店員の平均販売数

    // 各店員の合計販売数と平均販売数を計算
    for (int i = 0; i < STAFF_COUNT; i++) {
        for (int j = 0; j < DAY_COUNT; j++) {
            staff_totals[i] += sales[i][j];
        }
        staff_averages[i] = (double)staff_totals[i] / DAY_COUNT;
    }

    // 各店員の結果を表示
    printf("各店員の合計販売数と平均販売数\n");
    for (int i = 0; i < STAFF_COUNT; i++) {
        printf("店員%d: 合計=%d, 平均=%.2f\n",
               i + 1, staff_totals[i], staff_averages[i]);
    }

    int day_totals[DAY_COUNT] = {0};         // 各日の合計販売数
    double day_averages[DAY_COUNT] = {0.0};  // 各日の平均販売数

    // 各日の合計販売数と平均販売数を計算
    for (int j = 0; j < DAY_COUNT; j++) {
        for (int i = 0; i < STAFF_COUNT; i++) {
            day_totals[j] += sales[i][j];
        }
        day_averages[j] = (double)day_totals[j] / STAFF_COUNT;
    }

    // 各日の結果を表示
    printf("\n各日の合計販売数と平均販売数\n");
    for (int j = 0; j < DAY_COUNT; j++) {
        printf("%d日目: 合計=%d, 平均=%.2f\n",
               j + 1, day_totals[j], day_averages[j]);
    }

    return 0;
}

実行結果

各店員の合計販売数と平均販売数
店員1: 合計=52, 平均=13.00
店員2: 合計=57, 平均=14.25
店員3: 合計=50, 平均=12.50

各日の合計販売数と平均販売数
1日目: 合計=31, 平均=10.33
2日目: 合計=47, 平均=15.67
3日目: 合計=36, 平均=12.00
4日目: 合計=45, 平均=15.00

プログラムの全体像をつかもう

このプログラムでは、同じ2次元配列 sales を使って、2つの方向から集計しています。

1つ目は、店員ごとの集計です。
これは「横方向」にデータを見ていくイメージです。

2つ目は、日ごとの集計です。
これは「縦方向」にデータを見ていくイメージです。

同じ表でも、どちらを外側のループにするかで、集計の視点が変わるのがポイントです。

各店員の合計販売数と平均販売数の計算

まずは、各店員が4日間で合計いくつ販売したのか、そして平均でいくつ販売したのかを求めています。

該当部分はこちらです。

for (int i = 0; i < STAFF_COUNT; i++) {
    for (int j = 0; j < DAY_COUNT; j++) {
        staff_totals[i] += sales[i][j];
    }
    staff_averages[i] = (double)staff_totals[i] / DAY_COUNT;
}

ここでは、外側のループの変数 i が店員を表し、内側のループの変数 j が日を表しています。

流れを表にすると、次のようになります。

ループ役割
外側の for文店員を1人ずつ選ぶ
内側の for文その店員の1日目〜4日目の販売数を順に足す

たとえば i が 0 のときは、1人目のデータを処理します。

そのとき内側のループでは、

  • sales[0][0]
  • sales[0][1]
  • sales[0][2]
  • sales[0][3]

を順番に足していきます。

つまり、

12 + 15 + 11 + 14 = 52

となり、1人目の合計販売数が 52 と求まります。
そのあとで、4日分なので 4 で割って平均を出します。

52 ÷ 4 = 13.00

という流れです。

各店員の集計イメージ

sales
  ↓ 1人目の4日分を加算
staff_totals[0]
  ↓ 4で割る
staff_averages[0]

sales
  ↓ 2人目の4日分を加算
staff_totals[1]
  ↓ 4で割る
staff_averages[1]

sales
  ↓ 3人目の4日分を加算
staff_totals[2]
  ↓ 4で割る
staff_averages[2]

処理の見方

この部分は、「行ごとの集計」と考えるとわかりやすいです。
表でいうと、1行ずつ横に見ていき、その行の合計と平均を求めています。

各日の合計販売数と平均販売数の計算

次に、1日目、2日目、3日目、4日目ごとに、全店員の販売数を合計し、平均を求めています。

該当部分はこちらです。

for (int j = 0; j < DAY_COUNT; j++) {
    for (int i = 0; i < STAFF_COUNT; i++) {
        day_totals[j] += sales[i][j];
    }
    day_averages[j] = (double)day_totals[j] / STAFF_COUNT;
}

今度は先ほどと逆で、外側のループの変数 j が日を表し、内側のループの変数 i が店員を表しています。

つまり、1日ずつ取り出して、その日の全店員の販売数を足しているわけです。

ループ役割
外側の for文日を1日ずつ選ぶ
内側の for文その日の全店員の販売数を順に足す

たとえば j が 0 のときは、1日目のデータを処理します。

内側のループでは、

  • sales[0][0]
  • sales[1][0]
  • sales[2][0]

を順番に足します。

つまり、

12 + 10 + 9 = 31

となり、1日目の合計販売数が 31 になります。
さらに、店員は3人なので 3 で割って平均を出します。

31 ÷ 3 = 10.33

という計算です。

各日の集計イメージ

sales
  ↓ 1日目の3人分を加算
day_totals[0]
  ↓ 3で割る
day_averages[0]

sales
  ↓ 2日目の3人分を加算
day_totals[1]
  ↓ 3で割る
day_averages[1]

sales
  ↓ 3日目の3人分を加算
day_totals[2]
  ↓ 3で割る
day_averages[2]

sales
  ↓ 4日目の3人分を加算
day_totals[3]
  ↓ 3で割る
day_averages[3]

処理の見方

この部分は、「列ごとの集計」と考えると理解しやすいです。
表でいうと、1列ずつ縦に見ていき、その列の合計と平均を求めています。

ループの向きを変えると集計対象が変わる

このプログラムのとても大事なポイントは、同じ2次元配列でも、外側と内側のループの組み合わせを変えるだけで、見方が変わるということです。

外側のループ内側のループ何を集計しているか
店員各店員の合計・平均
店員各日の合計・平均

この違いを意識できるようになると、2次元配列の問題がかなり読みやすくなります。

{0}で初期化する意味

このプログラムでは、合計を保存する配列を次のように初期化しています。

int staff_totals[STAFF_COUNT] = {0};
int day_totals[DAY_COUNT] = {0};

これはとてもよく使う書き方です。
配列を {0} で初期化すると、先頭要素だけでなく、すべての要素が 0 に初期化されます。

たとえば、

int numbers[5] = {0};

と書いた場合、実際には次のような状態になります。

要素
numbers[0]0
numbers[1]0
numbers[2]0
numbers[3]0
numbers[4]0

合計を求める配列は、最初の値がゴミ値のままだと正しい結果になりません。
そのため、最初にきちんと 0 クリアしておくことが大切です。

吹き出しで書かれていた内容は、まさにこの点を表しています。

{0}で初期化すると配列の全ての要素が0クリアできるよ。

これは実務でも学習でもよく出てくる便利な書き方なので、ぜひ覚えておきたいところです。

平均を計算するときに double を使う理由

平均を求める式では、次のように書いています。

staff_averages[i] = (double)staff_totals[i] / DAY_COUNT;
day_averages[j] = (double)day_totals[j] / STAFF_COUNT;

ここで大事なのは、整数同士の割り算にならないようにしていることです。

たとえば、31 ÷ 3 は本来 10.333... ですが、int 同士で割ると小数部分が切り捨てられて 10 になってしまいます。

そこで、

(double)staff_totals[i]

のように型変換を行い、計算全体を小数で行うようにしています。

書き方結果のイメージ
31 / 310
(double)31 / 310.333333...

平均値を正しく出したいときは、この型変換がとても重要です。

配列と添字の対応を整理しよう

2次元配列では、どの添字が何を表しているかを見失いやすいです。
そのため、最初に意味をはっきり整理しておくことが大切です。

今回の配列では次の通りです。

添字意味
i店員番号
j日にち番号
sales[i][j]i番目の店員のj日目の販売数

この対応が頭に入っていると、ループの意味も自然に読めるようになります。

たとえば、

sales[i][j]

を見たら、

「i番目の店員の、j日目の販売数」

と読めるようにしておくと理解がぐっと楽になります。

二重ループを読むコツ

二重ループは慣れないうちは少し複雑に見えますが、次の順番で読むと理解しやすいです。

1つ目のコツは、外側のループが何を固定しているかを見ること

for (int i = 0; i < STAFF_COUNT; i++)

なら、店員を1人ずつ固定しています。

for (int j = 0; j < DAY_COUNT; j++)

なら、日を1日ずつ固定しています。

2つ目のコツは、内側のループが何を動かしているかを見ること

外側で店員を固定して、内側で日を動かしているなら、各店員の1日目から4日目までを順に処理しています。

外側で日を固定して、内側で店員を動かしているなら、その日の全店員の値を順に処理しています。

3つ目のコツは、何に足し込んでいるかを見ること

staff_totals[i] += sales[i][j];

なら、店員ごとの合計に足し込んでいます。

day_totals[j] += sales[i][j];

なら、日ごとの合計に足し込んでいます。

この3つを意識すると、二重ループの読み取りがかなりしやすくなります。

図でイメージすると理解しやすい

この学習で身につけたいこと

今回の内容で特に身につけておきたいのは、次の3点です。

身につけたいこと内容
2次元配列の見方行と列でデータを管理する考え方
二重ループの使い方外側と内側のループで全要素を順番に処理する方法
集計の向きの違い行ごとの集計と列ごとの集計を切り替える考え方

2次元配列は最初こそ少し複雑に感じますが、表をそのままプログラムで扱える便利な仕組みです。
そして、その表を自在に処理するための基本が二重ループです。

今回のように、まずは「行ごとに見る」「列ごとに見る」という2つの見方をしっかり押さえておくと、この先の表計算的な処理や、少し複雑なデータ処理にもつながっていきます。