このページでは、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
関数の引数についておさらいしておくと、まず第1引数では入力を受け付けるデータのフォーマットを指定することになります。例えば整数を読み込みたいのであれば %d
を、文字列を読み込みたいのであれば %s
を指定しますよね!
今回対策を行うのは整数の入力受付を行なった際に発生する無限ループですので、本章では、第1引数に %d
が指定されていることを前提に解説を進めていきます。
また、第2引数では入力されたデータの格納先のアドレスを指定します。例えば入力された整数を int
型の変数 x
に格納したいのであれば &x
を指定します。
scanf
関数の読み込み動作
ここまでが、scanf
の動作を理解するための前提知識となります。続いて本題の scanf
関数の動作について考えていきましょう!
まず、先ほども説明したように、標準入力から入力されたデータはバッファに格納されます。
そして、scanf
関数が実行された際には、scanf
関数はそのバッファをチェックし、バッファに「まだ読み込んでいないデータ」が存在するかどうかを判断します。
もし、存在しないのであれば、scanf
関数はバッファにデータが格納されるまで入力待ちを行います。
そして、標準入力から入力が行われると「まだ読み込んでいないデータ」がバッファに格納されることになりますので、その際にはバッファから「まだ読み込んでいないデータ」を読み込み、読み込んだデータを第2引数で指定されたアドレスに格納します。
ただし、この際に読み込まれるデータは「第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
の読み込みが行われることになります(最初のスペースは飛ばされる)。
このように、文字列入力を行うことで無限ループを防ぐことはできるのですが、空白文字が存在する場合は入力データの区切りとして扱われる点に注意してください。
実は scanf
関数の第1引数を工夫すれば空白文字も含めて一度に読み込みを行うこともできるのですが、それを行うのであれば次に紹介する fgets
関数を利用する方が楽だと思います。
fgets
関数で文字列の入力受付を行う
もう1つの方法は scanf
関数を使うのではなく fgets
関数を利用する方法になります。
fgets
については下記ページで詳しく説明していますので、詳細は下記ページを参考にしていただければと思います。
fgets
はファイルからの読み込みを行う関数なのですが、ファイルを指定する引数に stdin
を指定することで標準入力からの読み込みを行うことができるようになります。そしてこれによって、scanf
関数同様に標準入力からの入力を受け付けることができるようになります。
char str[256];
// 標準入力からの入力の読み込み
fgets(str, 256, stdin);
実は、fgets
においても scanf
同様に「まだ読み込んでいないデータ」がバッファに残っている場合は入力待ちを行わずにバッファのデータの読み込みを行う特性があります。ですが、fgets
ではデフォルトで文字列入力を受け付けるようになっているため、scanf
関数と違って無限ループが起こらないようになっています。
また、scanf
関数とは異なり、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
関数の使い方については下記ページで解説していますので、詳しく知りたい方はこちらを参照していただければと思います。
今回は strtol
関数を利用して文字列から整数への変換を行いたいと思います。上記ページでも解説していますが、strtol
関数ではエラーチェックが行えるというメリットがあります(その分使い方は難しいですが…)。
例えば atoi
関数の場合、返却値が 0
の場合に入力された文字列が "0"
であったか、アルファベット等の整数以外であったかの判別ができませんが、strtol
関数を利用すればこれらの判別を行うことができます。
上記を踏まえ、さらに必要になる変数についても考慮すると、文字列入力で整数入力を実現するためには下記のような手順を踏む必要があります。
- 整数を格納するための変数を宣言する
- 文字列を格納するための変数を宣言する
scanf
で文字列入力の受付を行う- (入力された文字列は 2. で宣言した変数に格納する)
scanf
で読み込まれた文字列をstrtol
関数で整数に変換する- (変換後の整数は 1. で宣言した変数に格納する)
strtol
関数のエラーチェックを行う- エラーが発生している場合は 3. に戻る
- 変換後の整数を利用して必要な処理を実行する
文字列入力で無限ループ対策を行うサンプル で示したソースコードにおいても上記のような流れで処理を行なっていることが確認できると思います。
スポンサーリンク
バッファオーバーランに注意!
特にこの方法において注意が必要なのはバッファオーバーランになります。
バッファオーバーランの危険性
バッファオーバーランとは、簡単に言えば、配列のサイズよりも大きなデータを配列に格納することになります。これによって、配列の領域外のメモリにデータが書き込まれることになり、大事なデータが破壊されてしまう可能性があります。
このバッファオーバーランを利用して、例えば悪意あるユーザーによってパスワードを書き換えられてしまうようなケースもあり得ますので、バッファオーバーランが発生しないように注意が必要になります。
特に、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"
を指定しています。
scanf_s
などの読み込みサイズを指定可能な関数を使用できるのであれば、scanf
ではなく scanf_s
を使用することでバッファオーバーラン対策を行うことができます
一度に読み込めないケースが存在するので注意
先程の解説を踏まえて実装すれば、一番注意すべきバッファオーバーランを防ぐことは可能です。
ですが、一度に全ての文字列の読み込みができなくなる可能性があるので注意してください。
例えば、scanf
の第1引数に "%4s"
を指定した場合、5
文字以上の入力が行われた際には 5
文字目以降の文字列がバッファに「まだ読み込んでいないデータ」として残ることになります。
つまり、2回目の scanf
では入力待ちが行われずにバッファに残っているデータの読み込みが行われることになります。無限ループにはならないですが、ちょっとプログラムがおかしな挙動をしてしまうことになります。
これを完全に防ぐことは難しいですが、用意する配列のサイズを十分に大きくしておくことで大概のケースで本現象を防ぐことはできると思います。
例えば、ページの冒頭で紹介したソースコードでは 1
〜 5
の整数の入力を期待しているのに対し、配列のサイズを 256
、つまり scanf
の第1引数に "%255s"
を指定しておけば、256
文字以上の入力が行われない限り本現象を防ぐことができます。
printf
によって “1
文字の入力(1
〜 5
の入力)を行なって欲しい” と表示しているのにも関わらず、間違って 256
文字以上の入力を行うユーザーはまずいませんので、間違って入力が行われた際の対策としては十分だと思います。
ですが、悪意あるユーザーによって 256
文字以上の入力が行われる可能性は当然ありますので、その点は頭に入れておくと良いと思います。例えば実際の製品開発においては、悪意あるユーザーが存在することも考慮して仕様を決めたりプログラミングを行う必要があります。
ただ、おそらく実際の製品開発時には、ユーザーからの指示入力にC言語の scanf
を使う機会はまず無いと思うんですよね…。
なので、特にC言語の勉強やお試しのゲーム開発などで scanf
を使う方は、ユーザーが間違って入力を行なった際の対応だけ入れておくので十分だと思います。ただ、scanf
の危険性についてはしっかり覚えておいた方が良いです(バッファオーバーランが発生しやすい)。
また、標準入力に入力された文字列の中に空白文字(スペースやタブ)が存在する場合、単純に scanf
を実行すると一度に空白文字までしか読み込みが行われないので注意してください。
文字列入力で無限ループ対策を行うサンプルの動作確認
ここまで解説してきた内容を踏まえて、文字列入力で無限ループ対策を行うサンプル で紹介したソースコードの動作確認を行なってみましょう!
ソースコードをコンパイルして実行すれば入力受付が行われるのでアルファベットなどを入力してエンターキーを押してみてください。無限ループが発生することなく、正常に入力の再受付が行われるはずです。
私の方で色々入力を行なってみた結果が下記となります。文字列入力にすることで無限ループを防ぐことができ、また 1
〜 5
の整数が入力された際には正常に入力値の表示が行われてプログラムが終了することも確認できると思います。
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
関数を使用して無限ループ対策を行うサンプルを紹介しておきます。
そのソースコード例が下記となります。
#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
関数の時とは異なり、入力された文字列にスペース等が含まれる場合でも、そのスペースを含めて読み込みを行うことができます。
なので、この点は scanf
関数の時とは動作が異なることになります。
また、fgets
関数では文字列の入力なしにエンターキー入力のみが行われた場合でも読み込みが開始され、この場合、改行文字('\n'
)のみが読み込まれることになります。さらに、fgets
関数では最後の改行文字('\n'
)も含めて読み込みが行われます。
図では改行文字は省略していますが、エンターキーが入力された際には改行文字がバッファに入力されます
この辺りも 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'
となる)。
if (*endptr != '\0') {
printf("入力された値がおかしいです...\n");
continue;
}
continue
によってループの最初に戻るため、下記は行われずに次の入力受付が行われることになります。
if (x >= 1 && x <= 5) {
printf("入力値:%ld\n", x);
break;
} else {
printf("入力された値がおかしいです...\n");
}
それに対し、読み飛ばしで無限ループ対策を行うサンプル の場合、scanf
で最初の 1
のみが読み込まれて x
に 1
が格納されます。
その結果、1abc
が入力されたにも関わらず 1
が入力された時と同様に下記の条件が True
となります。そのため、正常に入力が行われたと判断されて 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度だけユーザーから整数の入力を受け付けるようなものであれば、間違って整数以外を入力して無限ループが発生したとしても、プログラムを再起動すれば良いだけです。
それに対し、何度もユーザーからの整数の入力を受け付けるような場合、途中でユーザーが入力を間違って無限ループが発生すると、そこまで入力した作業が台無しになってしまいます。そんなプログラムは不便ですよね…。
そのようなプログラムを開発する場合、今回紹介した対策を導入することでプログラムの使いやすさを向上することができますので、是非このページの内容を参考にしていただければと思います!