今回でいっきに配列、ループと条件分岐を紹介します。
ついてこれるかな?
ルギア君「そういえば、ディアルガとパルちゃんはどこへ行ったんだろう?」
リザード「さあ、わかりませんね。探しましょうか。」
ルギア君「うん。」
ルギア君が外に行こうとするのを、リザードは止めた。
リザード「待って。せっかくだから、メタグロスに探させましょう。」
ルギア君「はあ。」
リザード「まずは、リストを作成しましょう。」
ルギア君「リストって?」
リザード「部屋のリスト。」
リザードは部屋のリストを紙に書き出した。
リザード「うん、こんなものかな。研究室にはいないことがわかっているから抜かしたよ。」
ルギア君「ほう。」
リザード「この中から『探せ』という魔法もあるけど、今回は練習だから、自分で探す方法を考えてみよう。」
ルギア君「えーっと、・・・」
・・・
リザード「それぞれいるかどうか問い合わせればいいね!」
ルギア君「うん・・・」
リザード「あまり納得がいかないみたいだけど、残念ながら、それしか方法はないよ。」
ルギア君「多くない?」
リザード「そのためにあるのが、この魔法さ。」
ルギア君「ー (ブエー) としか書いてないけど?」
リザード「それは、おなじ部分を繰り返し実行するための魔法なのさ。」
ルギア君「ほう。」
リザード「ー, ー , , . (ブエー、オク クセーガ リーゲ、オク デュペアコギク メ、オク ハゴタハゴタ。)」
つづけてリザードが「 ーー . (フォギクンジェルバ、フォーディーンジェニー、オク。)」というと、「0」、「1」、「2」、「3」、「4」と書かれた 5 枚のカードが降ってきた。
ルギア君「5枚も降ってきた!」
リザード「そう。 (フォギクンジェルバ)と1回いっただけなのに、5枚も降ってきたね。これが、繰り返しの効果だよ。」
ルギア君「へえ。」
リザード「繰り返しは、俺たちでやるのも大変なように、メタグロスにやらせるのも実は大変なんだ。良く覚えておいてね。このぐらいだとすぐこなしちゃうけどね。」
ルギア君「・・・」
リザード「じゃあ、次は、いるかどうか調べる部分を作ろう。」
ルギア君「はい。」
リザード「この部分には、この魔法がいいな。」
ルギア君「 (クバ) としか書いてないけど。」
リザード「これは、こう使うんだ。, ー . (クバ、オク クセーガ ク。)」
つづけてリザードが「 ーー . (フォギクンジェルバ、フォーディーンジェニー、オク。)」といっても、今度は何も出てこなかった。
リザード「 ー (オク クセーガ ク) という条件は成り立っていないから、メタグロスは (フォギクンジェルバ)の文は実行しなかったよ。」
ルギア君「難しいな。」
リザード「逆に、もしさっきの条件が成り立っているなら、メタグロスは実行してくれるはずだ。」
ルギア君「へぇ。」
リザード「最後に、このリストをメタグロスに伝える方法だ。」
ルギア君「まだあったのか。」
リザード「リストには複数のデータが入っているよね。それぞれ別の箱に入れればそれで済むけど、それだと、またさらに箱のリストを作らなくちゃいけないから、堂々巡りになっちゃうよね。だから、このまま、箱をたくさんつなげて、紐で縛り付けて置けば順にあけて調べることができるね。」
ルギア君「ふむふむ。」
リザード「それを実現するのが、 (ソサソムセ) という魔法だ。」
ルギア君「どう使うの?」
リザード「 (ソサソムセ) の後に数字を入れるんだ。そうすると、何番目かを指定することができる。」
ルギア君「どうやって紐で縛り付けるの?」
リザード「箱を作る時に、箱の名前の後に、 (ソサソムセ)といって、そのあと、数字をいうと、その数だけ繋がった箱ができるよ。」
ルギア君「やっぱり面倒だな。」
リザード「・・・」
ルギア君はディアルガとパルちゃんを探しに部屋を出ていってしまった。
みなさんはめげずにがんばってね。
まとめ
Lesson 1 「ループ」
これは、先ほどリザードが唱えていたものとおなじです。
実行すると、
0 1 2 3 4
と表示されます。
これが、printf が 1つしかないのに、たくさん出てくるという、マジックです。
では、くわしく見ていきましょう。
for(i = 0; i < 5; i++)
for
for は繰り返しであることを示します。
あとに出てくる { と } の中身を何回も実行します。
{ と } がない時は、後ろにある一番近い ; までを何回も実行します。
i = 0
i = 0 はその通り、i に 0 を代入するわけなのですが、この部分は繰り返しに入る前に、1度しか実行しません。
つまり、i = 0 は最初の i の値を決める役目をしているわけです。
i < 5
この部分は繰り返しの継続条件です。
i++ で i を 1 ずつ増やしていき、i が 5 になった瞬間、繰り返しのループから抜け出して { } の外側へ飛びます。
実行の順番
ループのいろいろ
このプログラムも Sample007-01.c とまったくおなじ動作をします。
i = 0 と i++ の場所を良く見ておいてください。
つまり、while は i < 5 が満たされているあいだずっと(いつまでだか知らないけどとにかくずっと)ループの中身を実行し続けます。
このプログラムも実はこの場合はおなじ動作をします。
これは i < 5 というループを抜け出すかどうかを決める部分がループの最後にあると言うことがポイントです。
つまり、この組み方をすると 1回目は無条件に実行されます。
このループはあまり使い勝手が良くないので、登場する機会は少ないのですが、役に立つ時はとても役に立ちます。
Lesson 2 「条件分岐」
このプログラムは実行しても何も出てきません。
これは、リザードのやったこととおなじです。
では、くわしく見ていきましょう。
if(i == 0) { ... } else if(i == 1) { ... } else { ... }
if
これが条件分岐の始まりを示します。
i == 0
最初の条件です。
この条件を満たせば、このあとにある { } の中身を実行します。
{ } がない場合は、一番近い ; までを実行します。
実行したら、if の最後の } まで飛びます。
条件を満たしていない場合は、{ } は飛ばして、その先に進みます。
else if
n 個の条件分岐を作る時に、2 個目から n (- 1) 個目まではこれを使います。
それぞれ条件を付けて (この場合は i == 1)、条件を満たせば、その中の { } を実行して、if の最後の } まで飛びます。
else
最後の最後までまったく見合う条件がなかった場合に実行されます。
else if、else の省略
else if や else は必要がなかったら、(そのあとの { } も含めて)省略できます。
だから、最初のような条件分岐でも OK なのです。
条件分岐のいろいろ
条件分岐は 2 種類あります。
以下で説明する条件分岐は if - else if - else をつかって書き直すことができますが、if - else if - else でかかれた条件分岐は 99% が以下で説明する条件分岐に直すことはできません。
つまり、かなり用途を限定して作ったものなのです。
(1) が i = 0 であれば、実行結果は、
case 0: i = 0
の1行だけなのですが、
(1) を i = 1 にすると、実行結果は、
case 1: i = 1 default: i = 1
と2行出てきます。
(1) を i = 99 にすると、実行結果は、
default: i = 99
と1行出てきます。
switch 条件文は以下のようにして使います。
関係する変数は全て整数型 (か文字型*2 )しか使えないのが特徴です。
このあいだ紹介した仕組んで正の数しかいれられなくしても整数型には変わりありませんので、使えますよ。
switch(i) { case 0: ... break; case 1: ... break; case 2: ... ... break; default: ... break; }
switch(i)
i は比較の対象となる変数を入れます。
「i の値が・・・」っていうふうに置き換えられます。
case 0:
一方でこっちは「0 ならば」っていうふうに置き換えられます。
繋げると「i の値が 0 ならば」となり、それ以降、break; までが一気に実行されます。
途中で他の case や default にぶつかっても「知らん顔」をして break; が見付かるまでそのまま下へ進んでいきます。
ですから、先のプログラムは case 1: のところで default: の前に break; がありませんでしたから、default: を無視して、そのまま先に進んだわけなのです。
default:
これは、if - else 条件分岐の else の部分にあたります。
「ほかのどの case でもなかったなら」というところでしょう。
default: は最後になければならないと言う決まりはありませんが、普通最後に書きます。
また、その最後にある break; も普通は省略しないで書くのが普通です。
(省略しても後に文がないから、} の外へそのまま続ける)
case や default の省略
この条件文でも case や default が必要なければ省略できます。
ただし、少なくとも case は 1 つ以上必要です。
まあ、この辺は当然ですよね。
case が 0 個だったり、default だけだと switch の意味を成しませんよね。
break は忘れない!
break; を書き忘れる事故が多発しているようです。
break; は書き忘れないように絶対に入れるものだと思って構いません。
下は break; を書かない代表的な例です。
switch(month) { case 1: case 3: case 5: case 7: case 8: case 10: case 12: day = 31; break; case 4: case 6: case 9: case 11: day = 30; break; case 2: if(uru) day = 29; else day = 28; break; default: printf("Error!\n"); break; }
1月、3月、5月、7月、8月、10月、12月は、31日(day)。
4月、6月、9月、11月は、30日。
2月は、閏(uru)年ならば、29日。そうでなければ、28日。
これ以外の月(month)は存在しないので、「エラー」と表示する。
こんなふうにとびとびの値をとるものの整理にも役立ちます。
Lesson 3 「配列」
この実行結果は、以下のようになります。
c[0] = 1 c[1] = 0 c[2] = 0 c[3] = 0 c[4] = 1 c[5] = 0 c[6] = 1 c[7] = 0 c[8] = 1 c[9] = 1
ぼーっと見ているとわかると思います・・・が、
int i, c[10] = {1, 0, 0, 0, 1, 0, 1, 0, 1, 1};
i は普通の箱です。
ここは変わりません。
c の後に [10] と付いていますね。
添字*3と呼び、c が 10 個の箱を繋げて紐で縛り付けた「配列」になります。
最初の箱に数字を入れるには、
c[0] = 2;
のようにします。
そう、最初の番号は 0 です。
また、定義した時だけは、
int c[10] = {1, 0, 0, 0, 1, 0, 1, 0, 1, 1};
のように一覧をそのままそれぞれの箱にまとめて入れることができます。
いままで { } の後に ; はありませんでしたが、今回は必要ですので忘れないでください。
実は文字列は配列です。
正確に言うと、文字型の配列です。
文字型の配列に、それぞれ1文字づついれて、配列にしてあげれば、文字列になりますね。
char c[] = "文字列のテスト";
あれ? 数字が抜けてますね。実は定義と同時にほかの何か(文字列の場合は定数だけ)で初期化する場合に限り、数字を抜かしても良いことになっています。
この場合、抜ける数字に変わりに入る数字は、文字列の場合、文字数(日本語の場合は文字数の2倍)+1 が、それ以外の配列の場合は入れようとする中身の個数が入ります。
+1 の部分には '\0' が入っています。'\0' は文字列の終わりを示す記号です。文字列の途中にわざとこれを入れて出力してみるとわかるでしょう。
int c[10] = {1, 0, 0, 0, 1, 0, 1, 0, 1, 1}; c[10] = 99;
とやるとどうなるでしょう?
c は 0 から始まって 10個ですから 9 番までですね。
と言うことは 10 番の箱は存在しませんね。
でも、このあと、
printf("%d\n", c[10]);
とやると、コンパイルも問題なく通り、99 と表示されて、正常に動作しているように見えるのですが、
調子にのって
#include <stdio.h> int main() { int g[10] = {2}; g[20000000] = 99; printf("%d\n", g[200000000]); }
なんてやると実行する時に「セグメンテーション違反です」と怒られます。
(Windows の場合は「このプログラムは不正な処理を行ったので・・・」と出てくる。)
では、安全に代入できる領域はどこからどこまでなのでしょう。
それは、言うまでもなく確保した箱の数だけです。
メタグロスはロッカー室(メモリ)にある箱の一部を切り取ってルギア君たちに渡してくれるので、さっき範囲外になっていた10番目の箱は9番目の箱のロッカー室での位置の次の箱ということが言えます。
したがって、
g[200000000]
はすでにそのような場所にはもう箱がない*4と言うことをメタグロスは教えてくれたわけです。
また、
#include <stdio.h> int main() { int g[200000000] = {2}; g[20000000] = 99; printf("%d\n", g[200000000]); }
も「セグメンテーション違反」だと怒られます。
今度はちゃんと範囲内に入っているのになぜでしょう。
これはメタグロスはロッカー室から箱を切り取ってルギア君たちに渡そうとしたのですが、残念ながら、使える箱の数が足りなかったということを意味しています。
ちなみに、代入するリストの個数とかっこで指定した数字が違う時は、かっこで指定した方が優先です。上の例では、200000000 個の箱を用意して、その先頭に 2 を入れてくれという意味です。
また、
int a[3] = {1, 0, 0, 1};
は 3個しかない箱に 4個のリストを入れようとしているのでエラーになります。
文字列はそれ自体が配列ですから、
char c[30] = "こんにちは。";
はエラーにはなりませんが、
char c[30]; c[0] = "こんにちは。";
はエラーになります。1つの文字の箱に文字列を詰め込むことはできません。
また、
char c[30]; c = "こんにちは。";
もエラーになります。
文字列をそのまま代入できるのは初期化の時だけと言うのはこのことです。
なお、文字の配列を出力する時は、
printf("%s\n", c);
のように、かっこを書かずに名前だけ伝えます。
あと、
c[30] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; printf("%c\n", c[4]);
とやると、「E」としかでてきません。
このように文字を出力する時は、%s を %c にかえてあげてください。
c[ 0] = 'A'; c[ 1] = 'B': ... c[26] = '\0';
となっているからです。
日本語は箱2つか3つ(システムによって異なる*5 )で1文字を表しているので、日本語を入れて c[...] 1文字だけ出力すると変な文字が出てきます。
ですから、日本語を入れる時は常に配列じゃなくちゃいけないのです。*6
文字列の配列
文字列は文字の配列だから、文字列の配列は、文字の配列の配列というふうに単純に置き換えられる。
どうやって実現するのかというと、
char c[][80] = { "こんにちは。", "俺はリザード。", "なんでもこなす仕事屋さ。", };
これで、この配列は、
- 「こんにちは。」
- 「俺はリザード。」
- 「なんでもこなす仕事屋さ。」
というリストを持っていることがわかります。
かっこが2つでてきました。
これは後ろから読むととわかりやすいでしょう。
この場合
「c は文字を入れる箱 80 個を紐で縛り付けたものを何個か持っている。」
となります。
つまり、
printf("%s\n", c[0]);
で
こんにちは。
と出てくるわけなのです。
この例に見られるように、数字を省略できるのは最初だけです。
以下のようにされると、メタグロスはそれぞれ何の数字を入れていいのか困惑してしまいます。
int c[][] = {2, 3, 4, 3, 4, 2, 4, 2, 5};
(1, 9)なのか(3, 3)なのか(9, 1)なのかまったくわかりません。
ちなみに、このようにすると、人間身としてはわかりますが、メタグロスは内側の { } を無視しますので意味がありません。
int c[][] = { {2, 3, 4}, {3, 4, 2}, {4, 2, 5}, };
いや、完全に無視するっていうと実は間違いなのです。
int c[3][3] = { {3, 1, 2}, {2, 3}, {4, 2, 2}, };
まんなかが 1つ 足りませんね。
この場合メタグロスはこう解釈することができます。
c[0] = {3, 1, 2}; c[1] = {2, 3}; c[2] = {4, 2, 2};
したがって、c[1][2] が何も設定されないまま残ることを意味しています。
上をこうすると、意味が変わります。
int c[3][3] = { 3, 1, 2, 2, 3, 4, 2, 2, };
こうしても、僕達にはその雰囲気が感じとれますが、メタグロスは英字と英字、英字と数字、数字と数字を区切るためのスペースやタブ、改行以外のスペース・タブ・改行は完全に無視します。
したがって、メタグロスはこう解釈します。
c[3 * 3] = {3, 1, 2, 2, 3, 4, 2, 2};
つまり、欠けるのは、最後のひとつ、つまり c[2][2] が何も設定されないまま残ることになります。
Lesson 4 「ディアルガを探せ」
では、ディアルガを探すプログラムを作ってみましょう。
(いまさらだけど日本語が入力できない環境の場合は英語に直して打ち込みましょう。)
僕の環境では文字の箱3つで日本語の文字1文字を表しているので、rooms に用意した箱は 25 となっています。
これより小さい数を指定すると、警告が出てきます。
これを実行すると、
ディアルガは 食堂 にいる。
と出てきて、食堂にいることがわかりました。
rooms にはあの部屋のリストを入れてあります。
existence がどの部屋にいるかという情報をもったものです。
1 のところにいるわけです。
このリストで 1 になるところと対応する部屋の名前を取り出して表示したわけです。
さて、break がありますね。これは、どういう意味でしょう。
switch のものかと思いきや、switch はどこにも出てきませんね。
この break は例えどんな時でも、一番内側のループを抜け出してその先に進むというものです。
メタグロスはこの文にであったら喜んでそとに抜け出しますので、
for(i = 0; i < 10; i++) { ... break; ... }
とすると、
1回だけ、しかも最初から break のところまでしか実行しません。
Sample007-07.c にある break は if の中に隠されていますから、何回かループしないと、break にたどり着かないわけです。
見付かったら、もうそのループに用はないので、break で外に抜けるのです。
もし、全部 0 にしちゃったらどうでしょう。
何も出てきませんね。
では、見付からなかったら「見付からなかったよ」というメッセージを出してみましょう。
ループの最後に注目しましょう。
「ベランダ」にいる場合
- i++ (i == 10 になった)
- i < 11 は満たす
- existence[10] == 1 は成立
- メッセージを表示して break で外へ
- for の外では i == 10 である。
見付からなかった場合
- i++ (i == 10 になった)
- i < 11 は満たす
- existence[10] == 1 は不成立
- i++ で i は 11 に
- i < 11 を満たさないから外へ
- for の外では i == 11 である。
したがって for のあとで、i が 11 なら、「見付からなかったよ」とメッセージを出してあげれば良いことになりますね。
ですね。
ディアルガは見付かったかどうかを調べる方法は他にもあります。
この一例として goto を使う方法を紹介します。
goto を使うのは邪道だと言われているので本当はあまり使わない方がいいかもしれません。
でも、どうしても必要になる場合が存在して、
for(...) { for(...) { if(...) { /* あ、エラーだ */ goto error; } } } error: ...; /* エラーの場合の処理をする */
のような場合。
つまり、一気に 2つ以上のループを抜け出すために使われます。
goto somename;
goto の後には適当な名前を入れます。
そうすると、
somename:
のようにおなじ名前に : を付けたもののところにジャンプしてきます。
ただ、: は文のおわりじゃないので*7、} の直前に書く場合は、: のあとに ; を入れてあげる必要があります。
おや? これは、
case 0: case 1: default:
と似てますね。
というか、機能はおなじです。
おなじですが、goto を使ってこれらのところにジャンプすることはできません。
適当な名前に : を付けたものを ラベル といいます。
ちょうど付箋(ふせん)とおなじです。単なる目印で、普通に上からきて、ラベルにぶつかっても、そのまま通り過ぎていきます。
これを利用すると、
となります。
逆に見付かった場合には、discovered: までジャンプして printf("ディアルガは見付かりませんでした\n"); は飛ばすというわけなのです。
いい加減メタグロス
いままでのプログラムには「どれかひとつだけ 1 にしてね」という注書きが入っています。
でも、注書きはあくまで注書きなのでやろうと思えば、何個かあるいは、全部 1 いや、2, 3 にすることも可能ですよね。
ですから、そんな時は
とします。
これを実行すると、
ディアルガは キノガッサの部屋 食堂 厨房 鐘のところ のどこかにいると思われます。
と出てきますね。
printf で必ず 1文出力しなくちゃいけないことはまったくないので、このように分割して出力しても問題ないのです。
discovered という箱に最初は 0 を入れておいて、候補があったら 1 にする。
という構造をとってますね。
したがってループを抜けても 0 だったら、「どこにもいない」ということがわかりますね。
さて、break の変わりに continue が出てきてますね。
これは、「今回のループの処理はもういいから次回のループに進んでくれ」という意味です。
言い替えると、「ループの最後にある } の直前に飛んでくれ」といった感じです。
なお、for 文の場合は continue をつかっても i++ を実行するということがミソです。
だから、上のプログラムを while で書き直した
これを実行する(このプログラムは(Web などの)サーバー上で実行しないでくださいね!)と、最初に条件として引っかかる「キノガッサの部屋」が延々と表示されているのがわかると思います。
つまり、while では、continue すると、i < 11 に飛ぶので、i++ が実行されず、i が止まってしまい、結果としていつまでもループを抜けられない「無限ループ」になってしまいます。
意図的に無限ループにする場合は、途中で何かの条件で抜けるのを入れておかないといけませんよ。
意図的に無限ループにする場合は、以下のように記述します。
while(1) { ... }
や、
for(;;) {
...
}
が良く使われます。
このように、for では 必要のない項目は省略できるんですね。
まんなかがループの継続条件ですから、基本的に、まんなかを省略すると無限ループになります。
この場合で continue をはずすことができず(判定の他にも処理がある場合など)、そしてどうしても while を使わなければいけない場合、いかのようにすると、回避できますが、「while を使わなければいけない場合」は存在しませんので(for の一部分を省略できるから)、参考までにしておいてください。
i が 11以上 になったら、無限ループを抜けて先に進みます。
あ、そうそう、無限ループは
で強制的に抜けて、プログラムを強制終了できます。
*1:地図は [f:id:lugia:20071130194504j]
*2:文字型は整数型の一部
*3:これも僕は「てんじ」と読んでいますが、大抵は「そえじ」らしいです。こっちは両方共変換で出てきますが、添字の注に <そえじ> と書いてあります。これはどっちも正しいと思います。
*4:実際にはプログラムが使えるメモリの範囲が決められていて、その範囲の外に出ると、怒る。
*5:Shift-JIS、EUC-JP 環境では 2 つ。Unicode 環境だと 3 つ。
*6:これを回避する方法が最近といっても10年ぐらい前だけど、登場している。
*7:→ [d:id:lugia:20071203] 第3章 初めての魔法