なぜforループなのか
if と switch は分岐を選んで一度だけ実行します。実際のプログラムでは、何かを繰り返し行う必要があります。すべてのスコアを表示する、数値のリストを合計する、グリッドの10行を描画する、文字列の各文字を調べる、といった具合です。for ループは、決まった回数だけコードを繰り返すためのC言語の主力で、自分で制御するカウンタを備えています。
for ループに必要なものはすべて1つのコンパクトなヘッダーに収まっているので、「何回、どのように」が1行で見て取れます。
3つのパートからなるヘッダー
for のヘッダーには、セミコロンで区切られた3つのパートがあります。初期化子、条件、更新です。
for (initializer; condition; update) {
// 本体 - 条件が非ゼロである間は実行される
}
これらは特定の順序で実行され、その順序を知っていれば、これから読むあらゆる for ループを説明できます。
- 初期化子が、ほかの何よりも先に一度だけ実行されます。
- 条件がテストされます。ゼロならループは即座に終了します。
- 本体が実行されます。
- 更新が実行されます。
- ステップ2に戻ります。
このリストで上のループを追ってみましょう。int i = 0 が一度実行されます。0 < 5 が成り立つので本体が i = 0 を表示し、i++ で i は 1 になります。そしてまた条件がテストされ、以下同様です。i が 5 に達するとテストは失敗し、本体は飛ばされて 完了 が表示されます。本体はちょうど5回実行され、i は 0 から 4 までの値を取りました。
ここから2つの帰結が直接導かれます。条件は最初の周回の前にチェックされるので、条件が最初から偽のループは0回実行されます - for (int i = 10; i < 5; i++) は本体に一度も入りません。そして更新は本体の後に実行されるので、本体の中では i は古い値を保ったままです。
カウンタをヘッダー内で宣言すること(int i = 0)はC99以降で合法であり、正しいデフォルトです。i はループの中にだけ存在するので、後続の何かと衝突することがなく、関数内の各ループが同じ名前を自由に再利用できます。
増加、減少、刻みでのカウント
更新のパートは i++ に限りません。減少させたり、任意の量で刻んだり、毎回2倍にしたりできます。
条件と更新を合わせましょう。増加のカウントは < か <= と、減少のカウントは > か >= と組み合わせます。この組み合わせを間違えると、終わらないループができあがります。
2つ目のループが <= 10 を使っているのは 10 が含めたい値だからで、最初の例が < 5 を使ったのは 5 がそうではないからです。この選択こそ off-by-one のバグが生まれる場所なので、意識して選びましょう。i < n は 0 から始めて n 回、i <= n は n + 1 回実行されます。
配列をループする
カウントループの最も一般的な用途は、配列を添字でたどることです。カウンタは、読み取る位置も兼ねます。
ここから持ち帰るべきことが2つあります。
sizeof の技。 sizeof(scores) は配列全体のバイト数、sizeof(scores[0]) は1要素のバイト数なので、その商が要素数になります。要素を追加しても自動的に追随してくれる点が、ハードコードされた 5 とは違います。ただし落とし穴 - しかも大きなもの - があって、これは配列そのものがスコープにある場所でしか動きません。配列を関数に渡すとポインタに成り下がるので、そこでの sizeof はポインタのサイズを返します。関数の中では、長さは必ず別の引数として受け取りましょう。
条件は i < n であって、決して i <= n ではない。 5要素の配列で有効な添字は 0 から 4 です。scores[5] を読み取るのは未定義動作で、ゴミを表示したり、クラッシュしたり、動いているように見えて静かにほかの何かを壊したりします。Cは境界チェックをまったく行わないので、これは完全にあなたの責任です。
配列を後ろ向きにたどるのも、同じ考えを逆にするだけです。
n - 1(最後の有効な添字)から始め、0 を含めて止まる - だから > ではなく >= です。ここでは i を符号付きの int のままにしておきましょう。その理由は次の節で説明します。
ネストしたループ
グリッド、テーブル、あるいは要素のすべての組を扱うには、for を別の for の中に入れます。内側のループは、外側のループの1ステップごとに最後まで実行されます。
外側のループが row を固定し、内側のループがその行のすべての col を走査し、内側のループの後の printf("\n") が行を終えます。その改行を内側のループの中へ移すと、表全体が1列になります - ネストの構造が具体的に分かるので、試してみる価値があります。
カウンタには別々の名前を付けましょう。row/col でも i/j でも構いませんが、両方に i を使い回すと外側のカウンタを覆い隠し、わけの分からない結果を生みます。コストにも注意してください。n 回のループを n 回のループの中に入れると本体は n * n 回実行されるので、1,000回のループ2つで100万周になります。
内側のループの上限が外側のカウンタに依存する、実際の仕事をするネストしたループの例です - 星の三角形です。
break と continue
2つのキーワードがループの途中で流れを変えます。break はループから即座に抜け、continue は現在の周回の残りを飛ばして更新へジャンプします。
最初のループは 7 を見つけた瞬間に止まり、残りの93個の値は一切チェックしません。2つ目は偶数のときに表示を飛ばすために continue を使いますが、ヘッダーの i++ は依然として実行されるのでループは進み続けます。両者については break と continue にもっと詳しく書かれています。単一の break ではできない、ネストしたループからの脱出方法も含めて。
無限ループと空のパート
ヘッダーの3つのパートはすべて省略できます。条件を書かなければ、永続的に真として扱われます。
for (;;) {
/* 永遠に実行される - break か return で抜ける */
}
for (;;) はC言語の慣用的な無限ループで、セミコロンは依然として必要です。内側に break、return、exit できる何かがあるかぎり間違いではありません - イベントループや「入力が正しくなるまで聞き続ける」ループは、まさにこう書かれます。
ほかのパートも落とせます。カウンタがすでに存在するなら初期化子を、本体がそれを進めるなら更新を省きます。
int i = 0;
for (; i < n; ) {
/* ... */
i += step;
}
これは合法ですが、実際のところそれが本当は while ループであるぶん、読みにくくなります。カウンタ・その上限・その刻みがヘッダーにまとまって属するときは for を、そうでないときは while ループを選びましょう。
ヘッダーの中のコンマ演算子
初期化子と更新はそれぞれ、コンマで区切った複数の式を持てます。2つのカウンタを同時に動かすのはこの方法です。
int i = 0, j = len - 1 が2つのカウンタを宣言し、i++, j-- が両方を進めます。これがコンマ演算子で、オペランドを順に評価します。本当に並行するカウンタに使いましょう。無関係な処理をヘッダーに詰め込むのは、それを隠すだけです。
よくある落とし穴
いくつかの罠が、C言語のforループのバグの大半を占めます。
off-by-one。 ゼロ始まりの配列に対する i <= n は、末尾を越えて1要素読み取ります。「0から始めてn回」には i < n を使いましょう。
ヘッダーの後のセミコロン。 これはコンパイルが通りますが、まず意図したものではありません。
/* BUG: ループ本体は空文であり、printf は一度だけ実行される */
for (int i = 0; i < 5; i++);
{
printf("%d\n", i);
}
; が本体になるので、ループは5回何もせずに回り、下の波かっこは単なるブロックとして一度だけ実行されます。gcc -Wall はこれを警告します。
符号なしカウンタでの減少のカウント。 これは無限ループです。
/* BUG: 符号なしの値は決して < 0 にならない */
for (size_t i = n - 1; i >= 0; i--) {
process(a[i]);
}
符号なし型では i >= 0 は常に真です。i が 0 に達すると i-- は巨大な正の数へ巻き戻り、ループは自分のものではないメモリへ進んでいきます。減少のカウントには符号付きの int を使うか、条件を for (size_t i = n; i-- > 0; ) と書きましょう。これは1ステップでデクリメントとテストを行い、ゼロで正しく止まります。
2か所でカウンタを変更する。 ヘッダーに加えて本体でも i を変えると、反復回数が予測できなくなります。1か所に決めましょう。
浮動小数点のカウンタ。 for (double x = 0.0; x != 1.0; x += 0.1) は終わらないことがあります。0.1 には正確な2進表現がなく、合計は 1.0 にぴったり当たらないまま通り過ぎるからです。整数のカウントでループし、値は内部で計算しましょう。
for (int i = 0; i <= 10; i++) {
double x = i / 10.0;
/* ... */
}
ループ中に配列の長さを変える。 条件 i < n は毎周 n を読み直すので、本体がコレクションを縮めるなら、両者がずれるのを放置せず、意図的に添字を調整しましょう。
よくある質問
C言語でforループはどう書きますか?
ヘッダーにセミコロンで区切った3つのパート、つまり初期化子・条件・更新を置きます:for (int i = 0; i < 5; i++) { printf("%d\n", i); }。これは i が 0 から 4 までの値を取りながら本体を5回実行し、条件が偽になった時点で止まります。
C言語で配列をループするには?
添字を 0 から長さの手前まで数えます:for (int i = 0; i < n; i++) { sum += a[i]; }。同じ関数の中で宣言された配列なら sizeof(a) / sizeof(a[0]) で長さを計算できますが、いったん関数に渡してしまうとこの技は使えなくなるので、長さを引数として渡しましょう。
なぜ私のC言語のforループは1回多く実行されるのですか?
それが off-by-one(1つずれ)のバグです。サイズ n のゼロ始まりの配列では有効な添字は 0 から n - 1 なので、i <= n は余分に1回反復して末尾の外を読み取ります - 未定義動作です。安全なデフォルトは i < n です。
C言語のforループの中でループ変数を宣言できますか?
C99以降はできます:for (int i = 0; i < n; i++)。この変数はループの中にだけ存在し、それが望ましい挙動です。gcc -std=c99 以降でコンパイルしてください(最近のGCCやclangは既定でC17なので、そのまま動きます)。古いC89スタイルでは i をループの前で宣言していました。