これまでに紹介した3つの制御構造「連接,選択,反復(ループ)」を組み合わせれば, あらゆる処理手順を表現できる. しかし,例外的な処理(反復の終了やエラーの回避など)を記述する場合には, これらだけではプログラムの全体像の見通しが悪くなることが多い. こんな場合には,第4の制御構造「分岐(跳躍,ジャンプ)」も利用するとよい.
大抵の場合,これらを選択制御(if文)と組み合わせて, 条件分岐として利用することになる. それぞれ単独で無条件分岐としても使える.
例外処理では,これらを適切に利用すれば, ソースコードをコンパクト(短く)かつクリア(明解)に記述できる場合が多い. しかし,使い過ぎると逆効果にもなりかねない. 例外処理のためだけに利用すること!! 乱用は禁止.
ソースコード記述方法:(while との併用例)
while (反復条件) { // ← continue の着地点
...
if (終了条件) break; // ループ外へジャンプ⤵
...
if (継続条件) continue; // ループ内の先頭へジャンプ⤴
...
}
... ← break の着地点(ループ外,ループ直後)
補足:
ソースコード記述方法:
ラベル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 の乱用は特に禁止する.
適切な goto の利用例のひとつとして, 二重ループからの脱出を紹介しておく:
int i, j;
for (i = 0; i < 10; i++) {
for (j = 0; j < 10; j++) {
...
if (...) goto END; // break ではダメ
...
}
}
END:
...
なお,break では内側のループ1段階だけしか脱出できない. という訳で,この例では,goto 使用やむなし,むしろ利点あり.
前回のタスク1の 合計計算プログラム total.c の while版(前判定ループ)を手本として, 平均計算プログラム average.c を作成してみよう.
#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 -= x と n--)が無駄手間となっている. 前回の total.c では,加算と入力の順序を変えて,調整を不要にできた. 今回の average.c でも同様にしてみると:
...
{
...
n = -1; // 個数を調整(最初の n++ の分)
x = 0; // ダミーのデータ(最初の反復条件の分)
while (x >= 0) {
total += x; // 加算(処理順序の変更)
n++; // 計数( 〃 )
printf(...);
scanf(...);
}
/* // 調整は不要に
total -= x;
n--;
*/
avg = ...;
...
}
これでも正しく平均を計算できるが, 個数の初期値を1つ少なく数えているし(個数がマイナスって何だよ?), 入力前に合計を計算している. これらは不自然で気持ちが悪いし, ソースコードの意味を理解しづらいので, 後々の改造の際,悩まされることになるだろう.
もし,while(前判定)ループを do-while(後判定ループ)に書き換えても, これらの状況は変わらない.
処理手順の不自然を解消するために, 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個だけの場合,括弧 { } を省略してよい.
入力ミスによる取消処理を追加してみよう. もし,入力データの値が 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 = ...
...
}
入力ミスがあった時,エラーメッセージを表示する等, ユーザに注意を促してみよう:
...
{
...
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 は,エラー処理や二重ループなど, 特に必要な場合にだけ使うこと!!
(このタスクについては,余裕ある人だけ挑戦すれば良い.)
前回のタスク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);
}
(本日の課題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);
}
(このタスクについては,余裕ある人だけ挑戦すれば良い.)
下記の考え方と基本ソースコードに従って, 直角二等辺三角形を描くプログラム 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);
}
実行例:(進化の過程)
$ ./draw-1 # 正方形には見えないよね? ########## ########## ########## ########## ########## ########## ########## ########## ########## ########## $ ./draw-2 # 間引きしてみた ########## ########## ########## ########## ########## ########## ########## $ ./draw-3 # 切り落としてみた # ### #### ###### ####### ######### ##########
質問 Q1〜Q3 に回答し, 電子メールで提出せよ.