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

このエントリーをはてなブックマークに追加

この章の概要

この章の概要です。

無限ループ

通常のループは、条件式を満たす間だけ繰り返し処理を行います。 普通、いつかは条件式を満たさなくなり、ループから抜け出します。

ところが、中には条件式を常に満たしてしまうようなループも存在します。 例えば、次のように while文を書いたらどうなるでしょう?

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

整数の 1 は、「0以外の値」ですから「真」を表します。 従って、この while文の条件式は、常に「真」のままであり、「偽」になることはあり得ません。

このようなループは、無限ループと呼ばれます。 実は、わざと無限ループを作ることもあります。 その場合、無限ループからは、条件式が「偽」になったときに抜け出すことがあり得ないので、代わりの手段で抜け出します。 この章では、そのための手段を幾つか紹介します。

無限ループを使う場面は、「基本的に終わることがないループ」を作りたいときです(そのままですが)。 例えば、1度起動したら電源を切るまで終わることがないような機械制御、ゲームなどが考えられます。

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

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

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

break文

break文は、2つの役割を持っています。 1つは、ループから強制的に抜け出すこと。もう1つは、switch文から抜け出すことです

switch文から抜け出す点に関しては、既に第12章で switch文を説明したときに確認しています。 ここではループから抜け出す点について説明します。

次のプログラムは、第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文とで分けて考える必要などなく、上記のように考えてしまって問題ありません。

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


break文によって抜け出すのは、最も内側のブロックだけであるということに注意が必要です。 例えば、ループの内側に switch文がある場合、その switch文に属する break文から抜け出すのは当然 switch文のブロックです。
同様に、ループの内側に別のループがあるようなネスト構造の場合、内側のループに属する 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文は、同じ関数内の別の場所へ一気に移動させます。 移動先はラベルで表現します。ラベルは、switch文で使う caseラベルや defaultラベルと同様「○○○:」のような形式で記述します。

goto文を使って、ラベルへ移動するには、

goto error;

のように書きます。ここで、error はラベル名です。 ただし、caseラベルや defaultラベルへは移動できません。

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

まず、1つ目の例として、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

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

ループの中身が長く複雑になるようなとき、処理対象にしないデータを早い段階で選別するために continue文を利用するのが、賢い使い方でしょう。 先ほど書いたように、continue文を使わずに if文だけで記述すると、後続の処理はインデントが1段階深くなります。 あまり深いレベルまで記述が及ぶのは、読み易さの観点から避けたいところです。

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

return文

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

return文は、関数から抜け出し呼び出し元へ戻す命令です。このとき、戻り値を1個だけ指定することができます
戻り値が指定できるかどうかは、関数の宣言で、戻り値の型を void以外にしているかで決まります。 戻り値が void型のときには、return文は戻り値を伴いませんし、void型以外なら、1つの戻り値を伴わなくてはなりません。

第9章で触れましたが、戻り値を文字列にしたい場合は注意が必要で、この時点の知識では無理です。


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文を使わない形に書き変えて下さい。

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

#include <stdio.h>

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

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

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


解答ページはこちら

参考リンク

更新履歴

'2009/5/4 新規作成。



前の章へ

次の章へ

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

Programming Place Plus のトップページへ