C言語の基本|多重ループの終了方法

内側だけ止めるのか、処理全体を止めるのか。多重ループの出口を正しく選んで、思いどおりに終了できるコードを書こう。

表の行と列を順番に調べたり、複数の値の組み合わせを確認したりするときには、ループの中に別のループを置く多重ループが活躍します。

ただし、多重ループの中でbreak文を使うときは、どの範囲まで終了するのかを正しく理解しておく必要があります。

内側のループでbreak文を実行しても、外側のループまで自動的に終了するわけではありません。break文が終了させるのは、そのbreak文を直接囲んでいる一番内側のループです。

そのため、外側のループまで終了したい場合には、次のような工夫が必要になります。

  • フラグ変数に終了状態を記録する
  • 外側のループでもフラグ変数を確認する
  • goto文でループの外にあるラベルへ移動する

ここでは、break文だけを使った場合の動きから、フラグ変数とgoto文で多重ループ全体を終了する方法まで、順番に確認していきます。

多重ループでbreak文を使ったときの基本動作

2重ループは、外側のループと内側のループで構成されます。

for (外側の制御) {
    for (内側の制御) {
        処理;
    }
}

内側のループでbreak文を実行すると、終了するのは内側のループだけです。

for (外側の制御) {
    for (内側の制御) {
        if (終了条件) {
            break;
        }
    }

    外側の処理;
}

break文で内側のループを抜けた後は、外側のループに書かれている次の処理へ進みます。その後、外側のループの継続条件が真なら、次の繰り返しが始まります。

図1:内側のbreak文が終了させる範囲

この図から分かること

内側のループにあるbreak文は、内側のループだけを終了させます。

break文が実行されると、内側のループに残っている処理は実行されません。ただし、外側のループは終了していないため、次のように処理が続きます。

  • 内側のループから抜ける
  • 外側のループ内にある次の処理へ進む
  • 外側の更新処理を行う
  • 外側の継続条件を判定する
  • 条件が真なら外側の次の繰り返しへ進む

break文を見つけたときは、どのループに直接囲まれているのかを確認することが大切です。

break文が終了させる範囲

break文の動作は、ループが何重であるかではなく、break文がどのループの中に置かれているかで決まります。

break文を実行する場所終了する範囲
1重ループの中そのループを終了する
2重ループの内側内側のループだけを終了する
2重ループの外側外側のループを終了する
3重ループの最も内側最も内側のループだけを終了する

3重以上のループでも考え方は同じです。break文は、最も近くにあるループまたはswitch文から抜けます。

内側のループだけを終了する動きを確認する

ここでは、3行4列に並んだ検査パネルを順番に確認します。2行3列で異常が見つかったら、その行の検査だけを終了します。

外側のfor文が行を担当し、内側のfor文が列を担当します。

異常が見つかった行の検査だけを終了するプログラム

ファイル名:9_11_1.c

#include <stdio.h>

int main(void)
{
    int row;
    int col;

    for (row = 1; row <= 3; row++) {
        for (col = 1; col <= 4; col++) {
            printf("%d行%d列のパネルを検査しています。\n",
                   row, col);

            /* 2行3列で異常が見つかったら、その行の検査を終了する */
            if (row == 2 && col == 3) {
                printf("異常を検出しました。\n");
                break;
            }
        }

        printf("%d行目の検査が終わりました。\n", row);
    }

    printf("パネルの検査処理が終わりました。\n");

    return 0;
}
実行結果の例
1行1列のパネルを検査しています。
1行2列のパネルを検査しています。
1行3列のパネルを検査しています。
1行4列のパネルを検査しています。
1行目の検査が終わりました。
2行1列のパネルを検査しています。
2行2列のパネルを検査しています。
2行3列のパネルを検査しています。
異常を検出しました。
2行目の検査が終わりました。
3行1列のパネルを検査しています。
3行2列のパネルを検査しています。
3行3列のパネルを検査しています。
3行4列のパネルを検査しています。
3行目の検査が終わりました。
パネルの検査処理が終わりました。

2行3列でbreak文が実行された後の流れ

終了条件は次の部分です。

if (row == 2 && col == 3) {
    printf("異常を検出しました。\n");
    break;
}

rowが2、colが3になったときに条件が真となり、break文が実行されます。

このbreak文が終了させるのは、colを使っている内側のfor文です。そのため、2行4列の検査は行われません。

内側のループを抜けた後は、外側のfor文の中にある次のprintfへ進みます。

printf("%d行目の検査が終わりました。\n", row);

そのため、2行目の検査が終わったというメッセージは表示されます。

続いて外側のfor文のrow++が実行され、rowが3になります。外側の継続条件row <= 3は真なので、3行目の検査が始まります。

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

  1. 2行3列で異常を検出する
  2. 内側のbreak文を実行する
  3. 2行4列の検査を行わずに内側のループを終了する
  4. 2行目の終了メッセージを表示する
  5. 外側のrowを3に更新する
  6. 3行目の検査を開始する

この動作から、内側のbreak文だけでは検査全体を止められないことが分かります。

内側のループだけを終了すればよい場面

内側のbreak文だけで十分な場合もあります。

たとえば、行ごとに目的のデータを1つだけ探す処理です。1つの行で目的の値が見つかったら、その行の残りを調べる必要はありません。しかし、次の行は引き続き調べたい場合があります。

処理の目的内側のbreak文だけでよい理由
各行から最初の一致だけを探す一致後も次の行は調べたい
行ごとに異常の有無を確認する1つ見つければ、その行の残りは不要
各グループから代表値を探す次のグループでも同じ探索を続けたい

このような処理では、外側を続けながら内側だけを終了する動きが役立ちます。

外側のループまで終了したい場面

異常が1つでも見つかったら検査全体を止める場合は、内側のbreak文だけでは足りません。

外側まで終了したい代表的な場面には、次のようなものがあります。

場面外側まで終了する理由
2次元データから目的の値を探す見つかった後は探索を続ける必要がない
表形式のデータを検査する重大な異常が見つかったら全体を停止したい
複数の値の組み合わせを試す条件を満たす最初の組が見つかればよい
行列を順番に調べる目的の位置を特定した後の比較が不要になる

C言語のbreak文には、複数のループを一度に終了する機能はありません。そのため、外側も終了するための処理を追加します。

代表的な方法は、フラグ変数を使う方法とgoto文を使う方法です。

フラグ変数で終了状態を記録する

フラグ変数は、特定の出来事が起きたかどうかを記録するための変数です。

異常が見つかったことを記録するなら、次のように0と1を使えます。

int error_found = 0;

最初は異常が見つかっていないため0にします。異常を検出したら1を代入します。

error_found = 1;

内側のループをbreak文で終了した後、外側でerror_foundを確認します。

if (error_found == 1) {
    break;
}

これにより、内側と外側を順番に終了できます。

図2:フラグ変数で内側と外側を順番に終了する

この図から分かること

フラグ変数は、内側で発生した出来事を外側へ伝える役割を持ちます。

内側で異常を見つけた場合は、次の順番で処理します。

  • フラグ変数に1を代入する
  • break文で内側のループを終了する
  • 外側でフラグ変数を確認する
  • もう一度break文を実行して外側のループを終了する

2つのbreak文は、それぞれ別のループを終了させています。1つのbreak文で2重ループ全体を終了しているわけではありません。

フラグ変数を使って検査全体を終了する

先ほどと同じ3行4列のパネルを検査します。ただし、今回は2行3列で異常が見つかったら、3行目へ進まずに検査全体を終了します。

フラグ変数で内側と外側のループを終了するプログラム

ファイル名:9_11_2.c

#include <stdio.h>

int main(void)
{
    int row;
    int col;
    int error_found = 0;

    for (row = 1; row <= 3; row++) {
        for (col = 1; col <= 4; col++) {
            printf("%d行%d列のパネルを検査しています。\n",
                   row, col);

            /* 異常を記録して内側のループを終了する */
            if (row == 2 && col == 3) {
                printf("異常を検出しました。\n");
                error_found = 1;
                break;
            }
        }

        /* 内側で異常が見つかっていたら外側も終了する */
        if (error_found == 1) {
            break;
        }

        printf("%d行目の検査が終わりました。\n", row);
    }

    printf("異常検出のため、検査処理を終了します。\n");

    return 0;
}
実行結果の例
1行1列のパネルを検査しています。
1行2列のパネルを検査しています。
1行3列のパネルを検査しています。
1行4列のパネルを検査しています。
1行目の検査が終わりました。
2行1列のパネルを検査しています。
2行2列のパネルを検査しています。
2行3列のパネルを検査しています。
異常を検出しました。
異常検出のため、検査処理を終了します。

フラグ変数を使った処理の流れ

プログラムの開始時点では、error_foundを0にしています。

int error_found = 0;

0は、まだ異常が見つかっていないことを表します。

2行3列に到達すると、error_foundに1を代入します。

error_found = 1;
break;

直後のbreak文によって、まず内側のfor文が終了します。

その後、外側のループ内にある次のif文が実行されます。

if (error_found == 1) {
    break;
}

error_foundは1なので、この条件は真です。2つ目のbreak文が実行され、外側のfor文も終了します。

その結果、次の処理は行われません。

  • 2行目の終了メッセージ
  • 2行4列の検査
  • 3行目のすべての検査

フラグ変数によって異常の発生が外側へ伝わり、2段階でループ全体を終了しています。

フラグ変数を使う利点

フラグ変数を使うと、終了する理由を変数名で表せます。

観点内容
状態を記録できる異常や発見などの出来事を後から確認できる
意図が伝わりやすいerror_foundなどの名前から終了理由が分かる
通常の制御文で書けるif文とbreak文の組み合わせで処理できる
後続処理にも利用できるループ終了後の表示や判定にフラグを使える

フラグ名は、役割が伝わるものにしましょう。

  • found
  • error_found
  • stop_requested
  • end_flag

flagだけでは何を記録しているのか分かりにくいことがあります。異常を表すのか、目的の値が見つかったことを表すのかが伝わる名前を選ぶと読みやすくなります。

フラグ変数を使うときの注意点

フラグ変数を使う方法にも注意点があります。

  • ループを始める前に初期値を設定する
  • 条件が成立した場所でフラグの値を変更する
  • 内側を抜けた後でフラグを確認する
  • 必要な外側ループごとに終了判定を置く
  • フラグの意味が分かる変数名にする

ループが3重、4重と深くなると、各段階でフラグ変数を確認する処理が増えることがあります。その場合は、処理全体の見通しがよいかを確認しましょう。

goto文でループの外へ直接移動する

C言語のgoto文を使うと、指定したラベルへ直接移動できます。

基本的な形は次のとおりです。

goto ラベル名;

/* 別の処理 */

ラベル名:
    処理;

gotoの後ろに移動先となるラベル名を書きます。移動先には、同じ名前とコロンを記述します。

多重ループを終了したい場合は、ループの外側にラベルを置き、内側からそのラベルへ移動します。

for (外側の制御) {
    for (内側の制御) {
        if (終了条件) {
            goto END_LOOP;
        }
    }
}

END_LOOP:
    終了後の処理;

この方法では、内側のbreak文と外側のbreak文を順番に実行する必要がありません。終了条件が成立した場所から、ループの外へ直接移動できます。

goto文で検査全体を終了する

ここでも3行4列のパネルを検査します。2行3列で異常を検出したら、goto文でループの外側にあるEND_INSPECTIONへ移動します。

goto文で2重ループの外へ直接移動するプログラム

ファイル名:9_11_3.c

#include <stdio.h>

int main(void)
{
    int row;
    int col;

    for (row = 1; row <= 3; row++) {
        for (col = 1; col <= 4; col++) {
            printf("%d行%d列のパネルを検査しています。\n",
                   row, col);

            /* 異常を検出したら2重ループの外へ移動する */
            if (row == 2 && col == 3) {
                printf("異常を検出しました。\n");
                goto END_INSPECTION;
            }
        }

        printf("%d行目の検査が終わりました。\n", row);
    }

END_INSPECTION:
    printf("検査処理を終了します。\n");

    return 0;
}
実行結果の例
1行1列のパネルを検査しています。
1行2列のパネルを検査しています。
1行3列のパネルを検査しています。
1行4列のパネルを検査しています。
1行目の検査が終わりました。
2行1列のパネルを検査しています。
2行2列のパネルを検査しています。
2行3列のパネルを検査しています。
異常を検出しました。
検査処理を終了します。

goto文が実行されたときの流れ

2行3列で条件が真になると、次の文が実行されます。

goto END_INSPECTION;

すると、プログラムの制御はEND_INSPECTIONラベルまで移動します。

END_INSPECTION:
    printf("検査処理を終了します。\n");

途中にある次の処理は実行されません。

  • 内側のfor文の残り
  • 外側のfor文の残り
  • 2行目の終了メッセージ
  • 3行目の検査

goto文はbreak文とは異なり、指定したラベルへ直接移動します。そのため、深い多重ループから一度に抜けることができます。

図3:フラグ変数とgoto文による終了方法の違い

この図から分かること

フラグ変数を使う方法は、各ループを順番に終了します。

内側でフラグを立て、内側をbreak文で終了した後、外側でフラグを確認して外側もbreak文で終了します。処理の段階がコードに表れやすいため、基本的にはこちらの方法が分かりやすいでしょう。

goto文を使う方法では、終了条件が成立した場所からループ外のラベルへ直接移動します。深い多重ループでも一度に抜けられますが、移動先が増えると処理を追いにくくなります。

フラグ変数とgoto文を比較する

どちらの方法でも多重ループ全体を終了できますが、コードの流れには違いがあります。

観点フラグ変数goto文
終了方法各ループでフラグを確認して順番に抜けるラベルへ直接移動する
読みやすさ状態の変化を追いやすい移動先が離れると追いにくい
記述量フラグの設定と確認が必要比較的短く書ける
深い多重ループ確認処理が増えやすい一度に外へ移動できる
学習時の使いやすさ基本として理解しやすい使用場所を慎重に選ぶ必要がある
後続処理での利用フラグの状態を再利用できる状態を残すには別の変数が必要

2重ループ程度であれば、まずフラグ変数を使う方法を考えるとよいでしょう。何が起きたためにループを終了したのかが、変数名から読み取りやすくなります。

goto文は、ループが深く、フラグ確認が何度も必要になる場合に役立つことがあります。ただし、使用する場合は移動先を明確にし、必要な場所に限定しましょう。

goto文を多用すると読みにくくなる理由

通常のプログラムは、上から下へ処理を追うことで流れを理解できます。

goto文があると、実行位置が離れたラベルへ移動します。goto文とラベルが複数ある場合、次にどこが実行されるのかを一つずつ探さなければなりません。

次のような状態になると、特に読みにくくなります。

  • goto文が何か所にもある
  • ラベルが複数ある
  • 前方と後方の両方へ移動する
  • ループ内へ飛び込むような流れがある
  • goto文の移動先が関数内で大きく離れている

多重ループから抜けるためにgoto文を使う場合は、終了位置へ向かって前方に移動する単純な形にすると、流れを比較的追いやすくなります。

どの終了方法を選ぶか

多重ループの終了方法は、終了させたい範囲から考えます。

終了させたい範囲選択しやすい方法
内側のループだけ内側でbreak文を実行する
外側も含めた2重ループフラグ変数を設定し、各ループでbreak文を実行する
深い多重ループ全体フラグ変数またはgoto文を検討する
関数内の処理を終了したい関数の設計に応じてreturnも検討する

今回扱う中心はbreak文、フラグ変数、goto文ですが、終了したい範囲が関数全体であれば、returnで関数を終了する設計も考えられます。

大切なのは、短く書ける方法を選ぶことだけではありません。どの条件で、どこまで処理を終了するのかが読み手に伝わる方法を選びましょう。

間違えやすい動きを確認する

多重ループでは、次のような勘違いが起こりやすくなります。

間違えやすい考え方実際の動き
内側でbreakすればすべてのループが終わる終わるのは内側のループだけ
break後は外側の残りも実行されない外側に残っている処理は実行されることがある
フラグを1にすれば自動的に終了するフラグを判定してbreakする処理が必要
goto文なら何も考えずに使える移動先や飛ばされる処理の確認が必要
ループが深くてもbreakの動きは変わる常に最も近いループまたはswitch文を終了する

特にフラグ変数は、値を変更するだけではループを終了しません。フラグを設定する処理と、その値を確認してbreak文を実行する処理の両方が必要です。

行と列の役割から終了範囲を考える

多重ループでは、外側と内側がそれぞれ異なる役割を担当していることが多くあります。

たとえば、外側が行、内側が列を担当している場合は、次のように考えられます。

  • 現在の行だけを打ち切るなら内側のbreak文
  • すべての行の確認を打ち切るなら外側も終了する
  • 異常の発生を外側へ伝えるならフラグ変数
  • 深い階層から特定の終了位置へ移るならgoto文

break文を書く前に、何を終了したいのかを日本語で整理してみると判断しやすくなります。

たとえば、現在の行の残りを調べないのであれば、内側のbreak文だけで十分です。一方、異常を検出したので検査全体を停止するのであれば、外側も終了しなければなりません。

読みやすい終了処理を書くためのポイント

多重ループを終了するときは、次の点を確認しましょう。

  • 終了条件をif文で明確にする
  • break文がどのループを終了するか確認する
  • 内側を抜けた後に実行される処理を確認する
  • 外側まで終了するなら、その意図をコードに表す
  • フラグ変数には役割が分かる名前を付ける
  • フラグ変数の初期化、設定、確認をそろえる
  • goto文の移動先は分かりやすい位置に置く
  • goto文とラベルを必要以上に増やさない
  • 終了によって実行されなくなる処理を確認する

多重ループでは、break文を見つけただけで処理全体が終わると判断してはいけません。break文が直接属しているループを確認し、その外側にどのような処理が残っているのかを追うことが大切です。