手続きの流れを制御する
これまでの連載で、さまざまなデータを表現し、それを加工(演算)することが、なんとなくできそうな気になってきました。しかし、まだ何かモノ足りませんね。そういえば、真偽値計算って、場合によって行う処理を変えるために使えるとか、前回言ってましたね。そんなわけで今回は「手続きの流れを制御する」です。
プログラムのふるまい
コンピュータの本質的な仕事というのは、変数、配列、構造体などによって表現されたデータを、各種演算によって加工し、その結果を何らかの形にして表現することです。このとき、複雑な加工を施そうとすれば、自ずと必要となってくるのが「場合分け」です。
例えば、家に訪ねてきた人に応対する場合、「場合分け」ができなければ、全ての人を同じように扱うことしかできないので、親戚だろうが、集金のおじさんだろうが、泥棒だろうが、居間に通してお茶を出すことになってしまいます。しかし「場合分け」ができれば、人によって応対を変えることができるので、会社の上司が来たら良いお茶を出し、訪問販売員が来たら居留守を使うといった応対が可能になります。
また、最も身近なコンピュータプログラムであるコンピュータゲームにおいても、宿屋に泊まったら回復し、王様に話しかけるとアイテムがもらえ、気になるあのコをしつこく追いかけまわすとラブラブになれる(注:あくまでコンピュータゲームの世界での話です)といったことができるのも、「場合分け」のおかげです。「場合分け」がないと困りますね。
コンピュータプログラムにおける「場合分け」とは、「特定の条件を満たす場合のみ、特定の処理を行うしくみ」と表現できるでしょう。このしくみによって、コンピュータゲームは紙芝居ではないモノになっているのです。(もし子供達の反応を見ながらストーリーを分岐させるような紙芝居屋のおじさんがいたらごめんなさい)
そもそも、ある程度高等な生物の行う知的活動も、本質的な部分ではこの「場合分け」に他ならないわけで、プログラムに限らず何らかの複雑なふるまいを見せるものは、たいてい「場合分け」で表現することが可能です。
今回はC言語における「場合分け」の表現を中心に、プログラムの手続きの流れ、ふるまいを制御することについて考えてみます。条件分岐
C言語では「場合分け」を表現するための「特定の条件を満たす場合」のチェックに、前回に解説した真偽値計算を使います。真偽値計算の結果が真の場合のみ特定の処理を行うしくみを持っているわけです。ここではif文、switch文の2種類のしくみについて解説します。
List.1:list1.c
#include <stdio.h> int main() { int a = 1, b = 2, c = 0; if(a > b) { c = 3; } printf("line 12 : c is %d\n", c); if(c < a) { c = 4; } else { c = 5; } /* 「else」を使わない場合はこう書ける if(c < a) { c = 4; } if(!(c < a)) { c = 5; } */ printf("line 30 : c is %d\n", c); if(c == a) { c = 6; } else if(c == b) { c = 7; } /* 「else if」を使わない場合はこう書ける if(c == a) { c = 6; } if((!(c == a)) && (c == b)) { c = 7; } */ printf("line 48 : c is %d\n", c); return 0; }
Fig.1:if-else条件の例
Fig.2:if - else if条件の例
if文
「場合分け」を実現する、もっとも基本的なしくみが、if文です。if文は
if(条件) {
処理
}
のように記述し、条件を満たせば、ブロックと呼ばれる{}内の処理を行うというしくみです。List.1の8〜10行は、「aがbよりも大きい」という条件を満たせば、c=3(cに3を代入)を行うという記述になっています。12行目の時点で、cの値がどのようになっているか、考えて見てください。
{}で括られる部分は、通常、その外側にある記述よりもタブ1個分程度右にずらしたところから書くという習慣があります。これを「字下げ」などと呼んでいます。実際には字下げしていなくとも、文法上間違いになることなどはありませんが、字下げによってブロックの記述状態が分かりやすく(見やすく)なります。{}を伴う記述を行う場合は、是非字下げを行うようにして下さい。
また、if文では、「else」というキーワードと組み合わせることで、条件を満たすときと満たさないときで、別々の処理を行うことができます。
if(条件) {
処理1
}
else {
処理2
}
と記述すると、条件を満たせば処理1を、条件を満たさなければ処理2を、それぞれ行わせることができます。List.1の14〜19行では、「cがaよりも大きい」という条件を満たした場合c=4を行い、そうでない場合c=5を行うように記述を行っています。30行目の時点で、cの値はどうなっているか、考えてみてください。22〜27行は、「else」を使わない場合に、14〜19行と同じ内容を記述した例になります。Fig.1を参考に、なぜ同じ意味になるのか考えてみてください。
「else if」を使うと、条件を満たさなかった場合に、さらに別の条件で条件分岐させることができます。
if(条件1) {
処理1
}
else if(条件2) {
処理2
}
と記述すると、条件1を満たす場合は処理1を行い、条件1を満たさず条件2を満たす場合には処理2を行うことができます。List.1の32〜37行では、「cとaが等しい」場合にc=6を行い、そうでなかったときに「cとbが等しい」場合にc=7を行うように記述されています。48行目でcの値はどのようになっているでしょうか? 40〜45行は、「else if」を使わないで32〜37行と同じの内容を表現しています。Fig.2を見ながら、なぜ同じ内容になるのか考えてみてください。演習:List.1、Fig.1、Fig.2を参考に、Fig.3をif文で表現せよ。
Fig.3:演習問題用の条件
このように、if文を使うことで、一本道ではない、状況に応じて行う処理を変化させることができます。しかし、実際にどのような場面でif文を使うのか、イメージが湧きにくいかも知れません。そこで、次のように考えてみてください。実生活の中で自分が行っている、さまざまな対応の中にも、if文で表現できることがたくさんあると思います。例えば、
if(テストの点が90点以上) {
親に見せて、ゲームを買ってもらう
}
else if(30点以下) {
親に見つからないように、処分する
}
のような対応をしていた経験がある人もいるのではないでしょうか? 自分でプログラムを書くときに、if文で何が表現できるのかを知っていなければ、if文を使いこなすことはできません。自分の普段の生活をC言語風に表現するとどうなるか考えることで、何がどんな風に表現できるのかを知ることができます。プログラミング言語は勉強したけど、自分でプログラミングできないという人のほとんどは、何がどんな風に表現できるのか分かっていないという問題をかかえています。プログラミングを特別に考えるのではなく、普段の生活の延長のように考えるようにしましょう。そうすることで、普段の生活でも手際良く、要領良く物事を処理できるようになっていくハズです。演習:初対面の人に会った場合、自分がどのような行動を行うのか、C言語風に表現せよ。
List.2:list2.c
#include <stdio.h> int main() { int a = 1, b = 2, c = 0; if(a == 1) { c = 3; } else if(a == 2) { b = 3; } else if(a == 3) { c = 2; b = 4; } else { c = 1; } return 0; }
List.3:list3.c
#include <stdio.h> int main() { int a = 1, b = 2, c = 0; switch(a) { case 1: c = 3; break; case 2: b = 3; break; case 3: c = 2; b = 4; break; default: c = 1; } return 0; }
List.4:list4.c
#include <stdio.h> int main() { int a = 1, b = 2, c = 0; if(a == 1) { c = 3; } else if(a == 2) { c = 3; b = 3; } else if(a == 3) { c = 2; b = 4; } return 0; }
List.5:list5.c
#include <stdio.h> int main() { int a = 1, b = 2, c = 0; switch(a) { case 2: b = 3; case 1: c = 3; break; case 3: c = 2; b = 4; } return 0; }
switch文
switch文も、if文同様に「場合分け」を表現する方法です。実質的には、特定のif文を見やすい形で書くための構文といえます。例えば、List.2をswitch文で書き直すとList.3になります。switch文は、条件の真偽によって「場合分け」するのではなく、条件の値そのものを見て条件分岐を行います。List.3では、変数aの値そのものを見て、1の場合はこうする、2の場合はこうするといった具合に記述されています。
switch文では、値に応じた「case」の行以下を処理します。このため、各case毎の処理の最後に「break;」を置いて、もうこれ以上{}内の処理を行うな、と書いてやる必要があります。例えば、List.3の11行目の「break;」がないと、aが1のときc=3を処理した後に続けてb=3を処理してしまいます。この性質を使って、List.4をList.5のように書くこともできます。
また、記述されているcaseにあてはまらない場合の処理を「default:」以下に書くことができます。このdefaultの記述が省略された場合は、記述されているcaseにあてはまらないときには何もしないことになります。演習:List.2、List.3、List.4、List.5を参考に、List.6をswitch文で書き直せ。
List.6:list6.c
#include <stdio.h> int main() { int a = 1, b = 2, c = 0; if(a == 1) { c = 3; } else if(a == 2) { c = 3; b = 3; } else if(a == 3) { c = 3; b = 3; } else { b = 4; } return 0; }
switch文は、使える場合がかなり限られるので、if文で書いた部分をあとで見て、「switch文の方がすっきりするなあ」という場合に使う程度の気持ちでよいでしょう。
ループ
「毎日毎日同じことの繰り返しで、お兄ちゃん生きてる気がしないよッ!」とコントでドリーが嘆いているように、私たちの日常の中には、なんと繰り返しが多いことでしょう。プログラムにおいても、同じような処理を何度も繰り返すことが多くあります。こうした繰り返し処理を行うことを「ループ」といいます。ここではC言語で使える3種類のループ記述方法について見ていくことにしましょう。
List.7:list7.c
#include <stdio.h> int main() { int a = 1, b = 2, c = 0; while(a != b) { c = c + 1; b = b - 1; } return 0; }
List.8:list8.c
#include <stdio.h> int main() { int a = 1; while((a * a) != 64) { printf("%d^2 != 64.\n", a); a = a + 1; } printf("%d^2 = 64.\n", a); return 0; }
whileループ
while(条件) {
処理
}
whileループでは、条件を満たす間、{}内の処理を繰り返し行います。List.7の例では、
1:初期条件でaが1、bが2。
2:aはbと等しくないので、ブロック内の処理を行う。
3:b=b-1でbが1になる。1回目のブロック内の処理が終わる。
4:再び、a!=bをチェックする。こんどは条件を満たさないので、ブロック内の処理は行わない。
という処理の流れになります。条件チェック、ブロック内の処理の繰り返しを、条件が満たされる限り行うわけです。
whileループで気をつけなければならないのは、ブロック内での処理を何回か行う中で、ループ条件を満たさなくなるように処理を入れておかないと、無限にループを繰り返してしまうということです。特定のプログラムでは、意図的にこの無限ループを用いる例もありますが、通常のプログラムでは、無限ループに陥らないように気をつけなければなりません。
ループの用途としてよく使われるのが、探索への応用です。何か適したものを見つけるという行為は、その答えである可能性のある候補を1つ1つ調べて、答えとしての条件を満たしたものを採用するということです。このとき、たいていの場合において、答えの候補はある順番をもって並べることができることが多く、ループによって答えの候補に順番にアクセスしながら、答えを探すことができるわけです。List.8の例では、数字を1から順番に調べ、2乗して64になる数値を見つけています。このように、単なる繰り返しではないループの使い方を知ることが、実際のプログラミングの上では重要になってきます。演習:List.8を参考に、3乗して1331になる数を求めるプログラムを書け。
List.9:list9.c
#include <stdio.h> int main() { int a = 0, i; for(i = 0; i < 10; i = i + 1) { a = a + i; } return 0; }
List.10:list10.c
#include <stdio.h> int main() { int a = 1; for(; (a * a) != 64;) { printf("%d^2 != 64.\n", a); a = a + 1; } printf("%d^2 = 64.\n", a); return 0; }
forループ
単純に10回繰り返すなどといった場合に使われるループとして、forループがよく使われます。
for(ループ前の処理; 条件; ブロック処理後の処理) {
処理
}
for文では、()内に「;」で区切られた3つの部分を持ちます。最初の部分がループ前に行う処理で、真ん中がループ条件、最後の部分がブロック内の処理を行った後に行う処理となります。実際の処理の流れでは、
1:ループ前の処理。
2:条件チェック。条件を満たしていなければループは終了。
3:ブロック内の処理。
4:ブロック処理後の処理。
5:2に戻る。
といった流れで、ループ処理を行います。List.9ではループを10回まわして、0〜9を全て足した数値がaに入るように、forループを使っています。
List.9では、変数iがカウンタの役目を果たしています。ループ開始前にカウンタを0にしておき、ループを回す毎にカウンタに1ずつ足していき、カウンタが10になったらループを終了させることで、10回まわすという処理を行っているのです。一般的にループ時に使われるカウンタ変数にはi,j,k……を使うことが慣習になっています。これは数学における数列などの添え字にi,j,k……を使う慣習に由来しています。こうしたルールは強制的なものではありませんが、これを守ることでプログラムの読みやすさは、かなり変わってきますので、是非守るように心がけて下さい。
forループは、ループ前の処理とブロック処理後の処理を除けば、whileループと完全に同じですから、List.8をList.10のように書き直すことができます。言ってみれば、全てのwhileループは無条件にforループに書き直し可能なわけです。実際にはfor文の特徴を生かして、List.11のように書くのが良いでしょう。演習:List.11を参考に、3乗して1331になる数を求めるプログラムを書け。
List.11:list11.c
#include <stdio.h> int main() { int a; for(a = 1; (a * a) != 64; a = a + 1) { printf("%d^2 != 64.\n", a); } printf("%d^2 = 64.\n", a); return 0; }
List.12:list12.c
#include <stdio.h> int main() { int a = 1, b = 2, c = 0; do { c = c + 1; b = b - 1; } while(a != b); return 0; }
do-whileループ
whileループとforループを、いわば兄弟のような関係とするなら、do-whileループは従兄弟になるのかも知れません。whileループとforループは、ブロック内の処理を行う前に条件チェックを行っていましたが、do-whileループでは、ブロック内の処理を行った後に条件チェックを行います。List.12がdo-whileループの使用例になります。
do {
処理
} while(条件);
whileループやforループでは、条件チェックが先にあるので、ブロック内の処理が1度も行われないままループを終了する可能性がありますが、do-whileループでは、最低1回はブロック内の処理が行われる点が異なります。条件、処理……ではなく、処理、条件……なのです。List.13:list13.c
#include <stdio.h> int main() { int a; for(a = 1; (a * a) != 64; a = a + 1) { if(a < 3) { contunue; } printf("%d^2 != 64.\n", a); if(a > 5) { break; } } return 0; }
breakとcontinue
switch文の解説で、{}内の処理を強制的に終了してしまうbreak文が出てきましたが、このbreak文は、ループが伴う{}に対しても有効です。ループ時のbreak文は、ループ自体を強制終了します。例えばList.13では、aが5より大きくなってしまった場合にループ自体を強制的に止めてしまうように、break文が使われています。
また、continue文を使うと、その回のブロック内処理は終了するが、その後条件チェックに進んでループ自体は継続させることができます。
少しややこしいかも知れませんが、break文やcontinue文が有効なのは、switch文及びループに伴う{}ブロックに対してのみであり、if文など他の構文に伴う{}ブロックに対しては、影響を及ぼしません。(だからこそList.13は成立している)
ループとbreak文、continue文を組み合わせると、かなり複雑な処理の流れを実現できますが、複雑になるということは分かりにくくなるということでもありますから、気をつけて使わないといけません。まとめ
今回はC言語上での条件分岐とループについて見てきました。プログラムの流れを制御するのに使われる手法は、今回見てきた何種類かの構文の組み合わせで表現されています。アルゴリズムの本質は、制御構文の使い方だとも言えるでしょう。
実は今回までの内容で、配列に入れられた数字を小さい順に並べるプログラムを組むぐらいの文法知識は解説し終わっています。ソートと呼ばれる、アルゴリズムの勉強のときに1番最初にやる処理ですが、来月号まで時間もあるので、どうやったらそうした処理が行えるのか考えてみてください。演習:配列に入れられた数字を小さい順に並べるプログラムを書け。
プログラミングは習うより慣れろという言葉がありますが、私はそこに想像力が必要だと思っています。最終的にプログラミングの目的は、どこにもないものを作ることです。ですから、既にあるものを見て、それのマネゴトで終わらずに、自分の作りたいものを想像できるようにならないとダメだと思うのです。そういった意味でも訓練になるように、あえて説明していないものをリストに散りばめたりしています。説明不足があったとしても、決して単なる手抜きではありません(^^;
次回は「関数を使う」です。謎のキーワード、printf()の正体がやっと明らかになります。それでは。
コラム
前回の演習問題の答え
最初の演習問題の答えとなるソースファイルを、list2a.cとして、今月号のCD-ROMに収録してあります。確認しておいて下さい。
真偽値計算問題の答えは、以下の通りです。
1. 30>15 → 真
2. 15==(7+8) → 15==15 → 真
3. !(40!=(4*10)) → !(40!=40) → !(偽) → 真
4. (10>15)&&(1==(2-1)) → (偽)&&(1==1) → (偽)&&(真) → 偽
5. (15==(30/2))&&(!(4!=(44%8))) → (15==15)&&(!(4!=4)) → (真)&&(!(偽))
→ (真)&&(真) → 真
6. (!(30>15))||(1==(2-1)) → (!(真))||(1==1) → (偽)||(真) → 真
ビット演算計算問題の答えは、以下の通りです。
42&((0x3f>>3)|(0x3e^0xf1))…@
00111111(0x3f) >> 3 → 00000111(0x07)
00111110(0x3e)
^ 11110001(0xf1)
----------------
11001111(0xcf)
よって、@は、42&(0x07|0xcf)…A
00000111(0x07)
| 11001111(0xcf)
----------------
11001111(0xcf)
よって、Aは、42&0xcf…B
42 → 00101010(0x2a)
00101010(0x2a)
& 11001111(0xcf)
----------------
00001010(0x0a)
よって、42&((0x3f>>3)|(0x3e^0xf1)) → 0x0a
上記の計算を行うプログラムのソースを、list3a.cとして、今月号のCD-ROMに収録してあります。確認しておいて下さい。
さいごに「2^nで表現される数での乗除算をシフト演算子で置き換える方法」について考えてみましょう。まず、シフト演算子と同様の操作(桁の上げ下げ)を、10進数表記の数値に対して行った場合の事を考えてみます。例えば、200を1桁下げると20、2桁上げると20000になります。これを一般化してみると、n桁下げると10^(-n)倍、n桁上げると10^n倍と表現されます。したがって、2進数で考えた場合の演算子であるシフト演算子では、>>nを適用すると2^(-n) = 1/(2^n)倍、<<nを適用すると2^n倍となることが分かります。このことから、例えば、*4は2^2倍なので<<2、/8は2^(-3)倍なので>>3と表現できることがわかります。この乗除算をシフト演算子に置き換える手法は、一般的な処理系において、算術演算より論理演算のほうが高速に行えることから、プログラムの動作速度を最適化する手法として用いられています。