前回,タートルの制御に反復(繰り返し,ループ)を利用して, 複雑な動作を少ない命令で実現できることがわかった. 今回はさらに,選択(条件判断)を利用して, タートルの動作を状況に応じて変化させ, より複雑な図形を効率的に描いてみよう.
作業に移る前に, 今回も,作業用ディレクトリの準備をお忘れなく. また,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); }
なお,PosX() と PosY() は, タートルの現在位置を調べるための状態関数(後述)である.
上記ソース in-circle.c における int i とか double x, y, r とかは, 数値を記録しておくための変数の宣言である. int は整数型,double は実数型を意味する. int 型の変数には,整数しか記録できない. 一方,double 型の変数には,実数(整数+小数)を記録できる.
データ型の使い分け:
なぜ,すべて実数としてはダメなのか? コンピュータの実数計算では,誤差が必ずつきまとうためだ. 例えば,理論的には等しいはずの2つの計算式の値が, プログラム的には等しくなかったりする. (ただし「まったく異なる値」というわけではなく 「ほぼ等しい値」とか「少しだけ異なる値」になるということだ.)
また,メモリ消費量を節約するためでもある. データ1個あたりの情報量は, int 型では 4 byte, double 型では 8 byte である.
これらより,特に必要のない限り,実数ではなく整数を使うべきだ.
実数計算の誤差について確認してみよう. ソースファイル 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
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個も必要となってしまった.
複数の条件式を組み合わせたりする場合, 論理演算を利用すると,単純化できるかもしれない:
if (条件式1) { 処理A } else if (条件式2) { 処理A } |
→ |
if ((条件式1) || (条件式2)) { 処理A } |
if (条件式1) { if (条件式2) { 処理A } } |
→ |
if ((条件式1) && (条件式2)) { 処理A } |
if (条件式) { // 処理なし } else { 処理A } |
→ |
if (!(条件式) { 処理A } |
では,OR 演算を利用して,in-recrangle.c のコードを効率化してみよう.
if 文と論理演算を使えば, 変化に富む複雑な処理を記述できるようになる. しかし,無闇に使ってしまうと, ソースコード自体も無駄に複雑になりがちでもある. if 文を書く際(特に else も使う際)には, ソースコードを効率よく記述できるように, 条件分けについて十分に整理すること.
条件整理のチェックポイント例:
非効率な例:
if ((条件A) && (条件B)) { 処理A 処理B } else if (条件A) { 処理A } else if (条件B) { 処理B }
修正案:
if (条件A) { 処理A } if (条件B) { 処理B }
非効率な例:
if (x == 0) { } else { 処理 }
修正案:
if (x != 0) { 処理 }
非効率な例:
if ((条件1) && (条件2) && ... && (条件10)) { 処理A // すべての条件が成立した場合 } else if ((条件1) || (条件2) || ... || (条件10)) { 処理B // どれかの条件が成立した場合 }
int bingo; // 成立する条件の個数 bingo = 0; if (条件1) bingo++; if (条件2) bingo++; ... if (条件10) bingo++; if (bingo == 10) { 処理A // すべての条件が成立した場合 } else if (bingo > 0) { 処理B // どれかの条件が成立した場合 }
ある動作を実現するための記述方法は, 一般に,ひとつだけではなく複数ある. 効率よく記述するために,複数の方法を考え, ベストと思われるものを選択すること. ひとつの方法だけしか考えないのでは不十分.
できるだけ効率的な if 文の構成を模索しよう.
選択文 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] # フォアグラウンドジョブの強制終了
動作中のタートルの現状は,次のような状態関数によって調べられる:
練習問題の in-torus.c または in-cross.c の一方または両方に取り組め.
担当教員へレポートを送信せよ:
SS画像の作成方法は前々回と同様.