先頭へ戻る

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

Programming Place Plus トップページC言語編

先頭へ戻る

このページの概要

以下は目次です。


無限ループ

for文、while文、do文によって作られるループは、条件式を満たしているあいだだけ繰り返し処理を行います。ふつう、いつかは条件式を満たさなくなって、ループから抜け出すようにしますが、ときには条件式をつねに満たし続けるループを作る場合があります。

たとえば、次のように while文を書いたらどうなるでしょう?

#include <stdio.h>

int main(void)
{
    while (1) {
        puts("!!!");
    }
}

実行結果:

!!!
!!!
!!!
!!!
(続く)

条件式のところに書いた 1 は、「0以外の値」ですから「真」です。1 は整数定数なので変化することがありませんから、ずっと「真」のままということになります。したがって、このループは一度始まったら最後、二度と抜け出すことはありません。

このようなループは、内側に書いた処理を無限に繰り返すことになるため、無限ループ (inifinite loop) と呼ばれます。

条件式をどうするべきか考えたすえに、決して条件式が「偽」にならないループを作ってしまう間違いを犯すことがあります。

#include <stdio.h>

int main(void)
{
    int num = 15;

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

実行結果:

15
13
11
9
7
5
3
1
-1
-3
-5
-7
-9
(続く)

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


一方で、わざと無限ループを作ることがあります。これには2つのケースがあります。

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

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

わざと無限ループを作る場合、先ほどのように while文の条件式に 1 と書くか、for文を使って次のように書くのが一般的です。

#include <stdio.h>

int main(void)
{
    for (;;) {
        puts("!!!");
    }
}

実行結果:

!!!
!!!
!!!
!!!
(続く)

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

break文

わざと無限ループを作るが、実際にはいつかは抜け出したいケースでは、条件式が「偽」になっていなくてもループから抜け出す方法が必要です。そのための方法として、break文 (break statement) があります。

break文はすでに switch文のところでも登場していますが(第11章)、そのほかの用途として、ループを抜けるという使い方があります。

無限ループから抜け出すための break文の使い方を確認してみましょう。次のプログラムは、標準入力から入力された文字列を出力するという処理を繰り返しますが、“exit” が入力されたらプログラムを終了します。

#include <stdio.h>

int main(void)
{
    while (1) {
        puts("Please enter the string.");
        char input_string[128];
        fgets(input_string, sizeof(input_string), stdin);
        if (input_string[0] == 'e' && input_string[1] == 'x' && input_string[2] == 'i' && input_string[3] == 't') {
            break;
        }
        printf("%s\n", input_string);
    }
}

実行結果:

Please enter the string.
hello  <-- 入力された文字列
hello

Please enter the string.
world  <-- 入力された文字列
world

Please enter the string.
exit  <-- 入力された文字列

break文が実行されると、break文を取り囲んでいる while、for、do、switch のいずれかのブロック({} のうち、最も内側のものから抜け出します。ループの内側にほかのループがある場合、最も内側のところにある break文は、内側のループからしか抜けられません。ブロックを抜け出した後は、ブロックの後ろにある続きの処理が実行されます。

この手のプログラムは do文を使って書けば、無限ループを作る必要はないですが、無限ループ+break文の方が分かりやすく感じられることも多く、こちらの方法を採ることはよくあります。


ところで、“exit” を判定するために、1文字ずつ分解して比較しています。これまでにも少し触れたことがありますが、fgets関数で受け取った文字列の末尾に余分な改行文字があるため、strcmp関数を使って、if (strcmp(input_string, "exit") == 0) のように比較して調べることができないため、このような方法を採っています。

fgets関数が受け取った文字列の末尾の改行を取り除く方法は、第40章で取り上げます。

不完全な方法ですが、sscanf関数を使って回避することもできます。“%s” 変換指定は、空白類文字でない部分だけを対象にするため(第8章)、改行文字を省くことができます。

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

int main(void)
{
    while (1) {
        puts("Please enter the string.");
        char input_string[128];
        fgets(input_string, sizeof(input_string), stdin);

        char s[128];
        sscanf(input_string, "%s", s);  // 末尾の改行を取り除く目的

        if (strcmp(s, "exit") == 0) {
            break;
        }
        puts(s);
    }
}

実行結果:

Please enter the string.
hello  <-- 入力された文字列
hello

Please enter the string.
world  <-- 入力された文字列
world

Please enter the string.
exit  <-- 入力された文字列

ループがネストしているとき

ループがネストしている場合に、break文によって終了させられるのは、break文が書かれているループだけです

次のプログラムは、1~9 の整数同士を掛け合わせた結果の表を出力しますが、答えが 50以上になるものを取り除いています。

#include <stdio.h>

int main(void)
{
    for (int i = 1; i <= 9; ++i) {
        for (int j = 1; j <= 9; ++j) {
            int ans = i * j;

            if (ans >= 50) {
                break;  // A に飛ぶ
            }
            printf("%d ", ans);
        }
        // A

        puts("");
    }
}

実行結果:

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
7 14 21 28 35 42 49
8 16 24 32 40 48
9 18 27 36 45

for文による二重のループが構築されており、break文は内側の for文の中にあります。この break文によって終了させられるのは、内側の for文だけです。コメントにあるとおり、「A」と書かれたところへ移動することになりますが、ここはまだ外側の for文の中です。

そのため、内側にあるループから、一気に2つ以上のループを終了したいときには、1つ1つ終了させていかなければなりません。今度は、答えが 50以上になるものを見つけた時点で、プログラムを終了させるようにしてみます。

#include <stdbool.h>
#include <stdio.h>

int main(void)
{
    for (int i = 1; i <= 9; ++i) {
        bool is_break = false;

        for (int j = 1; j <= 9; ++j) {
            int ans = i * j;

            if (ans >= 50) {
                is_break = true;
                break;  // A に飛ぶ
            }
            printf("%d ", ans);
        }
        // A

        puts("");

        if (is_break) {
            break;
        }
    }
}

実行結果:

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

コメントで「A」と書いた箇所にやってきたとき、内側のループが break文によって終了してきたのか、j <= 9 という条件式が満たされなくなって終了してきたのかを判断するような機能はC言語にはありません。この区別が必要なときには、自前で bool型の変数を使うなどして、自力で区別を付ける必要があります。ここでは、変数 is_break を追加し、break文で終了する直前でだけ true が代入されるようにしています。この変数の値を調べて true であれば、もう1度 break文を実行して、外側の for文からも抜け出します。

goto文

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

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

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

goto ラベル名;

goto文は「使ってはいけない」と言われることもあります。goto文は(同じ関数内であれば)どこへでも移動できるため、処理の流れが非常に分かりにくくなる可能性があります。

とはいえ、goto文を使う方がきれいに書けるケースもあるので、無条件に「使ってはいけない」というのは言いすぎでしょう。ここで紹介する2つのケースは、例外的に「使ってもよいのではないか」と言われています。

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

#include <stdio.h>

int main(void)
{
    for (int i = 1; i <= 9; ++i) {
        for (int j = 1; j <= 9; ++j) {
            int ans = i * j;
            if (ans >= 50) {
                puts("");
                goto loop_end;  // 2つのループを一気に抜ける
            }
            printf("%d ", ans);
        }
        puts("");
    }

loop_end:
    ;
}

実行結果:

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文を使う方がすっきりします。

loop_endラベルの次の行に ; とだけ書いた空文(第1章)がありますが、これは、ラベルとは文に付けられた名前(第11章)であるから、対象の文が必要だからです。


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

#include <stdio.h>

int main(void)
{
    const int num1 = 30;
    const int num2 = 10;
    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) {    // ゼロ除算への対応
        if (input != 0) { goto incorrect; }
    }
    else{
        if (input != num1 / num2) { goto incorrect; }
    }

    puts("Very good!");
    return 0;  // 下に進んでしまわないように、return文で終了させる

incorrect:   // ここには、途中で問題に間違えた場合にジャンプしてくる
    puts("Miss.");
}

実行結果:

30 + 10 = ?
40
30 - 10 = ?
20
30 * 10 = ?
300
30 / 10 = ?
3
Very good!

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

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

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

continue文

continue文 (continue statement) はループの内側でのみ使用でき、現在の周回での残りの処理を飛ばして、末尾へ移動するという効果があります。末尾へ移動したあとで、ループの「条件式」を確認します。そのため、ループは継続するかもしれませんし、終了することになるかもしれません。

continue文の構文は単純です。while文、do文、for文の「繰り返したい文」のところに、以下のように書くだけです。

continue;

次のプログラムは、文字列の入力を受け取り、その内容が “exit” なら終了、"" なら何もせずループを続行、それ以外なら入力された文字列をそのまま出力するという動作を取ります。ループを終了させるために break文を、何もせずに続行させるために continue文を使っています。

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

int main(void)
{
    while (1) {
        puts("Please enter the string.");
        char input_string[128];
        fgets(input_string, sizeof(input_string), stdin);

        char s[128] = "";
        sscanf(input_string, "%s", s);  // 末尾の改行を取り除く目的

        if (strcmp(s, "exit") == 0) {
            break;
        }
        if (strcmp(s, "") == 0) {
            continue;
        }
        puts(s);
    }
}

実行結果:

Please enter the string.
abc  <-- 入力した文字列
abc
Please enter the string.
     <-- 入力した文字列(Enterキーを押しただけ)
Please enter the string.
exit  <-- 入力した文字列

if (strcmp(s, "") == 0) のところは else を付けて、else if (strcmp(s, "") == 0) としても構いませんが、事実上まったく同じ意味になることも理解しておきましょう。if (strcmp(s, "exit") == 0) が true になった場合には break文が実行されるので、後続にある if (strcmp(s, "") == 0) のところに来ることはないからです。ただ、どちらの書き方がいいとはっきりいえるほどのものでは無いと思います。

do文や for文での continue文の動作

while文での continue の挙動は想像しやすいと思いますが、do文や for文は少し迷うかもしれません。基本的には同じことで、残りの処理を飛ばして、「条件式」をチェックし、次の周回に入るか、ループを終了するかします。for文の場合は、++i などを書いている箇所の実行が挟まることに注意してください。

do文なら、以下の順番で実行されます。

  1. continue文が実行される
  2. 「条件式」を確認。0以外なら続行、0 なら終了
  3. ループ本体の処理の先頭へ移動

for文の場合は、以下の順番で実行されます。

  1. continue文が実行される
  2. 「式」を実行(for (int i = 0; i < n; ++i) であれば ++i のこと)
  3. 「条件式」を確認。0以外なら続行、0 なら終了
  4. ループ本体の処理の先頭へ移動

ループがネストしているとき

break文と同様、ループがネストしている場合に continue文の効果が及ぶのは、continue文が書かれているループだけです

次のプログラムは、1~9 の整数同士を掛け合わせた結果の表を出力しますが、答えが奇数になるものを取り除いています。

#include <stdio.h>

int main(void)
{
    for (int i = 1; i <= 9; ++i) {
        for (int j = 1; j <= 9; ++j) {
            int ans = i * j;

            if (ans % 2 != 0) {
                continue;  // 奇数だったら、残りの処理を飛ばして A へ移動
            }
            
            printf("%d ", ans);

            // A (内側の for文の末尾)
        }
        puts("");
    }
}

実行結果:

2 4 6 8
2 4 6 8 10 12 14 16 18
6 12 18 24
4 8 12 16 20 24 28 32 36
10 20 30 40
6 12 18 24 30 36 42 48 54
14 28 42 56
8 16 24 32 40 48 56 64 72
18 36 54 72


練習問題

問題① 次の do文を、同じ結果になるように、無限ループを使ったかたちに書き換えてください。

#include <stdio.h>

int main(void)
{
    int value = 0;
    do {
        puts("Please enter the integer.");
        char input_string[32];
        fgets(input_string, sizeof(input_string), stdin);
        sscanf(input_string, "%d", &value);

        if (value > 0) {
            printf("%d is positive number.\n", value);
        }
        else if (value < 0) {
            printf("%d is negative number.\n", value);
        }
    } while (value != 0);
}

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

#include <stdio.h>

int main(void)
{
    int i;

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

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

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


解答ページはこちら

参考リンク


更新履歴

’2018/6/5 第18章から練習問題⑬を移動してきて、練習問題⑤とした。

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

’2009/5/4 新規作成。



前の章へ (第16章 while文)

次の章へ (第18章 整数の表現方法)

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

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
Twitter でツイート Twitter をフォロー LINE で送る
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー