C言語のfor文とwhile文の使い分け方
目次
- C言語のfor文とwhile文の使い分け
- for文の構造のおさらい
- while文の構造のおさらい
- for文はどう言う時に使われるか?
- while文はどう言う時に使えるか?
- 無限ループではどちらを使うべきか?
- ケン・トンプソンとロブ・パイクはfor文をどう使っているか?
- while文はいらない子?
- 結論は?
C言語のfor文とwhile文の使い分け
C言語を使っているとループ文がいくつか種類があることに気が付くと思います。
for文、while文、do-while文です。
今回はこの中からfor文とwhile文の使い分けを解説したいと思います。
結論としては
カウント変数の類が必要ならfor文
カウント変数の類がいらないならwhile文
という結論になりますが、この2つのループ文の特徴を掴んでおくのは有用と言えます。
for文やwhile文の構造から解説し、果てはハッカーであるケン・トンプソン達がどのようにfor文を使っているか? まで解説していきます。
for文の構造のおさらい
まずfor文の構造のおさらいからです。
for文は↓のような構造になっています。
for (初期化式; 比較式; 更新式) { 処理1; 処理2; ... }
これを実際のコードにすると↓のようになります。
for (int i = 0; i < 4; i++) { printf("%d\n", i); }
↑のサンプルコードはカウント変数を0
から4
より下までカウントし、それを出力するコードです。
かなり基本的なfor文です。
まず初期化式は「int i = 0;
」になります。ここでカウント変数i
を初期化しています。
そして比較式が「i < 4;
」になります。ここの比較が真(true
)の間、ループが実行されます。
それで更新式が「i++
」になります。この更新式はループが1回終わるごとに実行されます。
「printf("%d\n", i);
」はループ内で実行される処理です。これはカウント変数を出力しています。
for文では↓の順序で処理が進んでいきます。
- 初期化式の実行
- 比較式の実行
- 処理の実行
- 更新式の実行
- 2に戻る
つまり初期化して比較、そしてループ処理をして更新、そしてまた比較に戻る。
という感じですね。
for文もかみ砕いて解説するとけっこう複雑かもしれません。
しかし実際にコードを書いて見ればけっこうすんなり覚えられますよね。
while文の構造のおさらい
ついでwhile文の構造のおさらいです。
while文は↓のような構造をしています。
while (比較式) { 処理1; 処理2; ... }
for文に比べるとかなりあっさりした構造です。
処理の順序的には
- 比較式の実行
- 処理の実行
- 1に戻る
という感じで進みます。
比較式が真のあいだ処理が実行されていきます。
サンプルコードは
int i = 0; while (i < 4) { printf("%d\n", i); i++; }
になります。
上の場合、まずwhile文の外で「int i = 0;
」を実行してカウント変数i
を初期化しています。
そしてwhile文に入って「i < 4
」で比較します。
そして「printf("%d\n", i);
」でカウント変数を出力してループのお尻の方で「i++;
」を実行しカウント変数をインクリメントします。
for文はどう言う時に使われるか?
それでfor文はどう言う時に使われるのでしょうか?
for文とかwhile文とかいろいろあります。
その中でfor文を使う理由はなんでしょうか?
これはまずfor文は汎用的なループ文であるというのが大事な点です。
for文は汎用的なfor文で全般に使える
for文は汎用的、つまり色々なケースで使われるループ文です。
C言語でループを書きたい場合にまず最初に候補に挙がるのがfor文です。
というか、ほとんどのループはfor文を使えば書けます。
たとえば0
から4
より下までカウントしたい時は先ほども登場した
for (int i = 0; i < 4; i++) { printf("%d\n", i); }
というコードが書けます。
カウント変数の初期化はfor文の外で行うこともできるのでこれは、
int i = 0; for (; i < 4; i++) { printf("%d\n", i); }
という風に変形もできます。
また更新式の部分のインクリメントもループ内で行うことができますので、
int i = 0; for (; i < 4; ) { printf("%d\n", i); i++; }
と書くこともできます。
これはほぼ先ほどのカウント変数を使ったwhile文と同じ形になりますよね。
また更新式を省略すればfor文は無限ループにすることもできます。
for (;;) { printf("Hello\n"); }
こういう感じでfor文はありとあらゆるケースのループを書くことができます。
それならなんでwhile文とかあるのか? というところです。
これは言語設計者のデニス・リッチーの考えを読まないといけません。
おそらくデニス・リッチーはループ文のパターンを考えて、そのパターンに特化したループ文をそれぞれ考えたんだと思います。
つまり書き手の労力を減らすために文を追加したということです。
当時の書き手と言えばC言語でUNIXを開発していたケン・トンプソンやデニス・リッチーですので、彼らは彼ら自身の労力を減らすためにC言語をそのように設計したことになります。
C言語自体はUNIXの開発過程で生まれた言語で、C言語の開発自体は1971年に着手されたそうです。
その頃のUNIXを開発していたケン・トンプソンやデニス・リッチーはカーネルを含むシステムの全てを高水準言語で記述するという目標をおそらく持っていたと言われています。
ですのでC言語の効率性を高めるのはUNIXの開発の面から見ても非常に重要だったと推測できます。
ですのでループ文も複数用意してコーディングの効率を高めようとしたという想像ができます。
もっともC言語自体はB言語の影響を強く受けていますのでB言語由来である可能性も否定できません。
特にカウント変数が必要な時に使える
for文は特にカウント変数が必要な時に使えます。
カウント変数はさきほどから登場しているi
などの変数のことです。
ループではこのカウント変数をカウントして処理を継続していきます。
またカウント変数の他にはポインタなども代わりに使われます。
たとえば文字列をfor文で回したい時などは
const char *s = "Hello"; for (const char *p = s; *p; p++) { printf("%c\n", *p); }
などとよく書かれます。
↑の場合、カウント変数の代わりになっているのはポインタ変数の*p
です。
これに文字列の先頭アドレスを代入し、ポインタの指す文字がナル文字じゃない間、ループをします。
このようにfor文ではポインタ変数もあわせてよく使われます。
たとえばリスト構造を走査したい時は
for (ListItem *item = head; item; item = item->next) { printf("%d\n", item->value); }
こういう感じのループ文もよく書かれます。
リスト構造とfor文は相性がよくこのように綺麗に書くことができます。
while文はどう言う時に使えるか?
while文はどう言う時に使えるのでしょうか?
これはカウント変数が必要でない時によく使われます。
たとえばファイルポインタからfgetc()
を使ってデータを取り出すとき、fgetc()
はファイル終端に達するとEOF
を返します。
これを利用することで
int c; while ((c = fgetc(stdin)) != EOF) { printf("%c\n", c); }
↑のように美しいループを書くことができます。
ちなみにこれをfor文で書き直すと
for (int c; (c = fgetc(stdin)) != EOF; ) { printf("%c\n", c); }
こういう感じになります。
while文も美しいですがfor文もやはりすべてがfor文の中に納まっているという点で美しいと思います。
while文はfor文の縮小版である
while文はfor文から機能を削ったものです。
for文には
初期化式
比較式
更新式
処理
などの要素があります。
while文は
比較式
処理
だけになります。
for文と比べるとずいぶん要素が削られていてスマートになっています。
このようにwhile文はfor文に比べると学習コストが低いというメリットがあります。
カウント変数を使うこともできるが・・・
while文でもカウント変数を使うことは可能です。
その場合は
int i = 0; while (i < 4) { printf("%d\n", i); i++; }
という感じのコードになります。
しかしこのような形のループを書きたいのであればfor文を使うことをおすすめします。
C言語のようなfor文を書けない言語もありますが、C言語は柔軟なfor文を書くことができますので使わない手はないです。
↑のwhile文の問題点はループ末尾のカウント変数です。
カウント変数をカウントするときに見栄えが下がる
ループ末尾のカウント変数をカウントするわけですが、さきほどのような短い処理ならいいのですがたとえばwhile文の中身の処理が長くなってしまったらどうでしょうか?
その場合はカウント変数のインクリメントはループ末尾にありますので、カウント変数のインクリメントのチェックはいちいちループ末尾までエディタをスクロールしないといけません。
for文を使えばループの先頭で更新処理を書けますので、スクロールする必要もありません。
カウント変数を使わない場合はwhile文?
ですがwhile文はかなり省コストな文です。
たとえば比較式だけのfor文とwhile文を比べてみましょう。
for (; i < 4; ) { } while (i < 4) { }
for文の方はセミコロンとか入ってて多少美しさが低下します。
ですがwhile文はピッタリドンピシャで美しいコードになります。
多少の美しさの低下を気にしないならfor文でループ文を統一する手もあるでしょう。
じっさいこれは合理的な考えです。
ですがコードの美しさを追求したいのであればwhile文もあわせて使っていった方がいいかもしれません。
無限ループではどちらを使うべきか?
無限ループではどちらを使うべきでしょうか?
これも比べてみましょう。
まずはfor文から。
for (;;) { }
次にwhile文です。
while (1) { }
どちらもシンプルですが、1つ大きな違いがあります。
それはwhile文はマジックナンバーを使っている点です。
マジックナンバーはバグが混入する場合がある
while文の無限ループはマジックナンバーを使っています。
マジックナンバーはタイポの可能性があります。
たとえば「1
」をタイポして「0
」と書いてしまうケース。
while (0) { }
このタイポをすると無限ループは実行されません。
ですのでwhile文の無限ループはタイポの可能性がある分、for文の無限ループよりは劣るかもしれません。
for文はセミコロンなどをタイポしてもコンパイルエラーになりますからね。
ケン・トンプソンとロブ・パイクはfor文をどう使っているか?
アメリカの有名なハッカーであるケン・トンプソンとロブ・パイクは「プログラミング作法」という本で次のように述べています。
無限ループに関しては、我々自身は次の書き方が好きだが、
for (;;)
ケン・トンプソンとロブ・パイクは無限ループについてはfor文を使う方が好きらしいです。
しかしwhile文の無限ループを否定しているわけではありません。
つまり極論、これは好みですね。
Goではwhile文は廃止された
ロブ・パイクはGo言語の設計でも知られています。
そのGo言語ではwhile文は廃止されています。
つまりGo言語では無限ループは
for { }
のように書けるようになっています。
これはC言語のfor (;;)
をブラッシュアップした結果と言えるでしょう。
while文はいらない子?
while文はいらない子でしょうか?
そんなこと言ったら言語設計者のデニス・リッチーが悲しむか、あるいは「うん、そうだな」と納得するかもしれません。
実際のところwhile文はいるのでしょうか?
先ほどの話では美しさを追求するならwhile文も使っていった方が良いという話でした。
しかし見た目の美しさも合理性の美しさの前では無力と言えます。
while文は使わなくてもfor文で代用できる
while文はfor文で代用できるという点がやはり強いです。
while文を使わずにGoのようにfor文で統一するという選択肢もありでしょう。
しかし私としてはやはり気分で書き分けたいところです。
for文を書くのが飽きたらたまにはwhile文も使うという感じですね。
そういう遊び心は長いプログラミングをしていく上で大事と言えます。
もっともバグを混入しなければの話ですが。
結論は?
結論としては
for文はカウント変数を使いたい時に使う
while文はカウント変数が必要ない時に使う
ということになります。
for文とwhile文のどっちを使うか迷ったらカウント変数をヒントに考えてみるといいでしょう。
好きな方を使え
またこれは完全に私の意見ですが、極論として
好きな方を使え
というのも加えておきたいと思います。
C言語は自分のコーディングスタイルを確立していくという楽しさもあります。
ですので自分のルールを考えてそれを適用してもいいんです。
もっともグループ開発ではグループのルールに従う必要があります。
この辺は注意が必要です。
以上です。
なにか参考になれば幸いです。