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な値との比較に注意しましょう。
