akyboxブログ

旧AkiIroブログ

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

scanfでループする

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

/*
    scanfに不正入力をするとループするテスト
*/
#include <stdio.h>
#include <stdlib.h>

int main(void){
    int x = 0;
    while(true){
        printf("Please input x\n");
        scanf("%d", &x);
        printf("x = %d\n", x);
        if(x == -1) break;
    }
    return EXIT_SUCCESS;
}

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

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

標準入出力関数(1)

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

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

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

fgetsのあとatoiする

// 独自関数fgetiを実装する
// ほぼ問題ないがatoiが範囲外の処理ができない
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<limits.h>
#define N 512

int fgeti(int*, FILE*);

int main()
{
    int x;
    while(true)
    {
        printf("Please input a number\n");
        if (fgeti(&x, stdin) == EOF)
        {
            continue;
        }
        printf("x = %d\n", x);
    }
}

int fgeti(int* p, FILE *fp)
{
    char buf[N];
    if (fgets(buf, N, fp) == NULL)
    {
        return EOF;
    }
    *p = atoi(buf);
    if(*p == 0 && (strcmp(buf, "0\n")!=0))
    {
        return EOF;
    }
    return 1;
}

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

fgetsのあとstrtolする

atoiよりエラーに強いstrtolを使って
strtoiという関数を定義しました.

strtolは第二引数のendptrに変換出来ない文字列を格納します.
C言語関数辞典 - strtol

// 独自関数fgetiを実装する
// atoiからstrtoi(自作)に変更
// これでどんな入力にも対応できる...はず
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<limits.h>
#define N 512

int fgeti(int*, FILE*);
int strtoi(const char*);

int main()
{
    int x;
    while(true)
    {
        printf("Please input a number\n");
        if (fgeti(&x, stdin) == EOF)
        {
            continue;
        }
        printf("x = %d\n", x);
    }
}

int fgeti(int* p, FILE *fp)
{
    char buf[N];
    if (fgets(buf, N, fp) == NULL)
    {
        return EOF;
    }
    *p = strtoi(buf);
    if(*p == 0 && (strcmp(buf, "0\n")!=0))
    {
        return EOF;
    }
    return 1;
}

int strtoi(const char* str)
{
    char* endptr;
    long lstr = strtol(str, &endptr, 10);
    if (*endptr != '\0' || lstr < INT_MIN || INT_MAX < lstr)
    {
        return 0;
    }
    return (int)lstr;
}

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