あやしいfor文

デバッグしやすくするには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の罠

ところで、signedunsignedの比較は要注意だっていうのは聞いたことあるかしら?
え?なんの話ですか?
例えば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回もループしないってことですか?
そう。iyの値を比較している部分に問題があるの。
ちなみに、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とかの違いは関係ないんですね。
signedunsignedを比較しているところが問題ってことね。
ええと。yunsigned intになってますね。ちょっと直してみていいですか?
yintにしてみたらどうかな。
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
うん、直ったわね。何が原因だったかというと……

この関数では、変数iyの値を比較しています。前者の型はint、後者の型はunsigned intです。でも型が違うままでは値を比較できません。そこで、実際には比較する前に、両辺の値が同じ型に揃えられます。

このとき、intunsigned intに変換する処理が実行されます。そのため、intの符号が失われてしまうのです。具体的には、次のようになります。

  • intの値が0以上なら、unsigned intに変換後も同じ値になる
  • intの値がマイナスの場合は、unsigned intに変換すると、intで表現できる最大値よりも大きな値になってしまう

これにより、例えば-5 <= 10という比較をしているつもりが、4294967291 <= 10のような予想外な動きになっていたのです。

この問題は、実際の値がマイナスになるときだけ発生するっていうのが気を付けたいポイントね。
なるほど。だからvaluesUpTo10(5);は大丈夫で、valuesUpTo10(-5);とするとおかしな動作になったんですね。
そういうこと。ループ条件には値の大小を比較する式を書くことが多いけど、マイナスの値とunsignedを一緒に使わなければ大丈夫よ。
はーい!
ここがポイント!
マイナスの値を扱うときは、unsignedな値との比較に注意しましょう。