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つの値を比較すると、思ったのと違う結果になることがあるのよ?
次の関数を見てください。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:
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
な値との比較に注意しましょう。