デバッグしやすくするには
if
文を整理して
分岐を減らしておくといいって教えてもらいましたけど、ループを書くときもコツってあるんですか?
そうねぇ……まずはif
文と同じで、「やりがちな間違い」について知っておくのがいいかしら。
ループにも「やりがちな間違い」があるんですか?
いろいろとね。例えば……
次のfor
文では変数x
を「10」で初期化して、それが「0」になるまで「-2」で割るという処理をしようとしています。
for (int x=10; x=!0; x/=-2) {
printf("%+d\n", x);
}
もし、これが正しいプログラムなら次のように表示されるはずです。
+10
-5
+2
-1
ところが、実際に実行してみると「+1」が表示され続けてしまい、いつまでもループが終わりません。
+1
+1
+1
……(ループが止まらない!)
どこかを書き間違えているせいなのだけど、分かるかしら?
ええーっと、ちょっと待ってくださいね……
ふふふ。シンキングタイムね。
……ん?あぁ、分かりました!x=!0
のところが書き間違いですね。
そうそう。正しくは?
x!=0
です!
正解!ちなみに、今のはfor
文だったけれど、while
文にも同じ間違いはありそうね。
例えばこう書いても、やっぱりループは止まらない。
int x = 10;
while (x =! 0) {
printf("%+d\n", x);
x /= -2;
}
なるほど。同じ動きになりそうですね……って、あれれ?
どうかした?
これって、!=
を=!
と書き間違えたんだから、そもそも文法エラーで実行できないのでは?
あー、それはね……
念のため、なぜx=!0
がコンパイルできてしまうのか確認しておきましょう。
C言語に=!
という演算子はありません。でも=
と!
はありますね。そのため、この式はx = !0
と解釈されているのです。!
は否定を表すので!0
の部分は1
と書くのと同じであり、式全体としてはx = 1
と代入するのと同じ意味になります。
また、代入を式と見たときの計算結果は、代入した値そのものになります。この式の場合は、計算結果が常に1
になるということです。その結果、いつまでもループが終わらなくなってしまっていたのでした。
……と、原因を突き詰めていくと、どうしても細かい話になるのよね。
うーん。本当の原因は、単純に書き間違えたことだと思うんですけど。
そのとおりね。だから大切なのは、「動作がおかしいぞ」って気付いたときに……
そっか。どこが間違っているのか考えてデバッグしやすいように、プログラムを整理しておくことが大切なんでしたね。
そういうこと!
for文の使いどころ
さっき、for
文のところをwhile
文で書いたらこう、っていう例を見せてくれましたよね。この2つは何が違うんですか?
うん、そこは疑問に思うところよね。それじゃあ、for
文とwhile
文の違いについて確認しておきましょうか。
お願いします!
C言語のfor
文は、だいたい次のような形をしています。
for (ループ変数を初期化; ループ条件をチェック; ループ変数を更新) {
……
}
これは、while
文の「ループ条件」のすぐ近くに、「初期化」と「状態の更新」も書けるようにしたような文法だといえます。これと同等の意味をもつwhile
文は、次のように書けます。
ループ変数を初期化;
while (ループ条件をチェック) {
……
ループ変数を更新;
}
実際のところ、while
文がこういう形に収まることは多いんじゃないかしら。
はい。そんな気がします。
その場合は、for
文に置き換えられるはずね。
while
でもfor
でも、どちらを使ってもプログラムを書けるという状況は意外によくあります。これは、プログラムが分かりやすくなりそうなほうを選べるということです。
基本的には、for
文を使ったほうが分かりやすいケースが多いでしょう。なぜなら、ループ変数に関する処理をまとめておけるからです。これにより、次の2つを区別しやすくなります。
- どういう条件でループするのか
- ループ中で何をするのか
視覚的に書くと、ループをこういう構造にできるということですね。
for (どういう条件でループするのか) {
ループ中で何をするのか;
}
逆のことをいえば、上の構造を意識せずに書いたfor
文は分かりづらくなりがちね。
例えば、このfor
文はちょっとあやしい……
int sum;
int i;
for (i=1, sum=0; i<=10; sum+=i, i++);
うーん、なんだか分かりにくく感じます。行数は少ないのに……
そうよね。for
文には、もっとプログラムを分かりやすくできるポテンシャルがあるの。それを引き出してあげましょう。
例えば、こう手直ししたらどうかしら?
int sum = 0;
for (int i=1; i<=10; i++) {
sum += i;
}
わー、こんどは読みやすくなりましたよ。でも、なんで?
読みやすく手直ししたfor
文では、括弧((
と)
)の内側にループ変数に関する処理だけが書かれています。変数sum
はループ変数ではないので、中括弧({
と}
)の中に移動しました。これで、何をしているのかが読み取りやすいプログラムになったのではないでしょうか。
もしかすると、「これって、そんなに重要なこと?」と思ったかもしれません。でも、何十行もあるループを想像してみてください。プログラムが長く複雑になるほど、こういう小さな工夫によってデバッグしやすいプログラムになるものです。
while
文がfor
文でも書けそうなときは、プログラムが分かりやすくなりそうなほうを選びましょう。
for文と「無限ループ」
for
文は、無限ループを書くときにもよく使われます。具体的には、次のような書き方をします。
for (;;) { /* ← 無限ループ */
……
if (ループ条件をチェック) {
break; /* ← ここでループ終了 */
}
……
}
中括弧で囲まれた処理の、なかほどでbreak
していますね。無限ループでは、ここのif
文の判定がループ条件にあたります(正確には、ループを終了する条件ですが)。
この書き方は、ループの入り口でループ条件を判定できない場合に便利ね。
そっか。だから「なかほどでbreak
」なんですね。
ちなみに、こういうループは「n+1/2ループ(nと2分の1ループ)」などと呼ばれることもあります。通常のループが処理をn回繰り返すのだとしたら、こちらはあと1/2回多く処理するという意味ですね。
ループとunsignedの罠
ところで、signed
とunsigned
の比較は要注意だっていうのは聞いたことあるかしら?
え?なんの話ですか?
例えばint
は「符号あり」の値を表すからsigned
な型ね。
はい。
で、unsigned int
は「符号なし」よね。この2つの値を比較すると、思ったのと違う結果になることがあるのよ?
まさか、「-1」が「1」より大きいとか……いわないですよね?
あら、スルドイじゃない。
次の関数を見てください。for
文を使って、引数x
から「10」までの値を列挙しようとしています。
void valuesUpTo10(int x) {
unsigned int y = 10;
printf("Values:");
for (int i=x; i<=y; i++) {
printf(" %d", i);
}
printf("\n");
}
この関数をvaluesUpTo10(5);
と呼び出すと、出力は次のようになります。
Values: 5 6 7 8 9 10
ふむふむ。x
が「5」でy
が「10」だから、for
ループでこうなりますね。
それじゃあ、valuesUpTo10(-5);
と呼び出したらどうなるかしら?
なんだか嫌な予感がするなぁ……こんどはx
が「-5」だから、こうじゃないんですか?
Values: -5 -4 -3 -2 -1 0 1 2 3 4 5 6 7 8 9 10
それがね、こうなってしまうのよ。
Values:
1回もループしないってことですか?
そう。i
とy
の値を比較している部分に問題があるの。
ちなみに、for
文の代わりにwhile
文を使っても結果は一緒。
void valuesUpTo10(int x) {
unsigned int y = 10;
printf("Values:");
int i = x;
while (i <= y) {
printf(" %d", i);
i += 1;
}
printf("\n");
}
無限ループに書き換えて比較の部分をif
文にしても、やっぱり結果は変わらない。
void valuesUpTo10(int x) {
unsigned int y = 10;
printf("Values:");
int i = x;
for (;;) {
if (i > y) {
break;
}
printf(" %d", i);
i += 1;
}
printf("\n");
}
つまり、for
とかwhile
とかif
とかの違いは関係ないんですね。
signed
とunsigned
を比較しているところが問題ってことね。
ええと。y
がunsigned int
になってますね。ちょっと直してみていいですか?
y
をint
にしてみたらどうかな。
void valuesUpTo10(int x) {
int y = 10; /* ← unsigned を削除してみた */
printf("Values:");
for (int i=x; i<=y; i++) {
printf(" %d", i);
}
printf("\n");
}
あ、出ましたよ!
Values: -5 -4 -3 -2 -1 0 1 2 3 4 5 6 7 8 9 10
うん、直ったわね。何が原因だったかというと……
この関数では、変数i
とy
の値を比較しています。前者の型はint
、後者の型はunsigned int
です。でも型が違うままでは値を比較できません。そこで、実際には比較する前に、両辺の値が同じ型に揃えられます。
このとき、int
をunsigned int
に変換する処理が実行されます。そのため、int
の符号が失われてしまうのです。具体的には、次のようになります。
int
の値が0以上なら、unsigned int
に変換後も同じ値になる
int
の値がマイナスの場合は、unsigned int
に変換すると、int
で表現できる最大値よりも大きな値になってしまう
これにより、例えば-5 <= 10
という比較をしているつもりが、4294967291 <= 10
のような予想外な動きになっていたのです。
この問題は、実際の値がマイナスになるときだけ発生するっていうのが気を付けたいポイントね。
なるほど。だからvaluesUpTo10(5);
は大丈夫で、valuesUpTo10(-5);
とするとおかしな動作になったんですね。
そういうこと。ループ条件には値の大小を比較する式を書くことが多いけど、マイナスの値とunsigned
を一緒に使わなければ大丈夫よ。
はーい!
マイナスの値を扱うときは、unsigned
な値との比較に注意しましょう。