前回,条件反復として,前判定ループ while と 後判定ループ do-while について勉強した.
しかし,実用的なプログラムでは,ループの前後ではなく, ループの途中で条件を判定し,反復から抜け出したり, 反復処理の一部を飛び越したい場合もよくある. そこでCでは,これらの処理を便利に記述するために, 分岐文 break,continue, および goto が用意されている.
教科書 pp.78-81 の説明もよく読もう.
まず,break の動作と利点について理解しよう.
前回の averager2.c のソースコードの要点は次のようなものだった:
... int f = 1; // 反復フラグの初期化 printf("複数個の非負整数(最後に -1) > "); while (f) { // 反復フラグの判定(f == 0 で反復終了) scanf("%d", &x); if (x < 0) { // 番兵データの判定 f = 0; // 反復フラグの設定(反復終了へ) } else { total += x; n++; } } ... // 結果表示
これは,ソースコード的には前判定ループにはなっているものの, 実質的には,ループの途中(if 文)で反復条件を判定していることになる. また,反復処理(反復フラグの初期化,設定,判定)のコードが 離れた場所に分散しているため, 処理の流れを見通しづらくなってしまっている.
このような中判定ループ場合,次のように, break を利用すれば, より簡潔に記述できるようになる:
// 反復フラグは不要 ... while (1) { // 無条件ループ ... if (x < 0) break; // 番兵が来たらループ外へジャンプ total += x; n++; } ... // break のジャンプ先(ここに着地)
これで,フラグ変数と if-else が必要なくなったため, コードが少なくなり,インデントも浅くなり, 処理の流れを見通しやすくなった. 実際に averager2.c のコードを書き換えて, 動作が変わらないことを確認してみよう.
なお,break は,while だけでなく, do-while と for(それと switch)でも同様に, ブロック { } の内部から外部へジャンプするために利用できる.
次に,continue について理解しよう.
前のセクションで改造したaverager2.c を更に改造し, 入力データの値の範囲を「100以下」の非負整数に制限してみたい. もし範囲外の値が入力された場合,とりあえずは,無視するとしよう. これまでの知識だけでは,コードは次のようになるだろう:
...
while (1) {
printf("[#%03d] 100以下の非負整数(最後に -1) > ", n);
scanf(...);
if (x < 0) break;
if (x <= 100) { // 100以下なら
total += x;
n++;
}
}
...
実際に,これを averager3.c とし,動作を確認してみよう. なお,前のプログラムでは,複数のデータを一度に入力していたが, このプログラムでは,データを1個ずつ入力することにした.
このコードを更に改造し,continue を使えば, 次のように(少しだけだが)簡単になる:
...
while (1) {
printf(...);
scanf(...);
if (x < 0) break;
if (x > 100) continue; // 100より大ならループの先頭へ
total += x;
n++;
}
...
これで,インデントが(1 段階だけだが)減少し, コードが全体的に単純かつコンパクトになった. なお,選択構造が「〜だったら〜する」から 逆の「〜じゃなかったら〜しない」に, 変えられているが, この改造の前後でプログラムの動作は何も変わらない. (if 文の推敲との相乗効果.)
今回紹介した例は元々簡単なコードだったので, break と continue によるソースコードのコンパクト化の効果は, 非常に薄いものに思えるかもしれない. しかし,長く複雑な処理の場合では, このちょっとした簡単化が効いてくる.
特に,エラーなどの例外的な処理について, break と continue で先に処理しておけば, その後は,本質的な処理だけに専念できるということがポイント. また,インデントが浅くなるので,ソースコードも見易くなり, 無用な間違いを予防できる.
もちろん,continue は,break と同様, while だけでなく do-while と for のブロック { } 内でも利用できる.
最後に,goto について理解しよう.
前のセクションのaverager3.c を元にして, エラー処理(エラー状態の検出とエラー状態からの回復)の機能を追加してみたい. なお,範囲外のデータが入力された場合をエラーとする.
これまでの知識では...:
...
while (1) {
...
if (x > 100) { // 範囲外の場合
printf("エラー:範囲外の値\n");
printf("続ける?(0:終了/その他:続行)> ");
scanf("%d", &x);
if (x == 0) break;
continue;
}
total += x;
n++;
}
...
では,これを averager4.c として,実行してみよう.
ところで,このソースコードでは, 入力エラーと入力終了のような「例外的な処理」が多く, 合計計算のような「このプログラムの本質的な処理」が目立たないため, プログラムの全体像が把握しづらくなってしまっている.
例外的な処理を目立たないように記述するには, goto 文が有効である.書き換えよう:
main() { ... LOOP: while (1) { ... ... break; // goto RESULT; でも同じ if (x > 100) goto ERROR; // 範囲外の場合 ERROR へ total += x; n++; } RESULT: printf(...); return; // プログラムの終了 ERROR: // 例外的な処理をループ外にまとめておく printf("エラー:範囲外の値\n"); printf("続ける?(0:終了/その他:続行)> "); scanf("%d", &x); if (x == 0) goto RESULT; // break; だったもの goto LOOP; // continue; だったもの }
これで,本質的な処理の見通しが良くなった.
なお,LOOP,RESULT,ERROR は, ジャンプ先の ラベル(名札) であり, 変数名等と同様,プログラマが自由に命名できる.
なお,goto 文については,本当に注意して使うべきものだ. なぜなら,goto を乱用したソースコードは, 非常に解読しづらくなりがちだからだ. 例外的な処理だけに限定すること.
他の使用例と注意事項については, 教科書 pp.79-81 を参照せよ. また,goto を使う場合, ラベルのインデントの例外にも注意しよう.
averager4.c を元にして, 平均値,最大値,最小値および得点分布を出力するプログラム analyzer.c を作成せよ. ただし,break,continue,goto を適切に使い分けること.
ヒント:
Fig.A は,ループ,break,continue,goto による 処理順序の変化を示している.
ポイントは次の通り: