制御構造3(分岐)

前2回に紹介した3つの制御構造「連接,選択,反復(ループ)」を組み合わせれば, あらゆる処理手順を表現できる. しかし,例外的な処理(反復の終了やエラーの回避など)を記述する場合には, これらだけではプログラムの全体像の見通しが悪くなることが多い. こんな場合には,第4の制御構造「分岐(跳躍,ジャンプ)」も利用するとよい.

教科書の該当範囲:なし

基礎知識

例外処理の例
分岐制御文
えーと,break文は以前, 選択制御文 switch-case のブロック内でも利用していたなー.

大抵の場合,これらを選択制御(if文)と組み合わせて, 条件分岐として利用することになる. それぞれ単独で無条件分岐としても使える.

例外処理では,これらを的確に利用すれば, ソースコードをコンパクト(短く)かつクリア(明解)に記述できる場合が多い. しかし,使い過ぎると逆効果にもなりかねない. 例外処理のためだけに利用すること. 乱用は禁止.


break文とcontinue文

ソースコード記述方法:(while との併用例)

while (反復条件) {		// ← continue の着地点
	...
	if (終了条件) break;		// ループの下へジャンプ
	...
	if (継続条件) continue;		// ループの上へジャンプ
	...
}
...				// ← break の着地点

goto文

ソースコード記述方法:

	goto ラベル;	// ラベルへジャンプ
	...
ラベル:			// ← ここに着地
	...

ここで,ラベルの名前は自由に設定できるが,英語大文字(と数字)を使おう. 英語小文字の変数や命令文と区別し易くするためね.

この goto文 を使えば,処理の順序を自由に変えられる. たとえば,if と goto の組み合わせれば反復制御にもなる. while ループの構成例:

BEGIN:
	if (反復条件) {		// while (...) {
		...
		if (...) goto END;	// break
		...
		if (...) goto BEGIN;	// continue
		...
		goto BEGIN;	// }
	}
END:
	...

なお,これはあくまでも goto の意味を理解するための例であって, このような利用方法を推奨しているわけではない. 使い所を間違えると理解困難な処理順序も実現できてしまうので, 不適切な goto の乱用は特に禁止する.

適切な goto の利用例のひとつとして, 二重ループからの脱出を紹介しておく:

	int	i, j;
	for (i = 0; i < 10; i++) {
		for (j = 0; j < 10; j++) {
			...
			if (...) goto END;	// break ではダメ
			...
		}
	}
END:
	...

なお,break では内側のループ1段階だけしか脱出できない.


実習の準備

いつもの通り,作業用ディレクトリを準備しよう:

$  cd
$  mkdir  c-0530
$  cd  c-0530

成績処理プログラム

Step 1. 平均計算(前判定/後判定ループ版)

前回の 合計計算プログラム total.c の while版(前判定ループ)を改良し, 平均計算プログラム average.c を作成してみよう:

$  vim  average.c	# エディタは何でも OK
#include <stdio.h>

int main(void)
{
	int	total = 0;	// 合計
	int	n = 0;		// データの個数
	int	x;		// データ
	double	avg;		// 平均

	x = 0;			// ダミーのデータ(最初の反復条件の分)
	while (x >= 0) {
		printf("%d 番目の非負整数(-1 で終了)> ", n);
		scanf("%d", &x);	// データを入力
		total += x;		// 合計を加算
		n++;			// 個数をカウント
	}
	total -= x;		// 合計を調整(最後の x = -1 の分)
	n--;			// 個数を調整(    〃    )

	avg = (double)total/(double)n;	// 平均を計算
	printf("平均 = %f\n", avg);

	return (0);
}
$  cc  average.c  -o  average
$  ./average
...

前回と同様に,このソースコードでは, 入力データの初期化(x = 0)や 合計と個数の調整(total -= x と n--)が無駄手間となっている. 前回は加算と入力の順序を変えて,調整を不要にできたが, 今回も同様にしてみると:

...
{
	...
	n = -1;		// 個数を調整(最初の n++ の分)
	x = 0;		// ダミーのデータ(最初の反復条件の分)
	while (x >= 0) {
		total += x;	// 加算(処理順序の変更)
		n++;		// 計数(   〃   )
		printf(...);
		scanf(...);
	}
/*			// 調整は不要に
	total -= x;
	n--;
*/
	avg = ...;
	...
}

これでも正しく平均を計算できるが, 個数の初期値を1つ少なく数えているし(個数がマイナスって何よ?), 入力前に合計を計算している. これらは不自然で気持ちが悪いし, ソースコードの意味を理解しづらいので, 後々の改造の際,悩まされることになるだろう.

もし,while(前判定)ループを do-while(後判定ループ)に書き換えても, これらの状況は変わらない.


Step 2. 反復の中断(中判定ループ版)

処理手順の不自然を解消するために, if を使ってもよいだろう. これまでの知識だけで改造すると:

...
{
	...
	n = 0;		// 個数は普通にゼロから数えるよ
	x = 0;
	while (x >= 0) {
		printf(...);
		scanf(...);

		// x < 0 なら加算・計数しない,としたいんだけど...
		if (x >= 0) {	// 逆に,x >= 0 なら加算・計数...
			total += x;
			n++;
		}
	}
/*			// 調整は不要に
	total -= x;
	n--;
*/
	avg = ...
	...
}

これで処理順序は自然になった. しかし,同じ条件式が while と if とに重複し, 新たな無駄となった. また,ごく普通の入力データ x >= 0 が特別視されており, 新たな不自然が発生してしまった.

こんな場合, ループ内で break文 を利用し, ループの外側へジャンプすればよい:

...
{
	...
	n = 0;
//	x = 0;		// ダミー不要に
	while (1) {	// 無条件反復に
		printf(...);
		scanf(...);

		// x < 0 なら加算・計数しない
		if (x < 0) break;	// x < 0 ならループを脱出

		total += x;
		n++;
	}
	avg = ...		// break 直後,ここに着地
	...
}

これで無駄も不自然も解消され, 処理手順について理解しやすいコードにできた. なお,if-break では, while の反復条件(反復継続の条件 x >= 0)とは逆に, 終了条件(反復中断の条件 x < 0)を使っていることにも注意しよう.

また,if (...) break; は if (...) { break; } の短縮形ね. ブロック { } 内の 命令文; が1個だけの場合,括弧 { } を省略してよい.


Step 3. 反復の続行

入力ミスによる取消処理を追加してみよう. もし,入力データの値が 100 を超える場合をミスとし, そのデータを加算・計数しないことにする. これまでの知識だけで改造すると:

...
{
	...
	n = 0;
	while (1) {
		printf(...);
		scanf(...);
		if (x < 0) break;

		// 入力ミスなら加算・計数しない,としたいんだけど...
		if (x <= 100) {	// 逆に,入力ミスでなければ加算・計数...
			total += x;
			n++;
		}
	}
	avg = ...
	...
}

ここでもまた,入力ミスではない普通のデータが特別視され, 不自然となってしまった.

こんな場合には, ループ内で continue文 を利用し, ループの先頭へジャンプすればよい:

...
{
	...
	n = 0;
	while (1) {			// continue 直後,ここに着地
		printf(...);
		scanf(...);
		if (x < 0) break;
		if (x > 100) continue;	// 入力ミスならループの先頭へ

		total += x;
		n++;
	}
	avg = ...
	...
}

Step 4. エラーの処理

入力ミスがあった時,エラーメッセージを表示する等, ユーザに注意を促してみよう:

...
{
	...
	while (1) {
		...
		if (x < 0) break;
		if (x > 100) {
			printf("エラー:範囲外の値\n");
			printf("続けますか?(0:終了/その他:続行)> ");
			scanf("%d", &x);
			if (x == 0) break;
			continue;
		}

		total += x;
		n++;
	}
	...
}

これでは,例外的な処理(エラー処理)が多すぎて, 本質的な処理(平均計算)が目立たなくなってしまった. つまり,ソースコードの全体像の見通しが悪くなっており, これも理解・改造の障害となりかねない.

ループ内には例外の判定だけを残し, 例外処理の本体については, goto文を利用して ソースコードの片隅へ追い払おう:

...
{
	...
LOOP:
	while (1) {
		...
		if (x < 0) break;	// goto END でもよい
		if (x > 100) goto ERROR;

		total += x;
		n++;
	}
END:
	avg = ...;
	...
	return (0);

ERROR:
	printf("エラー:範囲外の値\n");
	printf("続けますか?(0:終了/その他:続行)> ");
	scanf("%d", &x;);
	if (x == 0) goto END;		// break の着地点へ
	goto LOOP;			// continue の着地点へ
}

これで,メインループ(本質的な処理の反復部分)が コンパクト(短く)かつクリア(明解)になった.

何度もクドいですが... goto は,エラー処理や二重ループなど, 特に必要な場合にだけ使うこと.


本日の課題

スパルタ式の九九練習プログラム 99.c を作成せよ. 誤答の場合,正答するまで何度でも繰り返すこと.

なお,ソースコードでは,選択・反復・分岐を適切に使い分けること. さらに,インデントを適切に整えること.

実行例:(メッセージは適当に変えてOK)

$  ./99
スパルタ式九九練習

何の段に挑戦する?> 3
3 * 1 =?> 3
◯
3 * 2 =?> 6
◯
3 * 3 =?> 9
◯
...
3 * 9 =?> 27
◯
なかなかやるじゃないかー

何の段に挑戦する?> 7
7 * 1 =?> 7
◯
7 * 2 =?> 14
◯
7 * 3 =?> 23
☓ もう一度!!
7 * 3 =?> 22
☓ もう一度!!
7 * 3 =?> 21
◯
7 * 4 =?> -1
ギブアップ?(1:yes/0:no)> 0
7 * 4 =?> -1
ギブアップ?(1:yes/0:no)> 1
出なおして来い!!
終了.

$  ./99
スパルタ式九九練習

何の段に挑戦する?> -1
また来いよ!!
終了.

提出:


(c) 2018, yanagawa@kushiro-ct.ac.jp