プログラムで「条件が真である限り」処理を繰り返したいとき、C言語ではwhile文を使います。
前判定であるため、条件が最初に満たされなければ一度も実行されないのが特徴です。
本稿では、基本構文から設計の勘所、制御文の使い分け、実用サンプル、落とし穴までを体系的に解説します。
while文とは
概要
while文は、条件式が真の間だけ本体ブロックを繰り返し実行する制御構文です。
反復回数が事前に決まらない「状態依存の繰り返し」に適しています。
いつ使うか
- 入力が続く限り処理する(標準入力・ファイルのEOFまで)
- センサー値や状態が変わるまで待機・監視する
- 受信キューが空になるまで取り出す 反対に、回数が明確に決まっている場合はfor文が読みやすくなることが多いです。
基本構文と評価タイミング(前判定)
構文
// 条件式が真(非0)の間、本体を繰り返します
while (条件式) {
// 繰り返したい処理
// 状態更新(カウンタ増加、入力の読み進めなど)
}
前判定の意味
- whileはループに入る前に条件を評価します。
- 最初の判定で偽なら本体は一度も実行されません。
- そのため「最初は必ず1回は実行したい」という要件にはdo-whileが向きます(後述)。
条件式の真偽と書き方(0は偽・非0は真)
真偽のルール
C言語では、0が偽、0以外の値は真として評価されます。
ブール型bool
(<stdbool.h>)を使うと意図が明確になります。
値/式 | 真偽の解釈 |
---|---|
0 | 偽 |
非0の整数 | 真 |
NULL(ポインタ) | 偽 |
非NULLポインタ | 真 |
書き方の実例
#include <stdio.h>
#include <stdbool.h>
int main(void) {
int x = 5;
int *p = &x;
bool ok = (x > 0); // 比較の結果は0または1
if (x) { // 非0なので真
printf("x is non-zero\n");
}
if (p) { // 非NULLなので真
printf("p is non-NULL\n");
}
if (ok) { // boolでも同様に評価
printf("ok is true\n");
}
return 0;
}
x is non-zero
p is non-NULL
ok is true
終了条件の設計と無限ループの回避
終了条件は「観測可能な状態」を基準にする
終了条件は、ループ内で必ず変化・観測される状態に基づいて設計します。
例えば「読み込むデータがもうない」「カウンタが上限に達した」「フラグが立った」など、外部要因や内部更新で確実に変わるものを条件にします。
無限ループの代表例と安全な抜け方
// 良くない例:iが増えないため、条件が変化しない
int i = 0;
while (i < 10) {
printf("%d\n", i);
// i++ を忘れて無限ループ
}
// 安全なパターン:明示的な更新、上限のフェイルセーフ、breakの併用
int i = 0;
const int LIMIT = 10;
while (i < LIMIT) {
printf("%d\n", i);
i++;
if (i > 1000) { // フェイルセーフ
fprintf(stderr, "unexpected loop length\n");
break;
}
}
外部入出力に依存する待機ループでは、タイムアウトやキャンセル条件を併設します。
ループ内の状態更新と副作用の扱い
更新の原則
- ループの最後で「次の反復に進むための更新」を必ず行います(カウンタの増加、入力の読み進めなど)。
- 更新は原則として本体内にまとめ、条件式の中での更新(副作用)は最小限にします。可読性とバグ回避のためです。
条件式での副作用の典型
// 代表的で有用:入力を読み進めながら判定
int c;
while ((c = getchar()) != EOF) { // 代入と比較を合わせたイディオム
putchar(c);
}
このようなイディオムはCでは一般的ですが、複雑な副作用を条件式に埋め込むと意図が読みづらくなります。
簡潔さと明確さのバランスを取りましょう。
ループ制御:break・continue・returnの使い分け
それぞれの意味
- break: 現在のループを即座に抜ける
- continue: 現在反復の残りをスキップし、次の判定へ
- return: 関数自体を終了(ループの外も含めて抜ける)
使い分けの指針
- 正常系での通常終了は「条件が偽になることで抜ける」が基本。
- 想定内の早期終了(見つかったら抜けるなど)はbreak。
- スキップ条件はcontinueで本体をすっきり保つ。
- 関数レベルの致命的エラーや即時終了はreturn。
#include <stdio.h>
int main(void) {
for (int i = 1; i <= 10; i++) {
if (i % 3 == 0) { // 3の倍数はスキップ
continue;
}
if (i == 8) { // 8に到達したらループ終了
break;
}
printf("%d ", i);
}
printf("\nDone\n");
return 0; // 関数終了
}
1 2 4 5 7
Done
サンプル:カウンタでN回繰り返す
説明
ユーザーからNを受け取り、1からNまでの整数を表示します。
前判定の性質により、Nが0以下なら本体は一度も実行されません。
#include <stdio.h>
int main(void) {
int N;
if (scanf("%d", &N) != 1) {
fprintf(stderr, "Nの読み取りに失敗しました\n");
return 1;
}
int i = 1;
while (i <= N) {
printf("%d", i);
if (i < N) {
printf(" ");
}
i++; // 状態更新を忘れない
}
printf("\n");
return 0;
}
5
1 2 3 4 5
サンプル:入力が有効である限り処理する(EOF/検証)
scanfとEOFを用いた簡潔な読み取り
scanf
は成功した変換数を返すため、整数の読み取りに成功した場合にのみ処理し、EOFで終了できます。
#include <stdio.h>
int main(void) {
long long sum = 0;
long long x;
// 整数が読めた間だけ加算。EOF(または非数)で抜ける。
while (scanf("%lld", &x) == 1) {
sum += x;
}
printf("sum = %lld\n", sum);
return 0;
}
10 20
-5
abc
30
sum = 25
上例ではabc
に到達した時点でscanf
は失敗し、ループを終了します。
純粋にEOFまで読みたい場合は、入力のクレンジングやエラー処理を別途行うほうが安全です。
fgets + strtolで厳密に検証する
行単位で読み取り、数値に変換できた行だけ加算します。
無効な行はスキップし、EOFで終了します。
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
int main(void) {
char buf[128];
long long sum = 0;
while (fgets(buf, sizeof buf, stdin) != NULL) {
char *end;
errno = 0;
long long v = strtoll(buf, &end, 10);
// 行頭から少なくとも1文字は変換され、末尾は空白か改行のみ
while (*end == ' ' || *end == '\t' || *end == '\n') {
end++;
}
if (errno == 0 && end != buf && *end == '\0') {
sum += v;
} else {
fprintf(stderr, "無効な行をスキップ: %s", buf);
}
}
printf("sum = %lld\n", sum);
return 0;
}
12
+8
oops
-3
無効な行をスキップ: oops
sum = 17
サンプル:文字列・配列を走査する
文字列を終端まで走査して統計を取る
#include <stdio.h>
#include <ctype.h>
int main(void) {
const char *s = "Hello, C while loop! 123";
int letters = 0, digits = 0, spaces = 0;
int i = 0;
while (s[i] != '\0') { // ヌル終端に到達するまで
if (isalpha((unsigned char)s[i])) {
letters++;
} else if (isdigit((unsigned char)s[i])) {
digits++;
} else if (isspace((unsigned char)s[i])) {
spaces++;
}
i++;
}
printf("letters=%d, digits=%d, spaces=%d\n", letters, digits, spaces);
return 0;
}
letters=16, digits=3, spaces=3
配列を走査して合計を計算する
#include <stdio.h>
int main(void) {
int a[] = {3, 1, 4, 1, 5, 9};
int n = (int)(sizeof a / sizeof a[0]);
int i = 0;
int sum = 0;
while (i < n) {
sum += a[i];
i++;
}
printf("sum=%d\n", sum);
return 0;
}
sum=23
do-whileとの違いと使い分け
特徴比較
項目 | while | do-while |
---|---|---|
判定タイミング | 前判定(最初に判定) | 後判定(少なくとも1回は実行) |
主な用途 | 条件成立時のみ実行 | 初回は必ず実行したい初期化・メニュー |
可読性 | 一般的で頻出 | 条件が後ろで見落としに注意 |
使い分け例
- メニューを1回表示し、ユーザーが終了を選ぶまで続ける → do-while
- 入力が成功したときだけ処理する → while
よくある落とし穴(=と==の取り違え、浮動小数の比較)
代入演算子と等価比較の取り違え
// 誤り:条件式で代入してしまっている
int done = 0;
while (done = 0) { // 0が代入され、常に偽→ループに入らない
/* ... */
}
// 正しい例:比較を使う
int done2 = 0;
while (done2 == 0) {
/* ... */
done2 = 1; // どこかで更新
}
コンパイラの警告(-Wall -Wextra)を有効にすると、こうした誤りを検出しやすくなります。
浮動小数の直接比較
丸め誤差のため、x != 0.0
のような比較で終了判定すると無限ループの原因になります。
許容誤差(イプシロン)で比較します。
#include <stdio.h>
#include <math.h>
int main(void) {
double x = 1.0;
const double eps = 1e-9;
int count = 0;
while (fabs(x) > eps) { // 0.0 と「ほぼ等しい」かで判定
x /= 2.0;
count++;
if (count > 1000) { // フェイルセーフ
fprintf(stderr, "収束せずに終了\n");
break;
}
}
printf("x=%.12f, steps=%d\n", x, count);
return 0;
}
x=0.000000476837, steps=21
可読性・保守性を高めるコツ
条件は「述語(真偽が読み取れる表現)」にする
while (has_more_data())
のように関数名や変数名で意図が伝わるように設計します。
否定の二重化は避け、必要なら関数を分けます。
本体は短く、更新は一箇所に
ループ本体を短く保ち、状態更新は見落とさない位置(通常は末尾)にまとめます。
複数箇所で更新するとバグの温床になります。
常にブレースを付ける
一行でも{}
を省略しないことで、行追加時のバグを防げます。
while (cond) {
do_something();
}
マジックナンバーを避け、定数名を使う
const int TIMEOUT_MS = 5000;
while (!ready() && elapsed_ms() < TIMEOUT_MS) {
/* ... */
}
警告を最大限に活用し、静的解析も併用する
-Wall -Wextra -Wconversion -Wshadow
などを有効にし、clang-tidy等も活用すると、典型的なループバグ(未初期化、オーバーフロー、条件の恒真/恒偽)を早期に発見できます。
入出力・同期はタイムアウトやリトライ方針を明記する
I/O待ちのwhileはタイムアウト・最大リトライ回数・キャンセル条件を明示し、ログも残すと運用で困りません。
まとめ
while文は「条件が真である限り」処理を繰り返す前判定の基本構文であり、入力処理、監視、走査など幅広い場面で役立ちます。
確実に変化する終了条件を設計し、状態更新を明確に保つことが無限ループ回避の鍵です。
break・continue・returnを適切に使い分け、浮動小数の比較や=
/==
の取り違えといった落とし穴を避けましょう。
可読性を意識した述語的な条件、短い本体、明示的な更新、警告の活用により、保守しやすく安全なループを実装できます。