制御構造3(分岐)

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

世間では「選択=分岐」と云われることもあるが, 厳密には「選択=条件分岐」(選択と分岐の組み合わせ)ですよ. 分岐だけ単独で,選択と組み合わせなくても使えます.
教科書の該当範囲:第3.3節の後半(3.3.4〜3.3.5)

基礎知識

例外処理の例
分岐制御文
break文とcontinue文

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

while (反復条件) {	// ← continue の着地点
	...
	if (終了条件) break;	// ループ外へジャンプ⤵ 
	...
	if (継続条件) continue;	// ループ内の先頭へジャンプ⤴ 
	...
}
...		 ← break の着地点(ループ外,ループ直後)

補足:

goto文

ソースコード記述方法:

ラベル1:		// ← 着地
	...
	goto ラベル1;	// ラベル1へジャンプ⤴
	...
	goto ラベル2;	// ラベル2へジャンプ⤵
	...
ラベル2:	// ← 着地
	...

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

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

BEGIN:
	if (...) {		// while (...) { と同等
		...
		if (...) goto END;	// if (...) break と同等
		...
		if (...) goto BEGIN;	// if (...) continue と同等
		...
		goto BEGIN;	// } と同等
	}
END:
	...

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

分岐を使いすぎなプログラムはまるで スパゲティ www
闇雲に,break や continue でも済む場合には, goto は使わないでおこう.
世間では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段階だけしか脱出できない. という訳で,この例では,goto 使用やむなし,むしろ利点あり.

内側のループで break しても, 外側のループは continue してしまう.

本日の作業

タスク1:成績処理プログラム

前回のタスク1の 合計計算プログラム total.cwhile版(前判定ループ)を改造し, 平均計算プログラム average.c を作成してみよう.

Step 1. 平均計算(前判定/後判定ループ版)
前回の total.c では,最終的に do-while版に書き換えてしまった. while版に戻し,さらに,平均計算の処理を追加することになる.
#include <stdio.h>

int main(void)
{
	int	total = 0;	// 合計
	int	x;		// データ
	int	n = 0;		// データの個数
	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 -= xn--)が無駄手間となっている. 前回は加算と入力の順序を変えて,調整を不要にできたが, 今回も同様にしてみると:

...
{
	...
	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 = ...
	...
}

これで処理順序は自然になった. しかし,同じ条件式が whileif とに重複し, 新たな無駄となった. また,ごく普通のハズの入力データ 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 は,エラー処理や二重ループなど, 特に必要な場合にだけ使うこと!!


タスク2:最大値の抽出

(このタスクについては,余裕ある人だけ挑戦すれば良い.)

前回のタスク5(余裕ある人向け問題)の部分的な解答例 max.c

#include <stdio.h>

int main(void)
{
	int	x;			// データ
	int	max = 0;		// 最大値

	do {
		printf("得点 > ");
		scanf("%d", &x);

		if (x > max) max = x;	// 最大値を更新
			// 入力終了の負の場合まで最大を調べていて,明らかに無駄
	} while (x >= 0);		// 非負なら反復を続行

	printf("最大値 = %d\n", max);

	return (0);
}

このように,連接・選択・反復だけを利用した場合, 少々不自然なコードになってしまう.

跳躍を利用すれば, 等価なプログラムを より単純なコードで記述できる.

...
int main(void)
{
	...
	while (1) {
		...
		if (x < 0) break;	// 負なら反復を終了
		if (x > max) max = x;	// 最大値を更新
	}
	...
	return (0);
}
ここでも,反復条件(非負なら続行)を逆転し,終了条件(負なら終了)に変更した.

タスク3:ロシアンルーレット

(本日の課題2です.)

下記の基本ソースコードを元にして, ロシアンルーレット のゲームプログラム rr.c を作成せよ.

実行例:

$ ./rr
ロシアンルーレットを始めるよー
弾倉を回す?
1以上:回して撃つ,0:そのまま撃つ,-1以下:パス > 0
カチッ...
セーフ
弾倉を回す?
1以上:回して撃つ,0:そのまま撃つ,-1以下:パス > 0
カチッ...
バーン...
終了

$ ./rr
...
1以上:回して撃つ,0:そのまま撃つ,-1以下:パス > 1
...
1以上:回して撃つ,0:そのまま撃つ,-1以下:パス > 1
...
:
:
1以上:回して撃つ,0:そのまま撃つ,-1以下:パス > 1
カチッ...
バーン...
終了

$ ./rr
...
1以上:回して撃つ,0:そのまま撃つ,-1以下:パス > -1
...
1以上:回して撃つ,0:そのまま撃つ,-1以下:パス > -1
...
:
:
:

パスだと,無限に続く... これは変ですが,気にしないでおきましょう.

rr.c の基本ソースコード: (処理順序を変えないこと!!)

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
	int	r;	// 弾倉内の実弾の位置が入るよ
		// 位置がゼロの時に撃つと発射・終了
	int	x;	// 入力

	printf("ロシアンルーレットを始めるよー\n");
	r = rand()%6;		// 実弾の初期位置をランダムに決定

	while (1) {
		printf("弾倉を回す?\n");
		printf("1以上:回して撃つ,0:そのまま撃つ,-1以下:パス > ");
		scanf("%d", &x);

		if (...) ...;		// パス
		if (...) r = rand()%6;	// 弾倉を回す(位置を再設定)
		printf("カチッ...\n");
		if (...) ...;		// 発射・終了

		printf("セーフ\n");
		r--;			// 弾倉を1段階だけ回す
	}
	printf("バーン...\n");
	printf("終了\n");

	return (0);
}
タスク4:図形描画

(このタスクについては,余裕ある人だけ挑戦すれば良い.)

下記の考え方と基本ソースコードに従って, 直角二等辺三角形を描くプログラム draw.c を作成せよ.

考え方:

draw.c の基本ソースコード:

#include <stdio.h>

int main(void)
{
	int	x, y;	// 列番号(右方向の位置),行番号(下方向の位置)
	int	r = 10;		// サイズ
	int	n = 3;		// 間引きの周期

	for (y = 0; y < r; y++) {
		if (...) ...;		// n行から1行を間引く
		for (x = 0; x < r; x++) {
			if (...) ...;	// 斜め半分を切り落とす
			printf("#");
		}
		printf("\n");
	}

	return (0);
}
間引きのヒント:剰余「%」を使えば簡単.
三角化のヒント:正方形の対角線は x == y.

実行例:(進化の過程)

$ ./draw-1	# 正方形には見えないよね?
##########
##########
##########
##########
##########
##########
##########
##########
##########
##########

$ ./draw-2	# 間引きしてみた
##########
##########
##########
##########
##########
##########
##########

$ ./draw-3	# 切り落としてみた
#
###
####
######
#######
#########
##########

本日の課題

レポートを提出せよ.

質問 Q1〜Q3 に回答し, 電子メールで提出せよ.