C言語編 第17章 処理の流れを制御する

先頭へ戻る

この章の概要

この章の概要です。

無限ループ

ループは、条件式を満たす間だけ繰り返し処理を行います。 ほとんどの場合、いつかは条件式を満たさなくなり、ループから抜け出すようにするものです。 しかし、ときには、条件式を常に満たすループを作ることがあります。 例えば、次のように while文を書いたらどうなるでしょう?

while( 1 ){
    printf( "!!!\n" );
}

整数の 1 は、「0以外の値」ですから「真」です。 従って、この while文の条件式は、常に「真」のままであり、「偽」になることはあり得ません。 このようなループは、内側に書いた処理を無限に繰り返すことになるため、無限ループと呼ばれます。

無限ループは、間違って作ってしまうこともあります。 つまり、条件式をどうするべきか考えた挙句、決して、「偽」になることがないループを作り込んでしまうことがあります。

int num = 15;
while( num != 0 ){
    printf( "%d\n", num );
    num -= 2;
}

この例では、変数num の値が 0 で無いときに繰り返しを行います。これはつまり、いつかは変数 num の値が 0 になるつもりで書いている訳です。しかし、初期値は 15 で、-2 を繰り返すようになっているため、「…, 3, 1, -1, -3,…」と変化していき、0 をすり抜けてしまいます。

一方で、わざと無限ループを作ることがあります。 この理由は2つあります。
1つには、本当に無限に繰り返すループを作りたいということです。 例えば、物理的なスイッチで電源を投入したら動き始め、電源を切るまで動き続けるようなプログラムでは、 勝手に main関数を抜け出してしまっては困るので、無限ループを作ることになります。

もう1つは、条件式は常に「真」にした方が書きやすいケースです。 例えば、初回だけは必ず実行して欲しいループを作るとき、while文や for文を使うとやや書きづらくなり、 do文を使うとやや分かりづらくなることがあります。 この場合は、while文や for文を使いつつも、条件式を常に「真」にしてしまうことで、解消できることがあります。

わざと無限ループを作る場合、先ほどのように while文の条件式に 1 とだけ書くか、for文を使って、

for( ;; ){
    printf( "!!!\n" );
}

のように書くのが一般的です。 これ以外にも書きようはありますが、変な書き方をせずに一般的な方法に従いましょう。

break文

無限ループから抜け出す典型的な方法は、break文を使うことです。

break文は、2つの役割を持っています。1つは、ループから強制的に抜け出すこと。もう1つは、switch文から抜け出すことです。switch文から抜け出す用途に関しては、第12章で解説済みです。

無限ループから抜け出すための break文の使い方を確認してみましょう。 次のサンプルプログラムは、第16章の do文によるループを書き変えたものです。

#include <stdio.h>
#include <string.h>

int main(void)
{
    char password[16] = "password";
    char try_password[16];
    char str[16];


    while( 1 ){
        puts( "正しいパスワードを入力して下さい。" );
        fgets( str, sizeof(str), stdin );
        sscanf( str, "%s", try_password );

        if( strcmp( try_password, password ) == 0 ){
            break;
        }
    }

    puts( "入力に成功しました。" );

    return 0;
}

実行結果:

正しいパスワードを入力して下さい。
passward
正しいパスワードを入力して下さい。
password
入力に成功しました。

break文が実行されると、break文自身を取り囲んでいる while、for、do、switch のいずれかのブロック({})から抜け出します。 実は、ループと switch文とで分けて考える必要などなく、上記のように考えてしまって問題ありません。 ブロックを抜け出した後は、ブロックの後ろにある続きの処理が実行されます。

このサンプルプログラムでは、正しいパスワードが入力されたときに、break文を使って、while のループから脱出しています。 結局のところ、while文の条件式のところに書く条件を逆にしたものを、if文で確認しているだけです。 ループの条件式は「繰り返す条件」を書きますが、break文を実行するための条件は「ループを終える条件」を書かないといけませんから、 真と偽が反転するはずです。

ところで、do文が使われる機会が少ない理由の1つが、このサンプルプログラムに現れています。 無限ループと break文を使うことによって、do文は実質的に不要になってしまうのです。
そもそも do文で実現しようとする理由が、初回の条件判定が難しい(初回だけは必ず真になってほしい)という点にあったので、 無限ループにしてしまえば、必ずループの中に進入させられます。 あとは望ましいタイミングを if文で判断して、break文を実行させれば、ループから抜け出すことができます。


break文によって抜け出すのは、break文を取り囲んでいる最も内側のブロックだけであるということに注意が必要です。 例えば、ループの内側に switch文がある場合、その switch文に属する break文を実行した場合、switch文のブロックからしか抜け出しません。

同様に、ループの内側に別のループがあるようなネストしたループで、内側のループ内にある break文があるとします。 この break文によって抜け出すのは、内側のループだけです。 break文は、2つ以上のブロックを一気に抜け出すような能力は持ち合わせていません。

次のサンプルプログラムで確認してみましょう。

#include <stdio.h>

int main(void)
{
    int i;
    int j;


    for( i = 1; i <= 9; ++i ){
        for( j = 1; j <= 9; ++j ){
            printf( "%d ", i * j );

            /* 答えが 50 を超えるものが現れたら、終了させたい */
            if( i * j > 50 ){
                break;	/* この break文によって抜けるのは、内側のループだけ */
            }
        }
        printf( "\n" );
    }

    return 0;
}

実行結果:

1 2 3 4 5 6 7 8 9
2 4 6 8 10 12 14 16 18
3 6 9 12 15 18 21 24 27
4 8 12 16 20 24 28 32 36
5 10 15 20 25 30 35 40 45
6 12 18 24 30 36 42 48 54
7 14 21 28 35 42 49 56
8 16 24 32 40 48 56
9 18 27 36 45 54

九九表のようなものを出力していますが、答えに 50 を超えるものが現れたら終了するようにしたいとします。 しかし実行結果を見ると、何度も 50 を超える値が登場しており、どうやらうまくいっていないようです。

コメントにも書かれているように、break文によって抜け出すのは内側のループだけであり、外側のループからは抜け出せていませんから、 次の段の計算へ進んでしまっている訳です。

正しく書くための手段は幾つかあります。 後で紹介する、goto文を使う方法が簡単で綺麗ですが、ここでは break文だけで済ませる方法を見ておきましょう。

#include <stdio.h>

int main(void)
{
    int i;
    int j;


    for( i = 1; i <= 9; ++i ){
        for( j = 1; j <= 9; ++j ){
            printf( "%d ", i * j );

            /* 答えが 50 を超えるものが現れたら、終了させたい */
            if( i * j > 50 ){
                break;	/* この break文によって抜けるのは、内側のループだけ */
            }
        }
        printf( "\n" );

        /* もう1度判定 */
        if( i * j > 50 ){
            break;	/* この break文によって抜けるのは、外側のループ */
        }
    }

    return 0;
}

実行結果:

1 2 3 4 5 6 7 8 9
2 4 6 8 10 12 14 16 18
3 6 9 12 15 18 21 24 27
4 8 12 16 20 24 28 32 36
5 10 15 20 25 30 35 40 45
6 12 18 24 30 36 42 48 54

今回は 54 が現れた時点で終了できています。

break文を 2回使うように書き変えたのですが、同じ判定を 2回行っている訳で、あまり綺麗ではありません。 基本的に、ネストしたループから一気に抜け出す際、break文を繰り返し実行させる方法は、どう書いてもあまり綺麗になりません。

goto文

goto文は、処理の実行位置を、 同じ関数内の別の場所へ一気に移動させます

goto文で移動する先は、ラベルを使って表現します。 ラベルは、switch文で使う caseラベルや defaultラベルと同様「○○○:」のような形式で記述します。 ただし、caseラベルや defaultラベルへは移動できません。

goto文を使って、ラベルへ移動するには、以下のように書きます。 ここで、error はラベル名です。

goto error;

goto文は「使ってはいけない」と言われることもあります。 goto文は(同じ関数内であれば)どこへでも移動できるため、処理の流れが非常に分かりにくくなる可能性があります。
とはいえ、goto文を使う方が綺麗なケースも存在するので、無条件に「使ってはいけない」というのは言い過ぎでしょう。 ここで紹介する2つのケースは、例外的に「使ってもよいのではないか」と言われています。

まず、1つ目は、break文を2回以上使わないと抜け出せないような、深いブロックから抜け出したいときです。 break文のところで取り上げたサンプルプログラムを、goto文を使って書き変えてみます。

#include <stdio.h>

int main(void)
{
    int i;
    int j;


    for( i = 1; i <= 9; ++i ){
        for( j = 1; j <= 9; ++j ){
            printf( "%d ", i * j );

            /* 答えが 50 を超えるものが現れたら、終了させたい */
            if( i * j > 50 ){
                printf( "\n" );		/* 途中で抜け出すので、改行しておく */
                goto loop_end;
            }
        }
        printf( "\n" );
    }
loop_end:	/* goto文はここへ飛んでくる */

    return 0;
}

実行結果:

1 2 3 4 5 6 7 8 9
2 4 6 8 10 12 14 16 18
3 6 9 12 15 18 21 24 27
4 8 12 16 20 24 28 32 36
5 10 15 20 25 30 35 40 45
6 12 18 24 30 36 42 48 54

このように、ネストしたループ、switch文から一気に外側へ抜け出す場合には、goto文を使う方がすっきりします。

goto文を活用できるもう1つの場面は、エラー処理を1か所にまとめるような場合です。

#include <stdio.h>

int question(int num1, int num2);

int main(void)
{
    question( 30, 10 );

    return 0;
}

/*
    加減乗除の問題を行う。
    引数:
        num1: 演算子の左側にくる整数。
        num2: 演算子の右側にくる整数。
    戻り値:
        全ての問題に正解すれば 0
        1問でも間違えると 1
*/
int question(int num1, int num2)
{
    char str[40];
    int input;


    /* 加算 */
    printf( "%d + %d = \?\n", num1, num2 );
    fgets( str, sizeof(str), stdin );
    sscanf( str, "%d", &input );
    if( input != num1 + num2 ){ goto incorrect; }

    /* 減算 */
    printf( "%d - %d = \?\n", num1, num2 );
    fgets( str, sizeof(str), stdin );
    sscanf( str, "%d", &input );
    if( input != num1 - num2 ){ goto incorrect; }

    /* 乗算 */
    printf( "%d * %d = \?\n", num1, num2 );
    fgets( str, sizeof(str), stdin );
    sscanf( str, "%d", &input );
    if( input != num1 * num2 ){ goto incorrect; }

    /* 除算 */
    printf( "%d / %d = \?\n", num1, num2 );
    fgets( str, sizeof(str), stdin );
    sscanf( str, "%d", &input );
    if( num2 == 0 ){	/* 0除算エラーへの対応 */
        if( input != 0 ){ goto incorrect; }
    }
    else{
        if( input != num1 / num2 ){ goto incorrect; }
    }

    puts( "全問正解!" );
    return 0;

incorrect:   /* ここには、途中で問題に間違えた場合にジャンプしてくる */
    puts( "正しくありません" );
    return 1;
}

実行結果:

30 + 10 = ?
40
30 - 10 = ?
20
30 * 10 = ?
300
30 / 10 = ?
3
全問正解!

出題される問題に対して、間違った答えを入力すると、goto文によって、incorrectラベルへ移動してきます。 このラベルのところには、不正解のときの処理を記述します。

不正解を検出したとき、それぞれの場所に直接処理を記述する場合と違って、goto文を使うと、処理を一か所にまとめられます。 これまでに何度か書いているように、同じ意味の同じコードを複数書くのは避けるべきであり、 1か所に処理をまとめるのは良いスタイルです。

なお、この例のような使い方をする場合、ラベルの前で return文を書いておかないと、 正常な場合にも、処理がラベルの後ろまで突き抜けて行ってしまいます。 ラベルは所詮、移動先を表す目印に過ぎず、それ以外の効力は何もありません。

continue文

continue文は、現在の回のループを打ち切って、次の回のループへ進ませる命令です。 for文の場合は、「再設定式」が実行されてから、次へ進みます。 次の回のループへ進んだとき、当然「条件式」がチェックされます。 これが偽になれば、ループ全体を終えることになります。

次のサンプルプログラムは、標準入力から得点を受け取り集計し、平均点を計算しますが、30点に満たない入力は無視するというものです。

#include <stdio.h>

int main(void)
{
    char str[40];
    int score;
    int total_score = 0;
    int count = 0;

    while( 1 ){
        puts( "得点を入力して下さい(負数を入力すると終了します)" );
        fgets( str, sizeof(str), stdin );
        sscanf( str, "%d", &score );

        /* 負数の入力で終わり */
        if( score < 0 ){
            break;
        }

        /* 目的に合わないデータは処理せず、次の入力へ進む */
        if( score < 30 ){
            continue;
        }

        /* 目的に合ったデータだけがここに来る */
        total_score += score;
        count++;
    }

    printf( "合計点は %d\n", total_score );
    printf( "平均点は %d\n", total_score / count );

    return 0;
}

実行結果:

得点を入力して下さい(負数を入力すると終了します)
100
得点を入力して下さい(負数を入力すると終了します)
10
得点を入力して下さい(負数を入力すると終了します)
50
得点を入力して下さい(負数を入力すると終了します)
-1
合計点は 150
平均点は 75

if文を使って判断を下した上で、continue文を呼び出しています。 break文もそうですが、普通はこのように、ある条件を満たしたときに実行する形になるはずです。

実のところ、continue文を使わなくても、他の形に書き換えることが可能です。 今回のサンプルプログラムでも、結局のところ、score の値が 30以上のときにだけ後続の処理を行わせればいいのですから、 後続の処理の部分を if文で囲んでしまうだけで同じ結果を生み出せます(実際に書き換えるのは、練習問題で行います)。

ループの中身が長く複雑になるようなとき、処理対象にしないデータを早い段階で選別するために continue文を利用するのが、賢い使い方でしょう。 早く選別して、今回のループを終えさせたり、次の回へ進ませたりすることで、後続の処理の流れを追わなくて済むため、 プログラムを理解しやすくなります。

なお、continue文は、break文と違って switch文との関わりは一切ありません。 そのため、switch文の内側に書いた continue文と break文とでは、効果が及ぶ対象が異なることに注意が必要です。

return文

これまでに何度も使っていますが、今一度、return文を取り上げておきます。 なぜなら、この章で取り上げた break文、goto文、continue文、そして return文は全て、 処理の流れを一気に変えるジャンプ文(跳躍文)に分類されるからです。

return文は、関数から抜け出し呼び出し元へ戻す命令です。 このとき、関数の戻り値の型が void型以外の場合は、その型に応じた戻り値を1個だけ指定することができます。 void型の場合は、単に「return;」と書きます。

break文と goto文のところで、ネストしたループから抜け出す方法に触れましたが、実は return文も候補の1つになり得ます。 この場合は、関数ごと終了することになります。

#include <stdio.h>

int main(void)
{
    int i;
    int j;


    for( i = 1; i <= 9; ++i ){
        for( j = 1; j <= 9; ++j ){
            printf( "%d ", i * j );

            /* 答えが 50 を超えるものが現れたら、終了させたい */
            if( i * j > 50 ){
                printf( "\n" );    /* 途中で終わるので、改行しておく */
                return 0;          /* return で終わらせる */
            }
        }
        printf( "\n" );
    }

    return 0;
}

実行結果:

1 2 3 4 5 6 7 8 9
2 4 6 8 10 12 14 16 18
3 6 9 12 15 18 21 24 27
4 8 12 16 20 24 28 32 36
5 10 15 20 25 30 35 40 45
6 12 18 24 30 36 42 48 54


練習問題

問題① continue文のサンプルプログラムを、continue文を使わない形に書き変えて下さい。

問題② 次のプログラムは何をしているのでしょうか?

#include <stdio.h>

int main(void)
{
    int i;

    i = 0;
loop:
    printf( "%d\n", i );
    ++i;
    if( i < 10 ){ goto loop; }

    return 0;
}

問題③ 問題②のプログラムを、goto文をなくし、for文だけで書き変えて下さい。

問題④ 問題③で書き変えたプログラムを更に、無限ループを使うように書き変えて下さい。


解答ページはこちら

参考リンク

更新履歴

'2018/2/9 全面的に文章を見直し、修正を行った。

'2009/5/4 新規作成。





前の章へ(第16章 同じ処理を繰り返す(do文))

次の章へ(第18章 理解の定着・小休止②)

C言語編のトップページへ

Programming Place Plus のトップページへ


このエントリーをはてなブックマークに追加
rss1.0 取得ボタン RSS