【C言語】scanf利用時に発生する無限ループの対策方法

scanf利用時に発生する無限ループの対策方法解説ページアイキャッチ

このページにはプロモーションが含まれています

このページでは、C言語における scanf 利用時に発生する無限ループの対策方法について解説していきます。

今回対策する「scanf 利用時に発生する無限ループ」とは、下記のようなループにおいて、scanf での「整数」の入力受付時にアルファベット等を含む「文字列」を入力することで発生する無限ループになります。

無限ループが発生する例
int x = 0;
while (1) {

    printf("1〜5の値を入力してください: ");
    scanf("%d", &x);

    if (x >= 1 && x <= 5) {
        printf("入力値:%d\n", x);
        break;
    } else {
        printf("入力された値がおかしいです...\n");
    }
}

上記の while ループでは、scanf でユーザーから整数の入力を受け付け、その入力値が期待するものでない場合に(上記の場合は 1 から 5 の整数でない場合に)、再度 scanf を実行してユーザーからの入力を受け付けるような処理を行なっています。また、ユーザーからの入力値が期待する整数である場合は、入力値を出力して終了するループとなっています。

実は、この while ループは整数が入力された場合は上手く動作してくれるのですが、整数以外、例えばアルファベットなどが入力されてしまうと以降の入力受付が行われなくなり、下記のように無限ループに陥ってしまいます。こうなると、ctrl + c 等の操作で強制終了しないとプログラムが終了しないという状況になります。

1〜5の値を入力してください: abc
入力された値がおかしいです...
1〜5の値を入力してください: 入力された値がおかしいです...
1〜5の値を入力してください: 入力された値がおかしいです...
1〜5の値を入力してください: 入力された値がおかしいです...
1〜5の値を入力してください: 入力された値がおかしいです...
〜以下略〜

こんな感じで、scanf 関数で整数の入力受付を行い、さらに scanf 関数で読み込んだデータが不適切な際に再度 scanf 関数を実行するような構造のループでは、整数以外が入力された際に無限ループが発生してしまいます。

文字列入力をしてしまってプログラムが無限ループする様子

この無限ループの発生は、プログラムによってはユーザーの操作性を著しく低下させる可能性があります。

例えば、プログラム開始時点で1度だけユーザーから整数の入力を受け付けるようなものであれば、間違って整数以外を入力して無限ループが発生したとしても、ユーザーはプログラムを再起動して再度入力を行えば良いだけです。

それに対し、何度もユーザーからの整数の入力を受け付けるような場合、途中でユーザーが間違って無限ループが発生してしまうと、そこまで入力した作業が台無しになってしまいます。

この場合、ユーザーはプログラムの再起動後、今まで行ってきた入力作業を再度行う必要があり非常に不便です。

こういったプログラムの場合、無限ループの対策を行い、間違った入力が行われた際には再度入力受付が行えるようにしてあげた方が便利です。

文字列入力をしてしまった場合でも無限ループが行われずに再度入力受付を行う様子

このページでは、間違って文字列が入力されてしまった場合にも再度入力受付が行えるよう、上記のような状況で発生する無限ループの対策方法について解説していきたいと思います。

無限ループに陥る原因

対策の説明を行う前に、まずは無限ループに陥ってしまう原因について簡単に説明しておきます。

すぐに対策を知りたいという方は 全て文字列入力にすることで無限ループを防ぐ までスキップしていただければと思います。

scanf 関数の動作

無限ループに陥ってしまう原因は、scanf 関数の動作を知ることで簡単に理解することができると思います。

ということで scanf 関数の動作について説明していきます。

scanf 関数

ご存知の通り、scanf 関数は標準入力からの入力を受け付ける関数になります。標準入力はキーボードに接続されていることが多いので、今回は scanf 関数がキーボードから入力受付を行うことを前提に解説していきます。

標準入力や標準出力については下記ページで解説していますので、詳しく知りたい方は下記ページをご参照いただければと思います。

標準出力・標準入力・標準エラーの解説ページアイキャッチ 【C言語】標準出力・標準入力・標準エラー出力とは?(stdout・stdin・stderr)

また、標準入力から入力されたデータは一旦バッファというメモリに格納されます。そして、このバッファから scanf 関数はデータを読み込むことになります。

scanfでバッファからデータが読み込みされる様子

続いて scanf 関数の引数についておさらいしておくと、まず第1引数では入力を受け付けるデータのフォーマットを指定することになります。例えば整数を読み込みたいのであれば  %d を、文字列を読み込みたいのであれば %s を指定しますよね!

今回対策を行うのは整数の入力受付を行なった際に発生する無限ループですので、本章では、第1引数に %d が指定されていることを前提に解説を進めていきます。

また、第2引数では入力されたデータの格納先のアドレスを指定します。例えば入力された整数を int 型の変数 x に格納したいのであれば &x を指定します。

scanf 関数の読み込み動作

ここまでが、scanf の動作を理解するための前提知識となります。続いて本題の scanf 関数の動作について考えていきましょう!

まず、先ほども説明したように、標準入力から入力されたデータはバッファに格納されます。

そして、scanf 関数が実行された際には、scanf 関数はそのバッファをチェックし、バッファに「まだ読み込んでいないデータ」が存在するかどうかを判断します。

もし、存在しないのであれば、scanf 関数はバッファにデータが格納されるまで入力待ちを行います。

scanfがバッファへの入力待ちを行う様子

そして、標準入力から入力が行われると「まだ読み込んでいないデータ」がバッファに格納されることになりますので、その際にはバッファから「まだ読み込んでいないデータ」を読み込み、読み込んだデータを第2引数で指定されたアドレスに格納します。

scanfがバッファに入力があった際にまだ読み込んでいないデータを読み込む様子

ただし、この際に読み込まれるデータは「第1引数で指定したフォーマットに合致するデータのみ」となります。

つまり、第1引数に %d が指定されている場合、整数以外は読み込みが行われないということになります。

例えば 123abc という入力された場合、最初の 123 のみが読み込まれることになります。

バッファから整数のみが読み込みされるデータ

つまり、バッファには abc が「まだ読み込んでいないデータ」として残ってしまうことになります。

続いて、再度 scanf 関数が実行された際の動作について考えていきましょう。

先ほども説明したように、scanf 関数は実行されるとバッファをチェックします。

もしバッファに「まだ読み込んでいないデータ」が存在しないのであれば入力待ちを行いますが、今回は先ほど入力された  abc が「まだ読み込んでいないデータ」として残っているため、scanf 関数は入力待ちを行わずに即座にバッファからの読み込みを行います。

バッファにまだ読み込んでいないデータがあるので入力待ちが行われない様子

ですが、abc は整数ではないため、実際には何も読み込まずに処理を終了することになります。

バッファに整数が残っていないので何も読み込みされない様子

つまり、この scanf 関数の実行を行なったとしても、1回目の scanf 関数実行後と比べてバッファの状態は同じになります。すなわち、バッファには abc が「まだ読み込んでいないデータ」として残っています。

スポンサーリンク

無限ループの原因

次回 scanf 関数が実行された場合の動作も同様で、バッファには「まだ読み込んでいないデータ」が残っているので scanf は入力待ちを行わずにバッファからデータを読み込もうとします。ですが、第1引数で指定されたフォーマットに合致するデータが存在しないので結局何も読み込みされず、バッファには abc が「まだ読み込んでいないデータ」として残ることになります。

以降も同様で、scanf 関数が何度実行されてもバッファに「まだ読み込んでいないデータ」として入力受付を行なっていないデータ(文字列 abc)が残り続けることになります。

そのため、ページの先頭で紹介したソースコードのような場合は無限ループに陥ることになってしまいます。

バッファにデータが残り続けることで無限ループになってしまう様子

ということで、ページの先頭で紹介したようなソースコードで無限ループに陥る原因は、scanf 関数で入力受付していないデータ(scanf 関数の第1引数で指定したフォーマットに合致しないデータ)がバッファに残り続けてしまうことが原因であると言えます。これによって scanf 関数での入力待ちが行われず、無限にループしてしまうことになります。

無限ループの対策

無限ループになってしまう原因は「scanf 関数で入力受付していないデータがバッファに残り続けてしまうこと」ですので、これを解消することで無限ループの対策を行うことができます。

まずここでは、この無限ループの対策方法の考え方についていくつか簡単に説明しておきたいと思います。

詳細な手順やソースコードについては 全て文字列入力にすることで無限ループを防ぐ 以降で解説していきたいと思います。

scanf 関数で文字列の入力受付を行う

まず、scanf 関数で文字列の入力受付を行うようにしてやることで無限ループの対策を行うことができます。

つまり、整数に制限して入力受付するのではなく、文字列の入力受付を行います。

こうすることで、scanf 関数からアルファベットや記号などの読み込みも行わるようになりますし整数も文字として読み込みされるようになります。

基本的にキーボードから入力されるデータは文字のみなので入力受付していないデータそのものが存在しなくなることになり、scanf 関数で入力受付していないデータがバッファに残り続けることを防ぐことができます。

これにより、先程の例でいえば、123abc が入力された際には 123abc 全てが読み込みされることになり、scanf 関数実行後の「まだ読み込んでいないデータ」を基本的に無くすことができます。

バッファのデータが文字列読み込みによって全て読み込まれる様子

ただし、scanf 関数では空白文字(スペースやタブ)の扱いがちょっと特殊なので注意してください。

scanf 関数において、空白文字は入力データの区切りとして扱われます。そのため、文字列で入力受付をしたとしても、scanf 関数で一度に読み込みが行われるのは空白文字までとなります。

例えば標準入力に対して abc def という入力をした場合、1回目の scanf 関数実行時には abc が読み込まれますが、 def はバッファに残ることになります。

スペースの直前の文字までしか一度に読み込まれずに、バッファにデータが残ってしまう様子

そのため、2回目の scanf 関数実行時には入力待ちは行われず、バッファに残った def の読み込みが行われることになります(最初のスペースは飛ばされる)。

2回目のscanf実行時にバッファに残っているデータが自動的に読み込みされてしまう様子

このように、文字列入力を行うことで無限ループを防ぐことはできるのですが、空白文字が存在する場合は入力データの区切りとして扱われる点に注意してください。

実は scanf 関数の第1引数を工夫すれば空白文字も含めて一度に読み込みを行うこともできるのですが、それを行うのであれば次に紹介する fgets 関数を利用する方が楽だと思います。

fgets 関数で文字列の入力受付を行う

もう1つの方法は scanf 関数を使うのではなく fgets 関数を利用する方法になります。

fgets については下記ページで詳しく説明していますので、詳細は下記ページを参考にしていただければと思います。

fgets関数の解説ページアイキャッチ 【C言語】fgets 関数について解説(テキストファイルの読み込み)

fgets はファイルからの読み込みを行う関数なのですが、ファイルを指定する引数に stdin を指定することで標準入力からの読み込みを行うことができるようになります。そしてこれによって、scanf 関数同様に標準入力からの入力を受け付けることができるようになります。

fgetsからの標準入力の利用
char str[256];

// 標準入力からの入力の読み込み
fgets(str, 256, stdin);

実は、fgets においても scanf 同様に「まだ読み込んでいないデータ」がバッファに残っている場合は入力待ちを行わずにバッファのデータの読み込みを行う特性があります。ですが、fgets ではデフォルトで文字列入力を受け付けるようになっているため、scanf 関数と違って無限ループが起こらないようになっています。

また、scanf 関数とは異なり、fgets 関数では改行までの文字を “空白文字も含めて” 一度に読み込まれるため、基本的には一度で全てのバッファのデータを読み込むことができます(改行はエンターキーを押した際にバッファに入力されることになります)。

fgets関数がスペースを含めて読み込みを行う様子

ただし、fgets 関数の第2引数で指定するサイズを超える文字列が入力された場合はバッファにデータが残ることになります。

また、scanf 関数と異なり、読み込んだ文字列の最後に改行文字 '\n' が含まれる点も注意が必要です。

読み飛ばしを行なう

3つ目の方法として、scanf 関数でバッファの入力受付を行なった後にバッファに残った「まだ読み込んでいないデータ」を全て読み込んでやる方法が挙げられます。

バッファに残った「まだ読み込んでいないデータ」を全て読み込んでやれば、「まだ読み込んでいないデータ」は存在しなくなります。

そのため、次回 scanf 関数が実行された際には、バッファに「まだ読み込んでいないデータ」が存在しないため入力待ちが行われ、標準入力から新たな入力を受け付けることができるようになります。

先程の例でいえば、123abc が入力された際には 123 の整数のみの読み込みを行なった後、abcの読み込みを行うことで「まだ読み込んでいないデータ」を無くすことになります。

通常の読み込みと読み飛ばしを行うことでバッファに「まだ読み込んでいないデータ」が存在しなくなる様子

ただし、今回は整数入力を行うプログラムを前提としているため abc の部分は不要なデータあり、読み込んだ abc は配列等には格納せずに捨てます。そのため「読み飛ばし」と表現させていただいています。

詳細は後述で解説しますが、結局この読み飛ばしも scanf 関数によって実現することができます。

スポンサーリンク

文字列入力にすることで無限ループを防ぐ

ここからは、ページの冒頭で紹介したようなソースコードで発生する無限ループの対策例をソースコードや具体的な手順・注意点を踏まえながら詳細に解説していきたいと思います。

まず、scanf 関数で文字列の入力受付を行う で説明した考え方に基づき、整数の入力受付を行う際にも scanf 関数での入力受付を文字列に対して行うようにすることで無限ループの対策を行なっていきます。

文字列入力で無限ループ対策を行うサンプル

最初に本章の結論として文字列入力で無限ループ対策を行うサンプルを紹介しておきます。

そのソースコード例が下記となります。

文字列入力で無限ループ対策
#include <stdio.h>
#include <stdlib.h>

int main(void) {

    long x = 0;
    char str[256];
    char *endptr = NULL;
    while (1) {
        
        printf("1〜5の値を入力してください: ");
        scanf("%255s", str);

        x = strtol(str, &endptr, 10);
        if (*endptr != '\0') {
            printf("入力された値がおかしいです...\n");
            continue;
        }

        if (x >= 1 && x <= 5) {
            printf("入力値:%ld\n", x);
            break;
        } else {
            printf("入力された値がおかしいです...\n");
        }
    }
}

strtol 関数の返却値の型に合わせて x の型を int 型から long 型に変更しているので注意してください。

文字列入力で整数入力を実現する手順

文字列入力を行うことで無限ループ自体は対策できるのですが、この方法の場合、読み込まれるデータが文字列である点に注意する必要があります。

まず、ユーザーからの入力受付を行なっているのは scanf("%255s", str) の部分になります。

255 の意味合いについては後述で別途解説しますが、"%s"scanf の第1引数に指定するため文字列入力の受付を実現することができます。

ですが、読み込まれて scanf の第2引数のアドレスに格納されるデータは文字列となります。

それに対し、実際にプログラムで扱いたいデータは整数ですので、文字列から整数に変換する必要があります。

そのため、scanf 関数の実行後に atoi 関数や strtol 関数等で文字列から整数への変換を行う必要があります。

atoi 関数や strtol 関数の使い方については下記ページで解説していますので、詳しく知りたい方はこちらを参照していただければと思います。

文字列の数値への変化方法の解説ページアイキャッチ 【C言語】文字列を数値に変換する方法(atoi・strtol など)

今回は strtol 関数を利用して文字列から整数への変換を行いたいと思います。上記ページでも解説していますが、strtol 関数ではエラーチェックが行えるというメリットがあります(その分使い方は難しいですが…)。

例えば atoi 関数の場合、返却値が 0 の場合に入力された文字列が "0" であったか、アルファベット等の整数以外であったかの判別ができませんが、strtol 関数を利用すればこれらの判別を行うことができます。

上記を踏まえ、さらに必要になる変数についても考慮すると、文字列入力で整数入力を実現するためには下記のような手順を踏む必要があります。

  1. 整数を格納するための変数を宣言する
  2. 文字列を格納するための変数を宣言する
  3. scanf で文字列入力の受付を行う
    • (入力された文字列は 2. で宣言した変数に格納する)
  4. scanf で読み込まれた文字列を strtol 関数で整数に変換する
    • (変換後の整数は 1. で宣言した変数に格納する)
  5. strtol 関数のエラーチェックを行う
    • エラーが発生している場合は 3. に戻る
  6. 変換後の整数を利用して必要な処理を実行する

文字列入力で無限ループ対策を行うサンプル で示したソースコードにおいても上記のような流れで処理を行なっていることが確認できると思います。

スポンサーリンク

バッファオーバーランに注意!

特にこの方法において注意が必要なのはバッファオーバーランになります。

バッファオーバーランの危険性

バッファオーバーランとは、簡単に言えば、配列のサイズよりも大きなデータを配列に格納することになります。これによって、配列の領域外のメモリにデータが書き込まれることになり、大事なデータが破壊されてしまう可能性があります。

バッファオーバーランの例

このバッファオーバーランを利用して、例えば悪意あるユーザーによってパスワードを書き換えられてしまうようなケースもあり得ますので、バッファオーバーランが発生しないように注意が必要になります。

特に、scanf ではこのバッファオーバーランが発生しやすいので注意してください。

例えば下記のように scanf を実行した場合、str の配列サイズは 5 ですので、5 - 1 文字を超える文字列が入力された際にバッファオーバーランが発生することになります(文字列の最後にはヌル文字が格納される)。

バッファオーバーランが発生する例
char str[5];
scanf("%s", str);

scanf では配列のサイズを考慮することなく、単にバッファから読み込んだデータを第2引数で指定されたアドレスから順に格納(書き込み)していくだけですので、平気で配列のサイズ以上のデータの書き込みが行われてバッファオーバーランが発生します。

バッファオーバーランの対策

このバッファオーバーランは、バッファから一度に読み込むデータのサイズを指定することで防ぐことができます。そして、このサイズは scanf の第1引数で指定することができます。

例えば scanf の第1引数に単に "%s" を指定した場合、基本的に標準入力から入力された文字列が全て読み込まれることになりますが、scanf の第1引数に "%4s" のように指定を行うことにより、一度に読み込まれる文字列を 4 文字に制限することができます。

これを利用すれば、配列のサイズを考慮して文字列の読み込みを行うことができるようになるため、バッファオーバーランを防ぐことができます。

具体的には、scanf の第1引数に "%Ns" を指定します。この N には用意した配列に対して 配列のサイズ - 1 以下の値を指定します。配列のサイズが 5 であれば、N には 4 を指定することになりますので、scanf の第1引数に例えば "%4s" を指定することになります。

バッファオーバーランの対策例
char str[5];
scanf("%4s", str);

N に指定するのが 配列のサイズ ではなく 配列のサイズ - 1 なのは、scanf では読み込んだ文字列の最後にヌル文字('\0')が格納されるようになっているためです。

そのため、N配列のサイズ を指定すると配列の領域外にヌル文字が格納される可能性があるので注意してください。

文字列入力で無限ループ対策を行うサンプル で紹介したソースコードにおいては、配列のサイズ256としているので、scanf 関数の第1引数には "%255s" を指定しています。

MEMO

scanf_s などの読み込みサイズを指定可能な関数を使用できるのであれば、scanf ではなく scanf_s を使用することでバッファオーバーラン対策を行うことができます

一度に読み込めないケースが存在するので注意

先程の解説を踏まえて実装すれば、一番注意すべきバッファオーバーランを防ぐことは可能です。

ですが、一度に全ての文字列の読み込みができなくなる可能性があるので注意してください。

例えば、scanf の第1引数に "%4s" を指定した場合、5 文字以上の入力が行われた際には 5 文字目以降の文字列がバッファに「まだ読み込んでいないデータ」として残ることになります。

一度にデータが読み込めずにバッファにデータが残ってしまう様子

つまり、2回目の scanf では入力待ちが行われずにバッファに残っているデータの読み込みが行われることになります。無限ループにはならないですが、ちょっとプログラムがおかしな挙動をしてしまうことになります。

これを完全に防ぐことは難しいですが、用意する配列のサイズを十分に大きくしておくことで大概のケースで本現象を防ぐことはできると思います。

例えば、ページの冒頭で紹介したソースコードでは 15 の整数の入力を期待しているのに対し、配列のサイズを 256、つまり scanf の第1引数に "%255s" を指定しておけば、256 文字以上の入力が行われない限り本現象を防ぐことができます。

printf によって “1 文字の入力(15 の入力)を行なって欲しい” と表示しているのにも関わらず、間違って 256 文字以上の入力を行うユーザーはまずいませんので、間違って入力が行われた際の対策としては十分だと思います。

ですが、悪意あるユーザーによって 256 文字以上の入力が行われる可能性は当然ありますので、その点は頭に入れておくと良いと思います。例えば実際の製品開発においては、悪意あるユーザーが存在することも考慮して仕様を決めたりプログラミングを行う必要があります。

ただ、おそらく実際の製品開発時には、ユーザーからの指示入力にC言語の scanf を使う機会はまず無いと思うんですよね…。

なので、特にC言語の勉強やお試しのゲーム開発などで scanf を使う方は、ユーザーが間違って入力を行なった際の対応だけ入れておくので十分だと思います。ただ、scanf の危険性についてはしっかり覚えておいた方が良いです(バッファオーバーランが発生しやすい)。

また、標準入力に入力された文字列の中に空白文字(スペースやタブ)が存在する場合、単純に scanf を実行すると一度に空白文字までしか読み込みが行われないので注意してください。

文字列入力で無限ループ対策を行うサンプルの動作確認

ここまで解説してきた内容を踏まえて、文字列入力で無限ループ対策を行うサンプル で紹介したソースコードの動作確認を行なってみましょう!

ソースコードをコンパイルして実行すれば入力受付が行われるのでアルファベットなどを入力してエンターキーを押してみてください。無限ループが発生することなく、正常に入力の再受付が行われるはずです。

私の方で色々入力を行なってみた結果が下記となります。文字列入力にすることで無限ループを防ぐことができ、また 15 の整数が入力された際には正常に入力値の表示が行われてプログラムが終了することも確認できると思います。

1〜5の値を入力してください: 100
入力された値がおかしいです...
1〜5の値を入力してください: abc
入力された値がおかしいです...
1〜5の値を入力してください: 10abc
入力された値がおかしいです...
1〜5の値を入力してください: abc10
入力された値がおかしいです...
1〜5の値を入力してください: abc def
入力された値がおかしいです...
1〜5の値を入力してください: 入力された値がおかしいです...
1〜5の値を入力してください: 3
入力値:3

ただし、上記の abc def 入力時のように、途中にスペースが存在する文字列を入力した場合は次回の scanf での入力待ちが行われずに自動的に def の読み込みが行われることになります。

また、文字列入力で無限ループ対策を行うサンプル における scanf("%255s", str)scanf("%4s", str) に変更して実行すれば、5 文字以上の文字列を入力した際に 5 文字目以降の文字列は次回以降の scanf 関数で読み込まれ、その際の入力待ちも行われないことも確認できると思います。

例えば下記の場合は 10 文字入力しているので、1 〜 4 文字目が1回目の scanf 関数実行で、5 〜 8 文字目が2回目の scanf 関数実行で、9 〜 10 文字目が3回目の scanf 関数実行で読み込まれることになります。そのため2回分の scanf 関数での入力待ちがスキップされることになります。

1〜5の値を入力してください: 1234567890
入力された値がおかしいです...
1〜5の値を入力してください: 入力された値がおかしいです...
1〜5の値を入力してください: 入力された値がおかしいです...
1〜5の値を入力してください: 4
入力値:4

それでも無限ループは防ぐことができてはいるのですが、使い勝手を考えると出来るだけ多くの文字列を一度に読み込みできるよう、読み込んだ文字列を格納する配列を大きくし、さらに、それに応じて scanf で読み込む文字数を大きくしてあげた方が無難だと思います。

fgets 関数を利用することで無限ループを防ぐ

続いて、scanf 関数の代わりに fgets 関数を使用することで無限ループを対策する方法について説明していきます。

スポンサーリンク

fgets 関数で無限ループ対策を行うサンプル

まずは、本章の結論として fgets 関数を使用して無限ループ対策を行うサンプルを紹介しておきます。

そのソースコード例が下記となります。

fgets 関数で無限ループ対策
#include <stdio.h>
#include <stdlib.h>

#define MAX_STR 256

int main(void) {

    long x = 0;
    char str[MAX_STR];
    char *endptr = NULL;

    while (1) {
        printf("1〜5の値を入力してください: ");

        char *ret = fgets(str, MAX_STR, stdin);
        if (ret == NULL) {
            printf("入力された値がおかしいです...\n");
            continue;
        }
        if (str[0] == '\n') {
            printf("入力された値がおかしいです...\n");
            continue;
        }

        x = strtol(str, &endptr, 10);
        if (*endptr != '\n' && *endptr != '\0') {
            printf("入力された値がおかしいです...\n");
            continue;
        }


        if (x >= 1 && x <= 5) {
            printf("入力値:%ld\n", x);
            break;
        } else {
            printf("入力された値がおかしいです...\n");
        }
    }
}

fgets 関数で整数入力を実現する手順

scanf 関数利用時と同様に、fgets 関数を利用する場合も読み込んだ文字列を整数に変換する処理が必要になります。一応補足しておくと、読み込んだ文字列は fgets 関数の第3引数で指定したアドレスに格納されます。

そして、この整数への変換に関しても scanf 関数の時同様に、atoi 関数や strtol 関数を利用することで実現することができます。

ですので、fgets 関数を利用して整数入力を実現するための基本的な処理の流れは scanf 関数の時と同様になります。

また、fgets 関数では第2引数で読み込みを行うサイズを指定することができるため、第2引数に読み込んだ文字列を格納する配列と同じサイズを指定しておくだけでバッファオーバーランを防ぐこともできます。

さらに、fgets 関数では 第2引数で指定した文字数 - 1 文字を読み込むまで or 改行文字を読み込むまで空白文字の有無に関わらず読み込みが行われます。

そのため、scanf 関数の時とは異なり、入力された文字列にスペース等が含まれる場合でも、そのスペースを含めて読み込みを行うことができます。

fgets関数がスペースを含めて読み込みを行う様子

なので、この点は scanf 関数の時とは動作が異なることになります。

また、fgets 関数では文字列の入力なしにエンターキー入力のみが行われた場合でも読み込みが開始され、この場合、改行文字('\n')のみが読み込まれることになります。さらに、fgets 関数では最後の改行文字('\n')も含めて読み込みが行われます。

MEMO

図では改行文字は省略していますが、エンターキーが入力された際には改行文字がバッファに入力されます

この辺りも scanf 関数と fgets 関数の違いであり、これを考慮しているために、前述のソースコードでは下記のように読み込んだ文字列が '\n' のみの場合や、文字列の最後が '\n' である場合についても考慮して動作できるように処理を記述しています。

読み込んだ文字列が改行文字のみの場合
if (str[0] == '\n') {
    printf("入力された値がおかしいです...\n");
    continue;
}
最後の文字が改行文字の場合の考慮
if (*endptr != '\n' && *endptr != '\0') {
    printf("入力された値がおかしいです...\n");
    continue;
}

fgets 関数で無限ループ対策を行うサンプルの動作確認

ここまでの解説を踏まえた上で、fgets 関数で無限ループ対策を行うサンプル で示したサンプルの動作確認を行なってみましょう!

fgets 関数で無限ループ対策を行うサンプル のソースコードをコンパイルして実行してアルファベットなどの入力を行なってみても無限ループが発生しないことを確認できると思います。

1〜5の値を入力してください: 100
入力された値がおかしいです...
1〜5の値を入力してください: abc
入力された値がおかしいです...
1〜5の値を入力してください: 10abc
入力された値がおかしいです...
1〜5の値を入力してください: abc10
入力された値がおかしいです...
1〜5の値を入力してください: abc def
入力された値がおかしいです...
1〜5の値を入力してください: 3
入力値:3

また、上記の abc def 入力時の挙動より、文字列入力で無限ループ対策を行うサンプル とは異なりスペースが含まれる場合も一度に読み込みが行われていることも確認することができると思います。

ただし、fgets 関数の第2引数を小さな値に設定した場合に複数回に分けて読み込みが行われる可能性がある点は、文字列入力で無限ループ対策を行うサンプル と同様になります。

スポンサーリンク

読み飛ばしを導入することで無限ループを防ぐ

最後に、読み飛ばしを導入することで無限ループの対策を行なっていきたいと思います。

読み飛ばしで無限ループ対策を行うサンプル

まずは、本章の結論として読み飛ばしを導入して無限ループ対策を行うサンプルを紹介しておきます。

そのソースコード例が下記となります。

読み飛ばしで無限ループ対策
#include <stdio.h>

int main(void) {

    int x = 0;
    int num = 0;

    while (1) {
        printf("1〜5の値を入力してください: ");
        num = scanf("%d", &x);
        scanf("%*[^\n]%*c");

        if (num > 0 && x >= 1 && x <= 5) {
            printf("入力値:%d\n", x);
            break;
        } else {
            printf("入力された値がおかしいです...\n");
        }
    }
}

読み飛ばしを導入する手順

この読み飛ばしに関しては、下記ページの内容を参考にさせていただいています。

https://www.letstryit.net/2009/10/c.html

読み飛ばしを導入する手順は単純で、目的のデータの読み込みに対する scanf 実行の後に scanf("%*[^\n]%*c") を実行するだけになります。

上記のソースコードで言えば、2つ目の scanf の実行が読み飛ばしを行う処理となります。

読み飛ばし
scanf("%*[^\n]%*c");

第1引数に指定しているフォーマットの中の * が読み飛ばしを意味する記号となります(読み込んだデータは捨てられる)。読み飛ばしをするため第2引数の指定は不要です。

また、一度目に実行する scanf では整数の入力受付を行なって整数が読み込まれるため、文字列から整数に変換するような処理は不要です。

そのため、第1引数に指定するフォーマットの指定が複雑ではあるものの、記述量としては 文字列入力にすることで無限ループを防ぐfgets 関数を利用することで無限ループを防ぐ で紹介した対策に比べて少なくて楽に実現することができると思います。

ただし、1度目に実行する scanf の返却値のチェックは行うようにした方が良いと思います。scanf は読み込んだデータの数を返却する仕様となっており、この返却値より scanf で読み込んだデータの個数をチェックすることができます。

この返却値が 0 の場合は、scanf で読み込まれたデータが 0 個であること、すなわち第1引数で指定したフォーマットのデータ(今回の場合は整数)が入力されなかったことをチェックすることができ、その場合に再度入力受付を行うような制御を行うことができます。

例えば abc のような整数が含まれない文字列が入力された際には一度目に実行する scanf の返却値は 0 となるため、整数以外が入力されたと考えて処理を行うことができます。サンプルでは、返却値が 0 の場合は次の入力受付が必ず行われるようにしています。

スポンサーリンク

読み飛ばしの動作

上記の scanf においては、引数 "%*[^\n]%*c" の指定によって次の2つの読み込みが行われるようになっています。

1つ目は改行文字までの全てのデータの読み込みになります。それを指定しているのが %*[^\n] 部分になります。

これにより、1回目の scanf 実行後にバッファに残った改行文字の直前までの全てのデータの読み込みが行われます。

改行文字の直前までの文字列が読み飛ばしされる様子

キーボードからの入力では最後にエンターキーを押して改行が入力されることになるため、その改行までのすべてのデータがの読み込みが行われることになります。

2つ目に行われるのは任意の1つの文字の読み込みになります。

1つ目に行われる読み込みによって改行文字までのデータの読み込みが行われるため、バッファに「まだ読み込んでいないデータ」として改行文字が残っていることになります。その改行文字が読み込まれることになります。

改行文字が読み飛ばしされる様子

そしてこれによって、バッファには「まだ読み込んでいないデータ」が残っていないことになります(全て読み込みが行われる)。

上記のような読み込みが行われるため、ループが一周するたびにバッファの「まだ読み込んでいないデータ」が綺麗になくなり、無限ループを防ぐことができることになります。

読み飛ばしで無限ループ対策を行うサンプルの動作確認

最後に 読み飛ばしで無限ループ対策を行うサンプル で紹介したサンプルの動作確認を行なっておきましょう!

読み飛ばしで無限ループ対策を行うサンプル で紹介したソースコードをコンパイルして実行し、さらに様々な入力を行なってみることで、無限ループ対策が行われていることを確認することができると思います。

1〜5の値を入力してください: 100
入力された値がおかしいです...
1〜5の値を入力してください: abc
入力された値がおかしいです...
1〜5の値を入力してください: 10abc
入力された値がおかしいです...
1〜5の値を入力してください: abc10
入力された値がおかしいです...
1〜5の値を入力してください: abc def
入力された値がおかしいです...
1〜5の値を入力してください: 3
入力値:3

動作としては、文字列入力で無限ループ対策を行うサンプルfgets 関数で無限ループ対策を行うサンプル と同じようにも思えるのですが、実は結構大きな動作の違いがあります。

例えば 1abc が入力された場合、文字列入力で無限ループ対策を行うサンプルfgets 関数で無限ループ対策を行うサンプル では scanf 後の文字列から整数への変換が strtol で行われるのですが、strtol 実行後の endptr のチェックにより、整数以外の文字が入力されていることを検知してエラーとなります。

より具体的には、文字列入力で無限ループ対策を行うサンプル の場合、1abc が入力された際には下記条件が True となって continue が実行されます(strtol 内で endptr には整数の変換に失敗した最初の文字のアドレスが格納されるため、*endptr'a' となる)。

1abcが入力された場合にTrueと判断される条件
if (*endptr != '\0') {
    printf("入力された値がおかしいです...\n");
    continue;
}

continue によってループの最初に戻るため、下記は行われずに次の入力受付が行われることになります。

1abcが入力された場合にスキップされる処理
if (x >= 1 && x <= 5) {
    printf("入力値:%ld\n", x);
    break;
} else {
    printf("入力された値がおかしいです...\n");
}

それに対し、読み飛ばしで無限ループ対策を行うサンプル の場合、scanf で最初の 1 のみが読み込まれて x1 が格納されます。

整数のみが読み込まれて変数に値が格納される様子

その結果、1abc が入力されたにも関わらず 1 が入力された時と同様に下記の条件が True となります。そのため、正常に入力が行われたと判断されて break が実行され、入力受付のループを終了することになります。

1abcが入力された場合にbreakを実行する処理
if (x >= 1 && x <= 5) {
    printf("入力値:%ld\n", x);
    break;
} else {
    printf("入力された値がおかしいです...\n");
}

このような違いがあるため、1abc のような純粋な整数以外が入力されたことを検知したいのであれば、文字列入力で無限ループ対策を行うサンプルfgets 関数で無限ループ対策を行うサンプル のように文字列の入力受付を行い、読み込んだ文字列を strtol で整数変換するようにした方が良いです。

ちなみに、文字列の整数変換に使用する関数として atoi を利用した場合は、1abc が入力された場合は 読み飛ばしで無限ループ対策を行うサンプル と同じ結果となります。

まとめ

このページでは、scanf 利用時に発生する無限ループの対策方法について解説しました!

scanf 関数で整数の入力受付を行い、さらに scanf 関数で読み込んだデータが不適切な場合にループして再度 scanf 関数を実行するような構造の処理を行なっている場合、整数以外が入力された際に無限ループが発生してしまいます。

この無限ループはバッファに「まだ読み込んでいないデータ」が存在し続けることが原因で発生するものであり、整数ではなく文字列の入力を受け付ける or scanf 関数実行後にバッファのデータの読み飛ばしを行うようにすることで対策することができます。また、前者に関しては scanf 関数でも実現できますし、fgets 関数を利用して実現することも可能です。

ただし、特に今回紹介したサンプルではそれぞれで微妙な動作の違いがあるので注意してください。

個人的には、特にプログラミング入門者の方であれば scanf 関数は完璧に使いこなせる必要はなく、自身が作成したプログラムの操作性が極端に悪くならないようにさえ気をつければ良いと思っています。

例えば、プログラム開始時点で1度だけユーザーから整数の入力を受け付けるようなものであれば、間違って整数以外を入力して無限ループが発生したとしても、プログラムを再起動すれば良いだけです。

それに対し、何度もユーザーからの整数の入力を受け付けるような場合、途中でユーザーが入力を間違って無限ループが発生すると、そこまで入力した作業が台無しになってしまいます。そんなプログラムは不便ですよね…。

そのようなプログラムを開発する場合、今回紹介した対策を導入することでプログラムの使いやすさを向上することができますので、是非このページの内容を参考にしていただければと思います!

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