C言語の基本|多重ループの使い分け

回数はfor、終了条件はwhile。ループの役割を見極めれば、多重ループはもっと読みやすく、もっと安全に書ける。

多重ループは、繰り返し処理の中に別の繰り返し処理を置く書き方です。表のような行と列を扱ったり、文字で図形を描いたり、複数の値の組み合わせをすべて調べたりするときに活躍します。

多重ループは、forだけでもwhileだけでも作れます。さらに、外側と内側で異なるループを組み合わせることもできます。

どの書き方でも同じ処理を実現できますが、読みやすさやミスの起こりやすさには違いがあります。大切なのは、次のようにループの目的から選ぶことです。

  • 繰り返す回数や範囲が決まっているならfor
  • ある条件を満たす間だけ続けるならwhile
  • 外側と内側の目的が異なるならforとwhileを組み合わせる

ループの種類を適切に選べるようになると、コードを読んだだけで処理の意図が伝わるようになります。

forとwhileの特徴を比べよう

forとwhileは、どちらも初期化、継続条件、更新によって繰り返しを制御します。ただし、それらを記述する場所が異なります。

観点forwhile
初期化forの括弧内に書けるループの前に書く
継続条件forの括弧内に書くwhileの括弧内に書く
更新forの括弧内に書けるループ本体に書く
得意な処理回数や範囲が決まった処理終了時期が条件で決まる処理
多重ループでの特徴行数や列数が見やすい柔軟だが初期化と更新に注意が必要
主な注意点条件や更新の境界を確認する更新忘れによる無限ループに注意する

行数と列数が決まった表を作るなら、外側と内側の両方にforを使うと簡潔です。一方、入力が続く間だけ処理するような場合はwhileが自然です。

図1:外側と内側で役割を分ける多重ループ

この図から分かること

外側と内側のループは、必ず同じ種類にする必要はありません。

たとえば、利用者が終了を指示するまで入力を受け付け、その入力値の回数だけ文字を表示するなら、次の組み合わせが自然です。

  • 外側は終了条件が入力によって決まるためwhile
  • 内側は表示回数が入力後に確定するためfor

このように、それぞれのループが担当する仕事を別々に考えると、適切な制御文を選びやすくなります。

外側whileと内側forを組み合わせる

まずは、終了時期が決まっていない外側の処理と、回数が決まっている内側の処理を組み合わせてみましょう。

入力された長さの記号を繰り返し表示するプログラム

利用者が正の整数を入力すると、その個数だけ=を表示します。0以下が入力されるまで、何度でも新しい長さを指定できます。

ファイル名:9_9_1.c

#include <stdio.h>

int main(void)
{
    int width;

    printf("線の長さを入力してください(0以下で終了)> ");
    scanf("%d", &width);

    while (width > 0) {
        for (int i = 0; i < width; i++) {
            printf("=");
        }
        printf("\n");

        printf("線の長さを入力してください(0以下で終了)> ");
        scanf("%d", &width);
    }

    printf("表示を終了します。\n");

    return 0;
}
実行結果の例
線の長さを入力してください(0以下で終了)> 5
=====
線の長さを入力してください(0以下で終了)> 3
===
線の長さを入力してください(0以下で終了)> 0
表示を終了します。

外側のwhileは、widthが正の間だけ処理を続けます。入力回数は利用者によって変わるため、回数を決めて繰り返すforよりもwhileが向いています。

内側のforでは、width回だけ=を表示します。こちらは入力後に繰り返し回数が明確になるため、forで簡潔に表現できます。

処理の役割は次のとおりです。

  • whileは次の線を表示するかどうかを管理する
  • forは1本の線を構成する文字数を管理する
  • widthが0以下になるとwhileが終了する

同じ表をforとwhileで書き比べる

行数と列数が決まっている処理は、forでもwhileでも書けます。ここでは、4行6列の番号表を両方の書き方で作り、コードの違いを比べます。

forの二重ループで4行6列の表を表示するプログラム

ファイル名:9_9_2.c

#include <stdio.h>

int main(void)
{
    for (int row = 1; row <= 4; row++) {
        for (int col = 1; col <= 6; col++) {
            printf("%d ", col);
        }
        printf("\n");
    }

    return 0;
}
実行結果の例
1 2 3 4 5 6
1 2 3 4 5 6
1 2 3 4 5 6
1 2 3 4 5 6

外側のforは4行を作り、内側のforは1行に6個の数字を並べます。

forの括弧を見るだけで、rowが1から4、colが1から6まで変化することが分かります。回数が明確な多重ループでは、処理範囲を把握しやすい書き方です。

whileの二重ループで4行6列の表を表示するプログラム

ファイル名:9_9_3.c

#include <stdio.h>

int main(void)
{
    int row = 1;

    while (row <= 4) {
        int col = 1;

        while (col <= 6) {
            printf("%d ", col);
            col++;
        }

        printf("\n");
        row++;
    }

    return 0;
}
実行結果の例
1 2 3 4 5 6
1 2 3 4 5 6
1 2 3 4 5 6
1 2 3 4 5 6

実行結果はfor版と同じですが、while版では初期化と更新を別々の場所に書きます。

特に重要なのが、colの初期化位置です。

int col = 1;

この初期化は外側のwhileの中にあります。そのため、新しい行が始まるたびにcolが1へ戻ります。

もしcolを外側のwhileよりも前で一度だけ初期化すると、1行目が終わった時点でcolは7になります。2行目ではcol <= 6が最初から偽になるため、数字が表示されません。

図2:whileの内側変数を毎行初期化する理由

この図から分かること

二重ループでは、内側のループが終了してから外側のループが次の周へ進みます。

whileで内側のループを書く場合は、内側で使う変数を適切な位置で初期化し直さなければなりません。

whileの多重ループでは、次の3点を確認すると事故を防ぎやすくなります。

  • 内側の変数を外側ループの中で初期化しているか
  • 内側の変数を更新しているか
  • 外側の変数も更新しているか

col++を忘れると内側が終わらず、row++を忘れると外側が終わりません。どちらも無限ループの原因になります。

行によって内側の回数を変える

多重ループでは、内側の繰り返し回数を外側の変数に合わせて変化させることもできます。

行番号から1までの数字を段状に表示するプログラム

ファイル名:9_9_4.c

#include <stdio.h>

int main(void)
{
    int lines;

    printf("行数を入力してください > ");
    scanf("%d", &lines);

    for (int row = 1; row <= lines; row++) {
        for (int col = row; col >= 1; col--) {
            printf("%d ", col);
        }
        printf("\n");
    }

    return 0;
}
実行結果の例
行数を入力してください > 4
1
2 1
3 2 1
4 3 2 1

外側のforでは、rowが1から入力された行数まで増えていきます。

内側のforはcolをrowから開始し、1になるまで減らします。そのため、1行目では1個、2行目では2個、3行目では3個の数字が表示されます。

ここでは内側の初期値が固定値ではなくrowになっている点がポイントです。外側の変数を内側の初期値や条件に使うことで、行ごとに長さが変わる形を作れます。

表の見出しと本体を分けて考える

整った表を表示するときは、すべてを多重ループに入れる必要はありません。

最初に見出しを表示し、その後で表の本体を二重ループで作ると、処理の役割が分かりやすくなります。

1から5までの足し算表を表示するプログラム

ファイル名:9_9_5.c

#include <stdio.h>

int main(void)
{
    printf("1から5までの足し算表\n");
    printf("   |");

    for (int col = 1; col <= 5; col++) {
        printf("%3d", col);
    }
    printf("\n");

    printf("------------------\n");

    for (int row = 1; row <= 5; row++) {
        printf("%2d |", row);

        for (int col = 1; col <= 5; col++) {
            printf("%3d", row + col);
        }

        printf("\n");
    }

    return 0;
}
実行結果の例
1から5までの足し算表
   |  1  2  3  4  5
------------------
 1 |  2  3  4  5  6
 2 |  3  4  5  6  7
 3 |  4  5  6  7  8
 4 |  5  6  7  8  9
 5 |  6  7  8  9 10

このプログラムでは、表示する場所ごとに処理を分けています。

  • 最初のforは列見出しを表示する
  • 外側のforは表の行を作る
  • 行頭のprintfは行見出しと区切りを表示する
  • 内側のforはrow + colの計算結果を横に並べる
  • 内側のforが終了した後で改行する

改行を内側のforに入れると、数字を1個表示するたびに行が変わってしまいます。どの処理が行全体を担当し、どの処理が1つの要素を担当するのかを意識しましょう。

三重ループですべての組み合わせを調べる

ループを3段にすると、3つの値の組み合わせを順番に調べられます。

たとえば、3個のサイコロを振ったときの出目をすべて確認するなら、各サイコロを担当するforを1つずつ用意します。

3個のサイコロの合計が指定値になる組を探すプログラム

ファイル名:9_9_6.c

#include <stdio.h>

int main(void)
{
    int target;
    int count = 0;

    printf("3個のサイコロの合計値を入力してください > ");
    scanf("%d", &target);

    for (int first = 1; first <= 6; first++) {
        for (int second = 1; second <= 6; second++) {
            for (int third = 1; third <= 6; third++) {
                if (first + second + third == target) {
                    printf("(%d, %d, %d)\n",
                           first, second, third);
                    count++;
                }
            }
        }
    }

    printf("合計が%dになる出目は%d通りです。\n",
           target, count);

    return 0;
}
実行結果の例
3個のサイコロの合計値を入力してください > 4
(1, 1, 2)
(1, 2, 1)
(2, 1, 1)
合計が4になる出目は3通りです。

3つのforは、それぞれ1個目、2個目、3個目のサイコロを担当します。各サイコロには1から6までの6通りがあるため、確認する組み合わせは次の数になります。

6 × 6 × 6 = 216通り

最も内側にあるifで、3つの出目の合計がtargetと一致するかを判定します。一致した組だけを表示し、countを増やしています。

三重ループは便利ですが、各ループの回数を掛けた数だけ処理が実行されます。ループを重ねるほど処理回数が増えることも覚えておきましょう。

図3:多重ループの段数と処理回数

この図から分かること

多重ループでは、外側のループが1回動くたびに、その内側のループが最初から最後まで実行されます。

各ループが6回ずつ動く場合、段数によって処理回数は次のように増えます。

ループの段数処理回数
1重6回
2重36回
3重216回

組み合わせをすべて調べる処理では、この性質を利用します。ただし、繰り返し範囲が大きいと処理回数も急激に増えるため、それぞれのループが本当に必要な範囲だけを回っているか確認することが大切です。

外側forと内側whileを組み合わせる

外側の繰り返し回数が決まっていて、各回の中で何回入力されるか分からない場合は、外側forと内側whileの組み合わせが自然です。

グループごとに正の整数の個数と合計を求めるプログラム

ファイル名:9_9_7.c

#include <stdio.h>

int main(void)
{
    int groups;

    printf("グループ数を入力してください > ");
    scanf("%d", &groups);

    for (int group = 1; group <= groups; group++) {
        int value;
        int sum = 0;
        int count = 0;

        printf("---- %dグループ目 ----\n", group);
        printf("正の整数を入力してください(0以下で終了)> ");
        scanf("%d", &value);

        while (value > 0) {
            sum += value;
            count++;

            printf("正の整数を入力してください(0以下で終了)> ");
            scanf("%d", &value);
        }

        printf("入力個数 = %d\n", count);
        printf("合計値 = %d\n", sum);
    }

    return 0;
}
実行結果の例
グループ数を入力してください > 2
---- 1グループ目 ----
正の整数を入力してください(0以下で終了)> 10
正の整数を入力してください(0以下で終了)> 25
正の整数を入力してください(0以下で終了)> 5
正の整数を入力してください(0以下で終了)> 0
入力個数 = 3
合計値 = 40
---- 2グループ目 ----
正の整数を入力してください(0以下で終了)> 8
正の整数を入力してください(0以下で終了)> 12
正の整数を入力してください(0以下で終了)> -1
入力個数 = 2
合計値 = 20

外側のforは、最初に入力されたgroups回だけ処理します。グループ数はすでに決まっているため、forが向いています。

内側のwhileは、0以下が入力されるまで続きます。何個の正の整数が入力されるかは事前に分からないため、whileが適しています。

sumとcountは、外側のforの中で0に初期化しています。これにより、新しいグループが始まるたびに集計結果がリセットされます。

図形表示では外側と内側の仕事を分解する

多重ループで図形を表示するときは、1行をどのような部品で構成するかを先に考えます。

中央寄せした図形なら、1行につき次の3つの処理が必要です。

  • 先頭の空白を表示する
  • 記号を指定個数だけ表示する
  • 1行の最後で改行する
入力された幅に合わせてプラス記号のひし形を表示するプログラム

ファイル名:9_9_8.c

#include <stdio.h>

int main(void)
{
    int width;

    printf("ひし形の幅を入力してください > ");
    scanf("%d", &width);

    if (width <= 0) {
        printf("1以上の整数を入力してください。\n");
        return 0;
    }

    int max_marks;

    if (width % 2 == 0) {
        max_marks = width - 1;
    } else {
        max_marks = width;
    }

    for (int marks = 1; marks <= max_marks; marks += 2) {
        int spaces = (width - marks) / 2;

        for (int i = 0; i < spaces; i++) {
            printf(" ");
        }

        for (int i = 0; i < marks; i++) {
            printf("+");
        }

        printf("\n");
    }

    for (int marks = max_marks - 2; marks >= 1; marks -= 2) {
        int spaces = (width - marks) / 2;

        for (int i = 0; i < spaces; i++) {
            printf(" ");
        }

        for (int i = 0; i < marks; i++) {
            printf("+");
        }

        printf("\n");
    }

    return 0;
}
実行結果の例1:奇数を入力した場合
ひし形の幅を入力してください > 7
   +
  +++
 +++++
+++++++
 +++++
  +++
   +
実行結果の例2:偶数を入力した場合
ひし形の幅を入力してください > 6
  +
 +++
+++++
 +++
  +

widthが偶数のときは、最大の記号数をwidth - 1にしています。記号数を1、3、5のような奇数で増減させるためです。

上半分では、外側のforがmarksを2ずつ増やします。下半分では、marksを2ずつ減らします。

それぞれの行では、2つの内側ループが働きます。

  • 1つ目の内側forが先頭の空白を表示する
  • 2つ目の内側forが+を表示する
  • 2つの内側ループが終わった後で改行する

多重ループを選ぶときは、外側と内側をまとめて考えるのではなく、それぞれに次の質問をすると整理しやすくなります。

  • 繰り返す回数や範囲は決まっているか
  • 終了のタイミングは入力や状態によって変わるか
  • 内側の変数を外側の1周ごとに初期化する必要があるか
  • 各ループの更新によって、いつか継続条件が偽になるか

回数や範囲が主役ならfor、終了条件が主役ならwhileを選ぶことで、多重ループの意図がコードから伝わりやすくなります。