【C言語】無限ループが発生する原因

無限ループの原因解説ページアイキャッチ

このページでは、無限ループが発生する原因の説明および、意図せず無限ループになってしまうループの例の紹介をしていきたいと思います!

forwhile を使えば簡単に利用できるループですが、下手すると意図せず無限ループになってしまい、プログラムが終わらなくなってしまうことも多いです。

そんな無限ループが起きてしまったような場合に、どのあたりに注目して原因を突き止めていけば良いのかについて、実際の例も踏まえながら解説していきたいと思います。

C言語の場合は「型」などの影響によって無限ループが発生することもありますので、その辺りの例を見ていただくことで、型の重要性なども理解していただけるのではないかと思います。

ちなみに、無限ループに陥ってしまったプログラムの停止方法は下記ページで解説していますので、プログラムの停止方法について知りたい方は下記ページをご参照いただければと思います。

無限ループ中のプログラムを強制終了させる方法の解説ページアイキャッチ無限ループで止まらなくなったプログラムを強制終了する方法

無限ループが発生する原因:終了条件が成立しない

無限ループはさまざまな要因で発生しますが、無限ループの根本的な原因は終了条件が成立しない点にあります。

この点について、終了条件についてあまり馴染みのない方もおられるかもしれませんので、継続条件と終了条件について整理しながら解説していきたいと思います。

継続条件が成立している間ループが続く

C言語においては、forwhile、さらには do while によりループが実現できます。

例えば for ループでは、ループを実現する際に下記のように3つの式を記述することが多いですが、for ループにおいては下記の 条件式 が成立している間、for 文の内側のブロックの処理が繰り返し実行されることになります。

forループ

for (初期化式; 条件式; 変化式) {
    繰り返す処理
}

つまり、for ループの 条件式 に指定するのはループの継続条件ということになります。

while ループにおいても 条件式 を指定しますが、この 条件式継続条件となります。

whileループ

while (条件式) {
    繰り返す処理
}

つまり、条件式 が成立している間、while ループの内側の処理が繰り返し実行されます。同様に do while でも指定するのは継続条件となります。

do whileループ

do {
    繰り返す処理
} while (条件式);

スポンサーリンク

終了条件を満たさないとループは終わらない

ここまで for ループや while ループ等で指定する 条件式 について解説してきましたが、全て継続条件でしたね!

では終了条件とはなんでしょうか?

終了条件とは、継続条件の逆で、その名前のとおり、ループが終了する条件になります。

例えば下記の for ループであれば i < 10、つまり「i10 未満である」が継続条件です。逆に考えれば「i10 未満である」が成立しなくなったらループが終了しますので、終了条件は i >= 10、つまり「 i10 以上である」になります。

iが10未満の間ループ

for (i = 0; i < 10; i++) {
    printf("%d\n", i);
}

このように、継続条件を裏返しに考えた時の条件が終了条件です。そして、この終了条件が成立した際にループは終了します。

通常の for ループや while ループでは継続条件を指定することの方が多いですが、下記のように終了条件を指定することで期待するループを実現するようなこともできます。

iが10以上になったらループ終了

while (1) {

    if (i >= 10) { // 終了条件
        break;
    }

    printf("%d\n", i);
    i++;
}

ちなみに上記で使用した break については下記ページで解説していますので、詳しく知りたい方は是非読んでみてください。

breakとcontinueの解説ページアイキャッチ【C言語】breakとcontinueについて解説(ループを抜け出す・ループをスキップする)

当然ですが、ループは終了条件が成立するまで繰り返し実行されます。逆に言えば、終了条件が成立しないとループは終了しません。つまり、無限ループに陥ってしまう理由は、おおざっぱに言ってしまえば「終了条件が成立しない」ことと言えます。

これは個人的な感覚かもしれませんが、無限ループが発生した際の原因を考える際は、継続条件を確認するよりも終了条件を確認する終了条件が本当に成立するかを考える方が、原因解明しやすいと思います。

無限ループに陥ってしまう例

次は実際にソースコードを見ながら、どんな時にどんな原因で無限ループに陥いることになるのか確認していきたいと思います!

終了条件に移行するための処理が抜けている

前述の通り、ループが終了するのはループの終了条件が成立した時になります。

ですので、意図せず無限ループに陥ってしまったような場合、あなたが書いた処理によって本当に終了条件が満たされるようになっているか?あなたが書いた終了条件は正しいのか?のあたりをまずは確認してみましょう。

例えば下記のような処理は無限ループになってしまいます。

終了条件に移行しない例

i = 0;
while (i < 10) {
    printf("%d\n", i);
}

この場合、継続条件が i < 10 なので終了条件は i >= 10 となります。ただし、i は初期値 0 が代入された後に変更されませんので、終了条件 i >= 10 が成立することはありません。

なので、上記のループは無限ループとなります。

客観的に見れば無限ループに陥る原因は一目瞭然ですが、割とやりがちな失敗です。

ループを終了させるためには終了条件が成立する必要があります。そして、無限ループにしたくないのであれば、いずれは終了条件が成立するように処理を記述してやる必要があります。

紹介する必要もないかもしれませんが、上記の while ループは次のように変更することで無限ループを防ぐことができます。これは、必ずループ処理が進めば i10 以上になり、終了条件が成立するからです。

終了条件に移行しない例

i = 0;
while (i < 10) {
    printf("%d\n", i);
    i++;
}

そもそもの話ですが、上記のようなループは for ループで実現した方が良いと思います。処理の記載漏れによる無限ループも防ぎやすく、読みやすくもなりますので。

この辺りは下記ページでも解説していますので興味があれば読んでみてください。

forとwhileの使い分けの解説ページアイキャッチfor と while の使い分け

スポンサーリンク

型と条件の不整合が発生している

先ほどは無限ループになってしまう原因が一目瞭然でしたが、次の例はいかがでしょうか?

私は何回か同様のことをやらかして意図しない無限ループに陥ってしまったことがあります。

-1にならなくて無限ループ

for (unsigned int i = 9; i >= 0; i--) {
    printf("%u\n", i);
}

ここで継続条件の i >= 0 にだけ注目していると、実は無限ループが発生する原因には気づきにくいのではないかと思います。

i90 の間はループが継続され、次に i-- が実行された際にループが終了すると考えてしまうと大間違いです。

今回の場合は継続条件が i >= 0 なので、終了条件は i < 0 となります。では i < 0 が成立することはあり得るでしょうか?

あり得ませんね!

i の型は unsigned int ですので、負の値として扱われることはありません。なので、終了条件 i < 0 が成立することはなく、無限ループとなってしまいます。

ループが終了するのはあくまでも「終了条件が成立した時」です。継続条件が i >= 0 だからといって、プログラムが i0 の時の次のループで気を利かせて終了してくれるようなことは無いので注意してください。プログラムはあなたが書いた通りに動作します。

おそらくですが、今回の例に関しては、継続条件 i >= 0 ではなく終了条件 i < 0 が本当に成立するのかどうかを考えた方が、無限ループが発生する原因に辿り着きやすいのではないかと思います。

先程の例は変数 i の型が符号なしであることが問題でしたので、下記のように変数 i の型を符号ありにして負の値も扱えるようにすることで、無限ループを回避することができます。

-1になるようにして無限ループを解決

for (int i = 9; i >= 0; i--) {
    printf("%u\n", i);
}

では、次の例はいかがでしょうか?

256にならなくて無限ループ

for (unsigned char i = 0; i <= 255; i++) {
    printf("%u\n", i);
}

この場合も基本的に無限ループになると思います。

無限ループになる原因は、先程同様に終了条件が成立するかどうかを考えれば、すぐに分かるのではないかと思います。

上記の継続条件は i <= 255 であり、終了条件は i > 255 となります。その一方で、unsigned char が取りうる最大値は 255 ですので、i256 になることはあり得ません。

つまり、終了条件が成立することはないので、無限ループとなってしまいます。

上記に関しては、変数 i の型を次のように unsigned int などの 255 よりも大きな値が扱える型にしてやれば、いずれは i256 になって終了条件が満たされるようになるため、無限ループを解決することができます。

256になるようにして無限ループを解決

for (unsigned int i = 0; i <= 255; i++) {
    printf("%u\n", i);
}

ここまで説明してきた通り、終了条件に利用する値や変数の型も考慮しながらループを記述しないと無限ループになることもあり得ますので注意してください。

また、先程の無限ループが発生する例において、下記のように記述した場合も同様に結局は無限ループが発生するのですが、コンパイル時に警告が発生するようになります

256にならなくて無限ループ2

for (unsigned char i = 0; i < 256; i++) {
    printf("%u\n", i);
}

この例のように、コンパイル時の警告より、プログラムを実行する前に「ループが上手く動作しないこと」や「計算がうまく実行できないこと」を検知できることもあります。是非、警告にもアンテナを張り巡らしながらプログラミングするように心がけましょう!

誤差が発生して終了条件が成立しない

浮動小数点数の誤差に関しても無限ループが発生する原因となる場合があります。

例えば下記は、浮動小数点数の誤差が原因で無限ループに陥ってしまう例になります。

誤差のせいで無限ループになる例

double x = 0;
while (1) {
    if (x == 1) {
        break;
    }
    printf("%f\n", x);
    x += 0.1;
}

この場合は、終了条件が x == 1 となります。

初期値 0 の変数 x をループの中で 0.1 ずつ増加させていっているので、10回繰り返しが行われれば x1 になって終了条件 x == 1 が成立しそうに思えます。ただし、コンピュータで扱う実数(浮動小数点数)には誤差が生じるので、上記の処理で x が丁度 1 になるようなことはありません。

さらに厄介なのが、この誤差が発生していることが単純に printf するだけだと気づきにくい点です。例えば上記を実行した場合、printf での表示結果は下記のようになります。

0.000000
0.100000
0.200000
0.300000
0.400000
0.500000
0.600000
0.700000
0.800000
0.900000
1.000000
1.100000
1.200000
1.300000
1.400000
1.500000
〜以下略〜

この結果だけ見ると、x が丁度 11.000000) になっているのに終了条件 x == 1 が成立してくれないので、プログラムに問題はないけどパソコン側がバグってるようにも思えてしまいますね…。

ただ、上記において x が丁度 1 になっているように思えるのは、printf で表示される小数点以下の値の桁数が少ないからです。実際には x には誤差が含まれおり、丁度 1 にはなっていません。

なので、終了条件 x == 1 が成立しないのは、もちろん意図した動作ではないものの、プログラム通りの動作になっているということができます。

ちなみに、下記ページで解説しているように、printf で表示する小数点以下の値の桁数を多くしたりデバッガーを利用したりすることで、変数の値に誤差が含まれていることを簡単に確認できるようになります。

これにより、浮動小数点数の誤差が原因のバグにも気づきやすくなります(今回のような無限ループなど)。

printfで小数点以下の桁をたくさん表示する方法の解説ページアイキャッチ【C言語】printfで小数点以下の桁をたくさん表示する方法

ちょっと話は逸れましたが、浮動小数点数の比較を行う場合は誤差に特に注意が必要です。誤差が発生することを考慮してプログラミングしないと、今回のように終了条件が成立せずに無限ループに陥ってしまうことがあります。

例えば、x1  になった時を終了条件にしたいのであれば、x1 - 許容する誤差1 + 許容する誤差 の値になった時を終了条件とすれば良いです。例えば下記は許容する誤差を 0.000001 とした時の修正例であり、少なくとも私の環境で無限ループを回避できることを確認しています。

誤差を考慮して無限ループを解決

double x = 0;
while (1) {
    if (1 - 0.000001 <= x && x <= 1 + 0.000001) {
        break;
    }
    printf("%f\n", x);
    x += 0.1;
}

この 0.000001(許容する誤差)の部分に関しては、float.h をインクルードして DBL_EPSILON などに置き換えることで厳密な比較が行えるようになりますし、許容する誤差として選んだ値にも根拠が示せるのでオススメです。

いずれにしても浮動小数点数は誤差があるので扱いが難しいです…。特に比較などを行う場合は、浮動小数点数ではなく整数で比較できないかどうかを検討してみるのが良いと思います。

例えば上記の例であれば、10 回繰り返すことが目的なのであれば整数型の変数を導入し、その整数を用いた終了条件とすることで、とりあえず誤差のせいで無限ループになるようなことは簡単に防ぐことができます。

整数を扱って無限ループを解決

int i = 0;
double x = 0;
while (i < 10) {
    printf("%f\n", x);
    x += 0.1;
    i++;
}

メモリ破壊が発生している

また、メモリ破壊(意図しないメモリの書き換え)が発生しているような場合も無限ループに陥る可能性があるので注意してください。

例えば下記では無限ループが発生する可能性があります。実際に、私の環境だと最適化 OFF でコンパイルすれば無限ループに陥ってプログラムが終了しなくなりました。

メモリ破壊が発生して無限ループ

#include <stdio.h>

int i;
int data[10];

int main(void) {

    for (i = 0; i <= 10; i++) {
        data[i] = 10 - i;
    }

    /* 以下略 */
}

MEMO

ちなみに、このソースコードは下記ページを参考にさせていただいています

現象が面白かったので、自身の環境で同様の現象が発生するソースコードを用意し、それを紹介させていただいています

https://dixq.net/forum/viewtopic.php?t=15033

さて、上記のソースコードでは何が原因で無限ループが発生しているのでしょうか…?

i <= 10 が明らかにおかしいことには気づくと思いますが、それがなぜ無限ループに繋がるのでしょうか?

この無限ループが発生する理由は、各変数がメモリ上で配置されている位置を考えると気付きやすいのではないかと思います。

私の環境で実際に無限ループが発生した際には、変数 i と 変数 data の先頭アドレスが下記のようになっていました(問題点が分かりやすいように10進数表記しています)。

  • i の先頭アドレス:4295000120
  • data の先頭アドレス:4295000080

2つの変数のアドレスの差は 40 バイトになります。さらに、私の環境では int 型のサイズは 4 バイトです。

つまり、上記のアドレスから、変数 i と 変数 data は次の図のようにメモリ上に配置されていることが分かります(data[9] の隣に変数 i が配置されている)。

変数dataと変数iのメモリは位置を示す図

そして、上記のプログラムでは、ループの継続条件が i <= 10 ですので、i10 の場合も data[i] = 10 - i が実行され、この際には data[10] = 0 が実行されることになります。

もちろん、配列 data の要素数は 10 なので data[10] は配列外へのアクセスになるのですが、data[10] に相当する位置に丁度変数 i が配置されているため、data[10] = 0 を実行する際に変数 i0 が格納されることになります。

data[10]=0によりiの値が書き換えられてしまう様子

つまり、ここで i の値が 10 から 0 に変わってしまいます。そうなると、当然ループの継続条件が i <= 10 が成立してループがまた実行されることになります。さらに、再度 i の値が 10 になった際には同様に再び i の値が 0 に書き換えられてしまい、これが延々と続いて無限ループになるというわけです。

今回はループの継続条件 i <= 10 と配列 data の要素数の食い違いによって発生する問題ですので、食い違いのないように修正、例えば継続条件を i < 10 に変更すればメモリ破壊を防ぎ、無限ループも回避することができます。

ここで知っておいていただきたいのは、こういった意図しないメモリの書き換え(メモリ破壊)によって無限ループが発生しうるということです。

もちろん無限ループだけでなく、メモリ破壊が原因でプログラムが意図した通りに動作してくれなかったり、プログラムの動作が不安定になってしまうようなこともあります。ですので、今回のような配列外へのアクセスによって発生するメモリ破壊には十分注意してください。

特にC言語ではメモリ破壊が発生しやすいので注意が必要ですし、上記のソースコードのように処理が単純だと原因はすぐに分かりますが、複数の処理が同時に並行して動作するようなプログラムだと、どの処理がメモリを破壊しているかを調査するのが大変です。

スポンサーリンク

処理が重すぎる

これは本当は無限ループではないのですが、ループの回数が多すぎる場合やループの処理が重すぎる場合、無限ループに陥ってしまったと勘違いするようなこともあります。

例えば下記のようにループの回数が大きすぎる&ループの中でそれなりに重い処理(sincos 関数の実行など)を行うと、いつまで経ってもループが終了せずに無限ループに陥っているかのように勘違いしてしまうことがあります。

処理が重すぎて無限ループと勘違い

#include <stdio.h>
#include <math.h>

#define N 1000000000

double sin_table[N];
double cos_table[N];

int main(void) {

    for (int i = 0; i < N; i++) {

        sin_table[i] = sin(i * 2 * M_PI / N);
        cos_table[i] = cos(i * 2 * M_PI / N);
    }

    /* 以下略 */
}

上記は結構極端な例ではありますが、再帰処理などで複雑な問題を解くような場合、無限ループに陥ったのではないかというくらいプログラムの処理時間が長くなってしまうようなことは結構あります。プログラムが止まってしまっているのではないかと不安になることもありますね…。

正常にプログラムが動作しているかどうかに関しては、下記のように適宜進捗状況を printf で表示することで確認することもできます。

処理の進捗の確認

for (int i = 0; i < N; i++) {
    if (i % 100000 == 0) {
        printf("%d %%\n", (int)((double)i / (double)N * 100));
    }
    sin_table[i] = sin(i * 2 * M_PI / N);
    cos_table[i] = cos(i * 2 * M_PI / N);
}

進捗状況を確認できるようにしておくと、単に処理に時間がかかっているだけなのか?プログラムがバグっているのか?が切り分けしやすくなります。

ただし、それなりに繰り返し回数の多いループの中で毎回 printf を実行するのはやめた方が良いです。printf は大量に実行すると凄く負荷が高くなるので、printfのせいでループ処理が終わらなくなってしまうようなこともあり得ます。printf を使って進捗を確認するような場合は、上記のようにある程度実行回数を間引いて printf するようにした方が良いです(上記では 100000 回に1回だけ )。

また、下記ページで紹介しているようなデバッガーを導入すれば、好きな位置やタイミングでプログラムを停止させ、その時点の変数の中身を表示したりすることもできますので、これによって進捗状況を確認するようなこともできます。

デバッガーはとにかく便利なので、プログラミングするのであれば導入しておくことをオススメします。

VSCodeでMacOSにC言語デバッグ環境を構築する方法の解説ページアイキャッチVSCodeでMacOSにC言語デバッグ環境を構築

まとめ

このページでは、C言語で無限ループが発生する原因の解説および、実際に無限ループが発生する例の紹介を行いました!

無限ループはさまざまな要因で発生しますが、無限ループの根本的な原因は終了条件が成立しない点にあります。

なので、無限ループの原因を調査する際は、まずは継続条件ではなく終了条件を考え、本当にその終了条件が満たされるように処理が行われるのかについて確認するのが良いと思います。

特にC言語やプログラミングに慣れていないと、無限ループが発生した際に型や誤差が原因であることに気づきにくいと思いますので、型の重要性(型で扱える値の範囲)や浮動小数点数を扱う難しさ(誤差が発生する)についてはあらかじめ頭の片隅にでも置いておくと良いと思います。

もちろん今回紹介したようにメモリ破壊が発生することで無限ループに陥ることもあり得るのですが、可能性としては低い方なのではないかと思います。なので、まずは終了条件が満たされるように処理が記述されているかどうかを確認するのが良いと思います(メモリ破壊以外の全ての可能性を確認したら、最後にメモリ破壊を疑う感じですかね…)。

オススメの参考書

C言語学習中だけど分からないことが多くて挫折しそう...という方には、下記の「スッキリわかるC言語入門」がオススメです!

まず学習を進める上で、参考書は2冊持っておくことをオススメします。この理由は下記の2つです。

  • 参考書によって、解説の仕方は異なる
  • 読み手によって、理解しやすい解説の仕方は異なる

ある人の説明聞いても理解できなかったけど、他の人からちょっと違った観点での説明を聞いて「あー、そういうことね!」って簡単に理解できた経験をお持ちの方も多いのではないでしょうか?

それと同じで、1冊の参考書を読んで理解できない事も、他の参考書とは異なる内容の解説を読むことで理解できる可能性があります。

なので、参考書は2冊持っておいた方が学習時に挫折しにくいというのが私の考えです。

特に上記の「スッキリわかるC言語入門」は、他の参考書とは違った切り口での解説が豊富で、他の参考書で理解できなかった内容に対して違った観点での解説を読むことができ、オススメです。題名の通り「なぜそうなるのか?」がスッキリ理解できるような解説内容にもなっており、C言語入門書としてもかなり分かりやすい参考書だと思います。

もちろんネット等でも色んな観点からの解説を読むことが出来ますので、分からない点は別の人・別の参考書の解説を読んで解決していきましょう!もちろん私のサイトも参考にしていただけると嬉しいです!

入門用のオススメ参考書は下記ページでも紹介していますので、こちらも是非参考にしていただければと思います。

https://daeudaeu.com/c_reference_book/

同じカテゴリのページ一覧を表示