アドプロ 2018.05.14

制御構造 (2)

前回,タートルの制御に反復(繰り返し,ループ)を利用して, 複雑な動作を少ない命令で実現できることがわかった. 今回はさらに,選択(条件判断)を利用して, タートルの動作を状況に応じて変化させ, より複雑な図形を効率的に描いてみよう.

作業に移る前に, 今回も,作業用ディレクトリの準備をお忘れなく. また,C言語プログラムのコンパイルと実行の方法について, 前々回の説明を再確認しておくとよい. さらに,反復について,前回の説明を確認.


状況に応じた制御

何はともあれ,簡単な例を実行してみよう. 次のプログラムでは,円形領域の外へ出ないようにタートルを制御している.

実際は少しハミ出しちゃいますが..., 今はまあ,気にしないでおきましょう. 気になった人は,後で, ハミ出さないように改良してみよう.

ソースファイル in-circle.c

#include "kame3d.h"

int main()
{
	int    i;
	double x, y;
	double r = 4.0;		// 円の半径

	Init("Kame3D in Circle");

	for (i = 0; i < 1000; i++) {
		x = PosX();		// 現在の x 座標
		y = PosY();		// 現在の y 座標
		if (x*x + y*y > r*r) {	// 領域外に出た場合...
			Turn(180.0);		// 完全に方向転換
		} else {		// その他(領域内の場合)...
			Turn(25.0);		// ちょっとだけ回転
		}
		Move(1.0);
	}

	Play();
	return (0);
}
前回の多角形のプログラムと基本的には同じである. これも Turn()Move() の反復なので.

なお,PosX()PosY() は, タートルの現在位置を調べるための状態関数(後述)である.


データ型と誤差

上記ソース in-circle.c における int i とか double x, y, r とかは, 数値を記録しておくための変数の宣言である. int は整数型,double は実数型を意味する. int 型の変数には,整数しか記録できない. 一方,double 型の変数には,実数(整数+小数)を記録できる.

「だったら,double だけ使えばいーじゃん」ではなく, これらを適切に使い分けよう.

データ型の使い分け:

なぜ,すべて実数としてはダメなのか? コンピュータの実数計算では,誤差が必ずつきまとうためだ. 例えば,理論的には等しいはずの2つの計算式の値が, プログラム的には等しくなかったりする. (ただし「まったく異なる値」というわけではなく 「ほぼ等しい値」とか「少しだけ異なる値」になるということだ.)

もちろん,整数計算でも誤差は発生するのだが, それは割り算の場合だけだし, しかも,割り切れない場合に限る. 実数計算では,加減乗除すべての場合に誤差が発生し得る.

また,メモリ消費量を節約するためでもある. データ1個あたりの情報量は, int 型では 4 byte, double 型では 8 byte である.

実数には 4 byte の float 型もあるが, 精度が悪いので,現代では特別な事情がない限り利用されない.

これらより,特に必要のない限り,実数ではなく整数を使うべきだ.

実数計算の誤差について確認してみよう. ソースファイル mul.c

// 実数計算における誤差を確認するためのプログラム
// 足し算の繰り返しで掛け算 d*n を計算
// n を 100000(10万)とか 1000000(100万)とかにも変えてみよう.

#include <stdio.h>

int main()
{
	double	d = 0.1;
	int	n = 10000;

	double	t = 0.0;
	int	i;

	for (i = 0; i < n; i++) {
		t += d;		// t = t + d; と同
	}
	printf("%f * %d = %f : %f\n", d, n, t, d*n);	// 結果表示

	return (0);
}

実行:

$  cc  error.c  -o  error	# タートルのプログラムではないので -lkame3d は不要
$  ./mul
0.100000 * 10000 = 1000.000000 : 1000.000000		# とりあえず正確そうだが...?

変数 n の数値を増やしてみるとどうなるだろうか?

0.100000 * 100000 = 10000.000000 : 10000.000000
0.100000 * 1000000 = 100000.000001 : 100000.000000
0.100000 * 10000000 = 999999.999839 : 1000000.000000
正確そうに見える数値でも,実際には, 表示範囲外の下位の桁に誤差が隠れている. 例えば,変数 d = 0.1 の値は正確に 0.1 ピッタリではない. また,計算(+,-,*,/,等)の反復によって,さらに誤差が付加・蓄積されてゆく. 誤差が増幅される場合もある.

if 文と条件式

if 文の使い方の一般形は次の通り:

if ( 条件式1 ) {
	条件式1 が成立したときだけ実行したい処理
	...
} else if ( 条件式2 ) {
	条件式2 が成立したときだけ実行したい処理
	...

} else {
	どの条件も成立しなかった場合の処理
	...
}

なお,else if (...) {...}else {...} の部分については, 場合によって省略可能である.次のようにしてもよい:

if ( 条件式 ) {
	処理
	...
}

とか,さらに単純な場合には:

if ( 条件式 ) 命令;
ここで「処理」と「命令」の違いに注意. 「処理」は複数の命令の組み合わせで, 「命令」は処理の構成要素の最小単位という感じ.

条件式は,次のような形式で記述される:

一致の記号が「=」ではなく, 「==」であることに注意. =」だと,比較ではなく, 変数への代入になってしまう. (文法的なエラーではないので,コンパイルには成功してしまう.) そして,変数値が意図せず変わってしまうと,当然, プログラムは異常動作に陥ってしまう. ありがちなミス.

なお,double 型の式を == で比較しないこと. 上述の通り,誤差の影響があるので,意図した通りには動かないだろう. もし何度かうまく動いたとしても,それは偶然であり, いつでも正常動作するかというと,その保証はない.


羅列の解消

選択を利用すると,羅列的な(長く無駄のある)ソースコードを, プログラムの動作を変えずに, コンパクトに(短く無駄なく)書き直せる場合がある.

ジグザグ線を例としよう. 最も単純な方法としては,左折して直進と右折して直進とを繰り返せばよい. この方法にもとづいたソースファイル zigzag.c

#include "kame3d.h"

int main()
{
	int i;

	Init("zigzag");
	for (i = 0; i < 10; i++) {
		Turn(120);	// 左折
		Move(0.5);	// 直進

		Turn(-120);	// 右折
		Move(0.5);	// 直進
	}
	Play();
	return (0);
}

これだと,同じ直進が2回記述されており,コードが冗長である.

別な方法として,交互に左折/右折して直進,これを繰り返せばよい. 次のように,zigzag.c の内容を修正しよう:

	...
	for (i = 0; i < 20; i++) {
		if (i%2 == 0) {         // i が偶数のとき...
			Turn(120);
		} else {                // その他(奇数)のとき...
			Turn(-120);
		}
		Move(0.5);
	}
	...
ここで,「%」は剰余(整数の割り算の余り)の演算子である.

これで,まったく同じ Move が2個から1個に減った. 再コンパイル・再実行し,結果は変わらないことを確認しよう.

まだ,同じような Turn が2個あるので, さらに修正:


	double angle;
	...
	for (i = 0; i < 20; i++) {
		if (i%2 == 0) {
			angle = 120;
		} else {
			angle = -120;
		}
		Turn(angle);
		Move(0.5);
	}
	...

要するに,相違部分(左折/右折)だけを場合分けし, 共通部分(直進)を 1 箇所にまとめており, 無駄が少なくなっている.

ただし,この単純過ぎる例では,共通部分が少なかったため, コードは,かえって長くなってしまった. しかし,より複雑なプログラムの場合には, コンパクト化が実現できることは,容易に予想できるだろう.

なお,これらの2つの方法の関係は, 数式に例えるなら「a x + b x」と「(a + b) x」との違いのようなものである. どちらも結果は同じであるが, どちらの記法がよりスマートか?


論理演算

in-circle.c を改造して,領域の形状を長方形に変更してみよう.

ソースファイル in-rectangle.c

int main()
{
	...
//	double r = 4.0;		// 円の半径
	double r1 = 5.0;	// 長方形の長辺の半分
	double r2 = 2.0;	// 長方形の短辺の半分

	Init("Kame3D in Rectangle");

	for (i = 0; i < 1000; i++) {
		...
//		if (x*x + y*y > r*r) {	// 領域外に出た場合...
//			Turn(180.0);		// 完全に方向転換
		if (x < -r1)  {		// 領域外に出た場合...
			Turn(180.0);		// 完全に方向転換
		} else if (x > +r1) {
			Turn(180.0);
		} else if (y < -r2)  {
			Turn(180.0);
		} else if (y > +r2) {
			Turn(180.0);
		} else {
			Turn(25.0);		// ちょっとだけ回転
		}
		...

領域外の判定と方向転換の同じような処理が,上下左右の辺のために, 4個も必要となってしまった.

複数の条件式を組み合わせたりする場合, 論理演算を利用すると,単純化できるかもしれない:

では,OR 演算を利用して,in-recrangle.c のコードを効率化してみよう.

コード例は省略... 4個の条件を OR で連結するだけ. 同じ動作を短く記述できることを確認しよう.

複雑な条件判断の単純化

if 文と論理演算を使えば, 変化に富む複雑な処理を記述できるようになる. しかし,無闇に使ってしまうと, ソースコード自体も無駄に複雑になりがちでもある. if 文を書く際(特に else も使う際)には, ソースコードを効率よく記述できるように, 条件分けについて十分に整理すること.

条件整理のチェックポイント例:

ある動作を実現するための記述方法は, 一般に,ひとつだけではなく複数ある. 効率よく記述するために,複数の方法を考え, ベストと思われるものを選択すること. ひとつの方法だけしか考えないのでは不十分.

世の中では, 「目的のためには手段を選ばない」のは 非常識な悪党とされるが, 実は,「手段を選べない」 (思いつかない,最初の思いつきだけで満足してしまう,今まで通りで不満がない) というのが常識の善良な人々も困り者だ. 多くの選択肢をもち「最良の手段を選ぶ」のが不常識で優秀な技術者だ. 検索キーワード: 不常識 非真面目

練習問題

  1. in-circle.c を元にして, 閉じ込め領域の形状を「ドーナツ形」(大小2つの円の組み合わせ)とし, in-torus.c を作成せよ.
  2. in-recrtangle.c を元にして, 閉じ込め領域の形状を「十字形」(2つの長方形の組合せ)とし, in-cross.c を作成せよ.

できるだけ効率的な if 文の構成を模索しよう.


while ループ

選択文 if と関連して,反復文 while もある. if では,条件が成立したとき,一度だけ処理を実行する. 一方,while では,条件が成立する間,何度も処理を繰り返す.

while 文の一般形:

while ( 条件式 ) {
	処理
	...
}

while 文の使用例の断片:

double d = 0.0;			// 1回のMoveでの移動距離
double total = 0.0;		// 移動距離の合計

while (total < 10.0) {	// 総移動距離が一定以内であれば
	Move(d); total += d; d += 0.02;
	Turn(30.0);
}
Play();
...

これは,前回のサンプル spiral.c の改造版である. 元のコードでは,for ループを使い, 反復回数を条件としていた. 一方,この改造版では,while ループを使い, 総移動距離を条件として,一定の長さの螺旋を描くようになっている.

注意:while の条件式の設計をミスると, 無限ループになり, プログラムが無反応になってしまう可能性がある. なってしまった場合:

$  ./プログラム &	# プログラムを実行した後...
[2] ...			# ...括弧内の数字を見とけ
$  jobs			# 見のがした場合...
...
[2] ... ./プログラム &
...
$  kill  %2	# バックグラウンドジョブの強制終了


$  ./プログラム		# & を付け忘れた場合...
[Ctrl] + [C]		# フォアグラウンドジョブの強制終了

基本的なタートル状態関数

動作中のタートルの現状は,次のような状態関数によって調べられる:

ここで,double とか int は, 関数値の型を学生様へお知らせするためだけに示したものだ. ソースコードに記述するものではない. 例えば,x = double PosX(); と書くのは間違い. in-circle.c のコードを参考にしよう.

本日の課題

練習問題の in-torus.c または in-cross.c の一方または両方に取り組め.

担当教員へレポートを送信せよ:

  • 提出期限:05月18日(金)17:00
  • SS画像の作成方法は前々回と同様.


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