Menu

C言語のforループ:構文、例、よくある間違い

C言語のforループでコードを繰り返す方法 - 3つのパートからなるヘッダー、増加・減少のカウント、sizeofの技を使った配列の走査、ネストしたループ、無限ループ、そして誰もがはまる off-by-one と符号なし型のバグ。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

なぜforループなのか

ifswitch は分岐を選んで一度だけ実行します。実際のプログラムでは、何かを繰り返し行う必要があります。すべてのスコアを表示する、数値のリストを合計する、グリッドの10行を描画する、文字列の各文字を調べる、といった具合です。for ループは、決まった回数だけコードを繰り返すためのC言語の主力で、自分で制御するカウンタを備えています。

for ループに必要なものはすべて1つのコンパクトなヘッダーに収まっているので、「何回、どのように」が1行で見て取れます。

3つのパートからなるヘッダー

for のヘッダーには、セミコロンで区切られた3つのパートがあります。初期化子条件更新です。

for (initializer; condition; update) {
    // 本体 - 条件が非ゼロである間は実行される
}

これらは特定の順序で実行され、その順序を知っていれば、これから読むあらゆる for ループを説明できます。

  1. 初期化子が、ほかの何よりも先に一度だけ実行されます。
  2. 条件がテストされます。ゼロならループは即座に終了します。
  3. 本体が実行されます。
  4. 更新が実行されます。
  5. ステップ2に戻ります。

このリストで上のループを追ってみましょう。int i = 0 が一度実行されます。0 < 5 が成り立つので本体が i = 0 を表示し、i++i1 になります。そしてまた条件がテストされ、以下同様です。i5 に達するとテストは失敗し、本体は飛ばされて 完了 が表示されます。本体はちょうど5回実行され、i0 から 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 <= nn + 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言語の慣用的な無限ループで、セミコロンは依然として必要です。内側に breakreturnexit できる何かがあるかぎり間違いではありません - イベントループや「入力が正しくなるまで聞き続ける」ループは、まさにこう書かれます。

ほかのパートも落とせます。カウンタがすでに存在するなら初期化子を、本体がそれを進めるなら更新を省きます。

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 は常に真です。i0 に達すると 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 をループの前で宣言していました。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める