05 月 13 日(水)1-2h

制御構造(1)選択

複雑なプログラムを簡単に設計・実装できるようになるために, 「構造化プログラミング」の考え方について理解しよう.

実はすでに,アルゴロジックと KTurtle でも, 説明はしていなかったが,この考え方を使っていた. 今回は,頭の中を整理しよう.

教科書 pp.67-73 を参考にしながら,作業を進めて行こう.


構造化プログラミングと NS チャート

一般に,次の3種類の制御構造 だけを組み合わせれば, どんなに複雑な手続きでも記述できる:

構造化プログラミングと呼ばれるプログラム作成の流儀では, これら3つの制御構造だけを利用してソースコードを記述する. 構造化されたソースコードでは,全体像がわかり易くなるので, この授業でも,基本的には,この方法論を採用する.

なお,他の制御構造として,跳躍(ジャンプ,goto)もある. 処理の全体像がわかりづらくなってしまう原因になり得るので, 本当に必要な場合以外,これを使ってはならない.

ただし,例外処理(エラーで強制終了させるなど)の場合などでは, 逆に,わかり易くなるので,goto の利用を推奨する. 後日説明.

制御構造を直観的に理解するための道具として, Fig.1 のような NS チャート(Nassi-Schneiderman chart)を紹介しておく.

Fig.1. 制御構造と NS チャート

Fig.1 は,次のようなソースコードと対応している:

// 連接
処理A
処理B
処理C
処理D		
// 選択
if ( 条件 ) {
	処理A
	処理B
	処理C
} else {
	処理A'
	処理B'
	処理C'
}		
// 反復
for (i = 0; i < n; i++) {
	処理A
	処理B
	処理C
}				
「選択」について, ソースコードではすべての処理を1列に並べて記述しており, 「分岐」とか「場合分け」というイメージから程遠い. 一方,NS チャートでは処理を2列に分離しており, 実際の動作のイメージを的確に視覚化している. 今後は,このようなイメージを脳内に描きながら, 手続きをソースコードへ変換して行こう.

以下,今回は「選択構造」について勉強しよう.


条件式

選択構造での分岐条件や反復構造での反復条件を指定するために, 次のような条件式(または関係式)を利用する:

詳しくは,教科書 pp.51-52 を参照.


if

真/偽,Yes/No,1/0,ON/OFF のような二者択一で処理を場合分けするには, if 文を利用する. 一般形は次の通り:

if ( 条件式1 ) {
	条件式1が成立した場合の処理
} else if ( 条件式2 ) {
	その他の場合で条件式2が成立した場合の処理
} else if ( 条件式3 ) {
	...
} else {
	その他の場合の処理
}

また,このコードに対応する NS チャートは,Fig.2 のようになる.

Fig.2. if 文(一般形)の NS チャート

コードがちょっと複雑に見えるが, 次のような省略も可能なので安心してよい:

もっとも単純な例:

悪い例

if ( 条件式 ) 文1; 文2;

これは文法的にはまちがっていないが, 動作的には意図した通りなのか? 混乱を避けるため,次のどちらかの形式で記述すること!!

良い例
if ( 条件式 ) 文1;
文2;
文2は,条件の成立/不成立に関わらず,必ず実行される. 上の悪い例のコードの意味は,これと同じだが...
...それとも...
if ( 条件式 ) {
	文1;
	文2;
}
文2は条件が成立した場合だけ実行される. 上のコードとは意味がまったく異なる.

選択構造の使用例(1)

では,if 文の簡単な使用例として,List 1 を試してみよう.

List 1. na-1.c
// 1 から 20 まで数えます.
// ただし,3 の倍数のときだけ関西弁になります.
main()
{
	int  i;  

	for (i = 1; i <= 20; i++) {
		printf("%d", i);		// 数を表示
		if (i%3 == 0) {		// 3の倍数のとき
			printf("でっせ.\n");	// 関西弁
		} else {		// その他のとき
			printf("です.\n");	// 標準語
		}
	}
}

なお,記号「%」は剰余(割り算の余り)の演算子である. もし,i/n の剰余が 0 の場合,i は n の倍数ということになる.

次に,問題を少しだけ複雑にする. List 1 を改造し,List 2a を作ってみよう.

List 2a. na-2a.c の断片
// 1 から 20 まで数えます.
// ただし,3 の倍数のときだけ関西弁になります.
// しかも 4 の倍数のときには自信がなくなります.
...
		if (i%3 ==0) {
			if (i%4 == 0) {			// 4の倍数のとき
				printf("でっしゃろか?\n");	// 関西弁で自信なく
			} else {			// その他のとき
				printf("でっせ.\n");		// 関西弁
			}
		} else {
			if (i%4 == 0) {			// 4の倍数のとき
				printf("だったかな?\n");		// 標準語で自信なく
			} else {			// その他のとき
				printf("です.\n");		// 標準語
			}
		}
...

これでは,ifelse が多すぎる気がしないか? 同じ if (i%4 == 0) を 2 回も書いているし... というわけで,List 2b のように書き換えてみよう.

List 2b. na-2b.c の断片
...
		if ((i%3 ==0) && (i%4 == 0)) {		// if (i%12 == 0) でも同じ
			printf("でっしゃろか?\n");
		} else if (i%3 == 0) {
			printf("でっせ.\n");
		} else if (i%4 == 0) {
			printf("だったかな?\n");
		} else {
			printf("です.\n");
		}
...

動作は変わらないが, コードは短くなった気がする. しかし,まだ場合分けが複雑な気がしないか?

この問題の条件は「3の倍数」と「4の倍数」という2個だけのハズなのに, ソースコードでは条件が3個にもなってしまっている.

if 文の推敲(すいこう)

if 文には, 同値な複数通りの書き方があり得る. もっとも良い書き方を選りすぐろう.

考え方は数学や論理回路での「式の簡単化」とほぼ同じ. ただし,プログラミングでは「意図の読み取り易さ」が重要. 「短けりゃ短いほど良い」ってものではない. また,これは自然言語(日本語や英語など)での作文でも同様だ.

特に,elseelse if が多く現れると, 全体の見通しが悪くなりがちだ. 使わずに済むときには,else を使わないこと.

もちろん,使うべきときは使ってよい. ただし,控えめに.

また,条件式を充分に吟味(ぎんみ)しよう. たとえば,考えていた条件とは逆の条件にした方がわかり易くなるかもしれない. つまり,「〜である場合,〜する」だけではなく, その逆に,「〜ではない場合,〜する」についても考えてみよう. そして,ソースコード的にわかり易くなる方を採用しよう.

たとえば:

if (x != 0) {
	...
} else {
	return;		// x≠0 なら終了
}

ではなく,次のように書くべき:

if (x == 0) return;	// x=0 なら終了
...

条件を逆にすることで,コードを短くできた.

大して違わないと思うかもしれないが, ブロック内の行数が大きくなった場合, 全体の見通しが大いに違ってくる. 「チリも積もれば山となる.

さらに,論理演算子に頼りすぎるのも悪い兆候だ. たとえば:

if ((条件A) && (条件B)) {
	処理A
	処理B
} else if (条件A) {
	処理A
} else if (条件B) {
	処理B
}

ではなく,次のようにすべき:

if (条件A) { 処理A }
if (条件B) { 処理B }

要するに,プログラミング作業に入る前に, 問題を充分に分析しておこう.

複雑な処理をそのままコーディングすることは誰にでもできる. 複雑なものを単純化するのが「プロの技」だ. 「動けば良い」のは素人レベル.


switch

選択肢がたくさんあるような多重分岐の場合には, 次のように,switch 文を使うと便利だ:

switch ( 式 ) {
case 値1 :
	式が値1に等しい場合の処理
	break;
case 値2 :
	式が値2に等しい場合の処理
	break;
	...
default :
	その他の場合の処理
	break;
}

Fig.3 は,switch 文の NS チャートである.

Fig.3. switch 文(一般形)の NS チャート
同じことは,たくさんの if を書き並べてもできる. しかし,それは非常に見苦しい. List 2a と 2b で見た通り...

なお,switch 文の利用に当たっては, 次のような注意が必要:

詳しく,教科書 pp.71-73 を参照.


選択構造の使用例(2)

List 2a および 2b の問題について, List 3 のように,コードをさらに書き換えてみよう.

List 3. na-3.c
...
	int  kansai;	// 関西弁フラグ
	int  jishin;	// 自信フラグ
...
		kansai = (i%3 == 0) ? 1 : 0;
		jishin = (i%4 == 0) ? 0 : 1;

		switch (kansai*0x10 + jishin) {
		case 0x00: printf("だったかな?\n"); break;
		case 0x01: printf("です.\n"); break;
		case 0x10: printf("でっしゃろか?\n"); break;
		case 0x11: printf("でっせ.\n"); break;
		default: break;
		}
...
?」と「:」については, 下記の補足(2)を参照.

コードは長くなってしまったが, 場合分けの処理が単純化されている. 動作は変わっていない.

なお,フラグ(flag;旗)とは, 0 か 1 かどちらかの値をとる変数のことである. 条件の判定結果を保存しておくために利用する. また,0xXX は, 数値 XX が 16 進数表記であることを示している. (たとえば,10進数の 16 を 16 進数で表わすと 0x10 となる.)

switch の条件式 kansai*0x10 + jishin の意味: 2個のフラグを1個の数値にまとめている. これにより,選択構造をシンプルに記述できた. (List 2 では選択構造が沢山あった. List 3 では1個だけになった.)

以上,同じ問題に対して, 複数の異なるコーディング方法があることが理解できただろうか? さまざまな方法を考え, 状況に応じて最適な方法を見分け, 使い分けて行こう.

ただし,どの方法がベストなのか?一概には言えないが... できるだけ,意味を分かりやすく,コードを短かく.

練習問題

  1. 前回の averager.c を元にして, 最大値および最小値を求めるプログラム maxmin を作成せよ.
  2. 動作例:

    $ ./maxmin
    number of data > 5
    5 integers > 11 31 -7 0 -12
    max = 31
    min = -12
    

    負の数も考慮すること.

    最大値・最小値の初期値を定数(例えば,ゼロ)にするのは間違い. 最初の入力データを初期値とすればよい. Hint:if (最初) 初期化;
  3. 次のコードの実行結果について:
    int a = 0;
    
    if (a == 0) printf("Bingo!\n");
    printf("%d\n", (a == 0));	// 条件式の値を確認
    
    printf("%d\n", a);		// 変数の値を確認
    

    条件式「a == 0」の部分を 以下のように変更するとどうなるか?

    注意:変更箇所は2個ある.どちらも同一内容に書き換えること.

    そして,これらの実験結果から, if 文の動作原理や条件式の意味について考察せよ.

    ヒント:条件式も代入式も式なので, たし算やかけ算などと同様に, 計算結果として値を持っている. その値は何なのか? そして,それが if 文でどのように取り扱われているのか? という問題...

補足(1):練習問題のヒント

最大値だけを求めるプログラムの NS チャートの一例を Fig.A に示す.

Fig.A. 最大値算出の NS チャートの例

練習問題では,最小値も求める必要がある. 結果表示も省略されている.

また,このチャートのままでは,コードに無駄があるかもしれない. (同じ処理「最大値=データ」を2回も書いてしまっている.) 各自で工夫しよう.


補足(2):三項演算式

「処理の場合分け」には if 文だが, 「値の場合分け」の場合には 三項演算式の方が便利だ.

ここで,「値」とは,1本の計算式だけからなるコード. 「処理」とは,あらゆるコードの組み合わせからなるもの.

たとえば,こんな if 文:

if (x < 0) {
	y = -x;
} else {
	y = x;
}
要するに,絶対値の計算ね.

これを三項演算式で書けば:

y = (x < 0) ? -x : x;	

このようにコンパクトに記述できる. 教科書 pp.63-64 を参照.

視力テスト:コロン「:」とセミコロン「;」を識別できるか? できないなら眼鏡を使うか文字を拡大表示すること.

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