【C/C++】scanfでループしたので標準入力から数値を受け取ることについてまとめる

scanfでループする

以下のコードは不正な入力(例えばabc)を与えると無限ループします.

 1/*    scanfに不正入力をするとループするテスト*/
 2#include <stdio.h>
 3#include <stdlib.h>
 4
 5int main(void){
 6    int x = 0;
 7    while(true){
 8        printf("Please input x\n");
 9        scanf("%d", &x);
10        printf("x = %d\n", x);
11        if(x == -1) break;
12    }
13    return EXIT_SUCCESS;
14}

この原因はscanfが予期しないデータの入力によって,
バッファのデータをそのまま残して動作を終了していることにあります.

scanfは予期しない入力があると無限ループに陥る(C学習中) - 虎塚

標準入出力関数(1)

変換指定文字列で、期待していなかったデータを入力すると、 バッファのデータをそのまま残し、動作を終了してしまいます。

1fflush(stdin);
1scanf("%*c");

このあたりも試してバッファのクリアを狙ってみましたが,思うような動作には至りませんでした.

fgetsのあとatoiする

 1// 独自関数fgetiを実装する
 2// ほぼ問題ないがatoiが範囲外の処理ができない
 3#include<stdio.h>
 4#include<stdlib.h>
 5#include<string.h>
 6#include<limits.h>
 7#define N 512
 8
 9int fgeti(int*, FILE*);
10
11int main()
12{
13    int x;
14    while(true)
15    {
16        printf("Please input a number\n");
17        if (fgeti(&x, stdin) == EOF)
18        {
19            continue;
20        }
21        printf("x = %d\n", x);
22    }
23}
24
25int fgeti(int* p, FILE *fp)
26{
27    char buf[N];
28    if (fgets(buf, N, fp) == NULL)
29    {
30        return EOF;
31    }
32    *p = atoi(buf);
33    if(*p == 0 && (strcmp(buf, "0\n")!=0))
34    {
35        return EOF;
36    }
37    return 1;
38}

こうすることで大抵の処理には耐えますが,範囲(INT_MIN〜INT_MAX)外と
123abcのような頭に数字のついた入力に耐えません.
文字列から数値への変換 - forest book
なお,atoiは変換できない文字列の際に0を返すことにも注意です.
【C言語】atoi関数|ato関数群(atoi, atol, atoll, atof)完全解説 | MaryCore

fgetsのあとstrtolする

 1atoiよりエラーに強いstrtolを使って  
 2strtoiという関数を定義しました
 3
 4strtolは第二引数のendptrに変換出来ない文字列を格納します  
 5[C言語関数辞典 - strtol](http://www.c-tipsref.com/reference/stdlib/strtol.html)
 6
 7
 8// 独自関数fgetiを実装する// atoiからstrtoi(自作)に変更
 9// これでどんな入力にも対応できる...はず
10#include<stdio.h>
11#include<stdlib.h>
12#include<string.h>
13#include<limits.h>
14#define N 512
15
16int fgeti(int*, FILE*);
17int strtoi(const char*);
18
19int main()
20{
21    int x;
22    while(true)
23    {
24        printf("Please input a number\n");
25        if (fgeti(&x, stdin) == EOF)
26        {
27            continue;
28        }
29        printf("x = %d\n", x);
30    }
31}
32
33int fgeti(int* p, FILE *fp)
34{
35    char buf[N];
36    if (fgets(buf, N, fp) == NULL)
37    {
38        return EOF;
39    }
40    *p = strtoi(buf);
41    if(*p == 0 && (strcmp(buf, "0\n")!=0))
42    {
43        return EOF;
44    }
45    return 1;
46}
47
48int strtoi(const char* str)
49{
50    char* endptr;
51    long lstr = strtol(str, &endptr, 10);
52    if (*endptr != '\0' || lstr INT_MIN || INT_MAX < lstr)
53    {
54        return 0;
55    }
56    return (int)lstr;
57}

少なくともscanfで直接受け取るよりは堅牢かと思います.