C
C言語
stdin

C言語で安全に標準入力から数値を取得

はじめに

多くの入門書で数値の入力を受けるのにscanf関数を使用していますが、これが明確に誤りなのは

[迷信] scanf ではバッファオーバーランを防げない | 株式会社きじねこ

scanf 系の関数では、整数や実浮動小数点数を読み込む際に、オーバーフローやアンダーフローが発生しても検知することができません。実引数で指定した格納先の型で、入力した数値を表現できない場合の動作は未定義なのです。

すでに有名な話です。というわけでなんとかしていきましょう。

エラーを戻り値で返す(これはダメ)

つまりfgetsとstrtol系関数を組み合わせることが唯一解となります。

strtol系関数は全部で以下のとおりです。

function name type
strtol long
strtoul unsigned long
strtoll unsigned long long C99
strtoull unsigned long long C99
strtof float C99
strtod double
strtold long double C99

ここに無い型はそれより大きい方から範囲チェックしたうえでキャストする必要があります。

一部関数はC99以降となりますが、今時C99すら使えない糞環境無いよね?(煽り)

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include <errno.h>

/**
 * @brief 標準入力を数字に変換する。
 * @param max 戻り値の最大値
 * @param min 戻り値の最小値
 * @return 入力した数字、エラー時はINT_MIN, EOFのときはEOF
 */
int get_integer_num(const int max, const int min){
    char s[100];

    if (NULL == fgets(s, 100, stdin)){
        if (feof(stdin)){//エラーの原因がEOFか切り分け
            return EOF;
        }
        //改行文字が入力を受けた配列にない場合、入力ストリームにごみがある
        size_t i;
        for(i = 0; i < 100 && '\0' == s[i]; i++);//strlenもどき
        if('\n' != s[i - 1]) while(getchar() != '\n');//入力ストリームを掃除
        return INT_MIN;
    }
    if ('\n' == s[0]) return INT_MIN;
    errno = 0;
    char* endptr;
    const long t = strtol(s, &endptr, 10);
    if (0 != errno || (0 == t && endptr == s) || t < min || max < t)
        return INT_MIN;
    return (int)t;
}
/**
 * @brief 標準入力を数字に変換する。
 * @param max 戻り値の最大値
 * @param min 戻り値の最小値
 * @return 入力した数字、エラー時は-2, EOFのときはEOF
 */
double get_double_num(const double max, const double min){
    char s[200];

    if (NULL == fgets(s, 200, stdin)){
        if (feof(stdin)){//エラーの原因がEOFか切り分け
            return EOF;
        }
        //改行文字が入力を受けた配列にない場合、入力ストリームにごみがある
        size_t i;
        for(i = 0; i < 100 && '\0' == s[i]; i++);//strlenもどき
        if('\n' != s[i - 1]) while(getchar() != '\n');//入力ストリームを掃除
        return -2;
    }
    errno = 0;
    char* endptr;
    const double t = strtod(s, &endptr);
    if (0 != errno || (0 == t && endptr == s) || t < min || max < t)
        return -2;
    return t;
}

エラー時は再帰する(これもだめ)

しかし戻り値としてエラーを通知すると、例えば上の場合INT_MINが入力されたらエラーに成ってしまう、という問題があります。つまり結果とエラーを分離する必要があるわけです。

かと言ってエラーコードを引数経由で返すのは冗長ですし、入力された値を引数経由で返すのは入力を受けた後constに変数をできないのでダメです。

さて、こういう入力の時、多くの場合はエラーメッセージを吐いて再度入力を求めたいことが多いはず。ならば再帰しましょう。

C99
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>//in gcc
#include <errno.h>//in gcc
#include <stdbool.h>
/**
 * @brief 文字列が文字を持っているか調べます。
 * @param str 対象文字列へのポインタ
 * @return false: nullptrか空白文字のみの文字列 true:それ以外
 */
static bool str_has_char(const char *str) {
    bool ret = false;
    for (; !ret && *str != '\0'; str++)
        ret = (*str != ' ');
    return ret;
}
/**
 * @brief 標準入力から入力を受け、int型に変換する
 * @details fgetsしてstrtolしている。max, minの条件に合わないかエラー時は再帰
 * @param message 入力を受ける前にputsに渡す文字列。表示しない場合はnullptrか空白文字のみで構成された文字列へのポインタを渡す
 * @param max 入力値を制限する。最大値を指定
 * @param min 入力値を制限する。最小値を指定
 * @return 入力した数字、EOFのときはEOF
 */
int input_int(const char* message, const int max, const int min){
    if(str_has_char(message)) puts(message);
    char s[100];

    if (NULL == fgets(s, 100, stdin)){
        if (feof(stdin)) return EOF;
        //改行文字が入力を受けた配列にない場合、入力ストリームにごみがある
        size_t i;
        for(i = 0; i < 100 && '\0' == s[i]; i++);//strlenもどき
        if('\n' != s[i - 1]) while(getchar() != '\n');//入力ストリームを掃除
        return input_int("再入力してください", max, min);
    }
    if ('\n' == s[0]) return INT_MIN;
    char* endptr;
    const long t = strtol(s, &endptr, 10);
    if (0 != errno || (0 == t && endptr == s) || t < min || max < t)
        return input_int("再入力してください", max, min);
    return (int)t;
}
typedef unsigned int uint;
/**
 * @brief 標準入力から入力を受け、unsigned int型に変換する
 * @details fgetsしてstrtoulしている。max, minの条件に合わないかエラー時は再帰
 * @param message 入力を受ける前にputsに渡す文字列。表示しない場合はnullptrか空白文字のみで構成された文字列へのポインタを渡す
 * @param max 入力値を制限する。最大値を指定
 * @param min 入力値を制限する。最小値を指定
 * @return 入力した数字、EOFのときはEOF
 */
uint input_uint(const char* message, const uint max, const uint min){
    if(str_has_char(message)) puts(message);
    char s[100];

    if (NULL == fgets(s, 100, stdin)){
        if (feof(stdin)) return EOF;
        //改行文字が入力を受けた配列にない場合、入力ストリームにごみがある
        size_t i;
        for(i = 0; i < 100 && '\0' == s[i]; i++);//strlenもどき
        if('\n' != s[i - 1]) while(getchar() != '\n');//入力ストリームを掃除
        return input_uint("再入力してください", max, min);
    }
    if ('\n' == s[0]) return INT_MIN;
    errno = 0;
    char* endptr;
    const unsigned long t = strtoul(s, &endptr, 10);
    if (0 != errno || (0 == t && endptr == s) || t < min || max < t)
        return input_uint("再入力してください", max, min);
    return (uint)t;
}
/**
 * @brief 標準入力から入力を受け、double型に変換する
 * @details fgetsしてstrtodしている。max, minの条件に合わないかエラー時は再帰
 * @param message 入力を受ける前にputsに渡す文字列。表示しない場合はnullptrか空白文字のみで構成された文字列へのポインタを渡す
 * @param max 入力値を制限する。最大値を指定
 * @param min 入力値を制限する。最小値を指定
 * @return 入力した数字、EOFのときはEOF
 */
double input_double(const char* message, const double max, const double min){
    if(str_has_char(message)) puts(message);
    char s[100];

    if (NULL == fgets(s, 100, stdin)){
        if (feof(stdin)) return EOF;
        //改行文字が入力を受けた配列にない場合、入力ストリームにごみがある
        size_t i;
        for(i = 0; i < 100 && '\0' == s[i]; i++);//strlenもどき
        if('\n' != s[i - 1]) while(getchar() != '\n');//入力ストリームを掃除
        return input_double("再入力してください", max, min);
    }
    if ('\n' == s[0]) return INT_MIN;
    errno = 0;
    char* endptr;
    const double t = strtod(s, &endptr);
    if (0 != errno || (0 == t && endptr == s) || t < min || max < t)
        return input_double("再入力してください", max, min);
    return t;
}

エラー時は関数内でループする(これもだめ)

コメントで @yohhoy さんから

蛇足ですが、「エラー時に再帰呼び出し」はスタックオーバーフロー攻撃への脆弱性になり得るため、同一関数内でのリトライ処理の方が良いかもしれませんね。

とか言われたので、再帰じゃなくてループに書き直します。

ついでにEOFかどうかと戻り値も分離。そもそも再帰した理由がエラーコードと入力値を混ぜて戻り値で返すとか正気じゃない、ということだったはずなのになんでEOFは分離しなかった、過去の私。

というわけでEOFかどうかは呼び出し元でfeof関数を呼んで調べてください。

C99
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>//in gcc
#include <errno.h>//in gcc
#include <stdbool.h>
#include <ctype.h>
#include <float.h>
#if defined(_MSC_VER) || defined(__cplusplus)
#   define RESTRICT
#else 
#   define RESTRICT restrict
#endif
/**
 * @brief 文字列が文字を持っているか調べます。
 * @param str 対象文字列へのポインタ
 * @return false: nullptrか空白文字のみの文字列 true:それ以外
 */
static inline bool str_has_char(const char *str) {
    if(NULL == str) return false;
    bool ret = false;
    for (; !ret && *str != '\0'; str++) ret = (*str != ' ');
    return ret;
}
/**
 * @brief 文字列が文字を持っているか調べます。
 * @param io 書き換えるbool型変数へのポインタ、呼び出し後はポインタが指す変数にnew_valueが代入される
 * @param new_value 新しい値
 * @return ioが指すbool変数がもともと持っていた値
 */
static inline bool exchange_bool(bool* RESTRICT const io, const bool new_value){
    const bool tmp = *io;
    *io = new_value;
    return tmp;
}
/**
 * @brief fgetsで失敗したときにストリームをクリアしてループする関数
 * @param s ストリームから読み取った文字列を格納するための領域へのポインタ
 * @param buf_size ストリームから読み取った文字列を格納するための領域の大きさ
 * @param stream FILE構造体へのポインタかstdin
 * @param message_on_error エラー時に表示してループする
 * @return 成功時は0, EOFのときはEOF
 */
static inline int fgets_wrap(char* RESTRICT const s, size_t buf_size, FILE* RESTRICT const stream, const char* RESTRICT message_on_error){
    bool first_flg = true;
    size_t i;
    for (i = 0; i < 100 && NULL == fgets(s, buf_size, stream); ++i){
        if (feof(stdin)) return EOF;
        if (!exchange_bool(&first_flg, false)) puts((message_on_error) ? message_on_error : "再入力してください");
        //改行文字が入力を受けた配列にない場合、入力ストリームにごみがある
        size_t j;
        for (j = 0; j < 100 && '\0' == s[j]; j++);//strlenもどき
        if ('\n' != s[j - 1]) while (fgetc(stream) != '\n');//入力ストリームを掃除
    }
    if (100 == i) exit(1);//無限ループ防止
    return 0;
}
/**
 * @brief 標準入力から入力を受け、int型に変換する
 * @details fgetsしてstrtolしている。max, minの条件に合わないかエラー時はループ
 * @param message 入力を受ける前にputsに渡す文字列。表示しない場合はnullptrか空白文字のみで構成された文字列へのポインタを渡す
 * @param message_on_error エラー時に表示してループする
 * @param max 入力値を制限する。最大値を指定
 * @param min 入力値を制限する。最小値を指定
 * @return 入力した数字、EOFのときは0
 */
static inline int input_int(const char* message, const char* RESTRICT message_on_error, const int max, const int min){
    if (str_has_char(message)) puts(message);
    char s[100];
    long t = 0;
    size_t i = 0;
    for(char* endptr = s; ((0 == t && endptr == s) || 0 != errno || t < min || max < t) && i < 100; ++i){
        if (0 != fgets_wrap(s, 100, stdin, message_on_error)) return 0;//EOF
        if ('\n' == s[0]) continue;//数字が入っていないときはループ
        t = strtol(s, &endptr, 10);
    }
    if (100 == i) exit(1);//無限ループ防止
    return ((int)(t));
}
/**
 * @brief 標準入力から入力を受け、unsigned int型に変換する
 * @details fgetsしてstrtolしている。max, minの条件に合わないかエラー時はループ
 * @param message 入力を受ける前にputsに渡す文字列。表示しない場合はnullptrか空白文字のみで構成された文字列へのポインタを渡す
 * @param message_on_error エラー時に表示してループする
 * @param max 入力値を制限する。最大値を指定
 * @param min 入力値を制限する。最小値を指定
 * @return 入力した数字、EOFのときは0
 */
static inline unsigned int input_uint(const char* message, const char* RESTRICT message_on_error, const unsigned int max, const unsigned int min){
    if (str_has_char(message)) puts(message);
    char s[100];
    unsigned long t = 0;
    size_t i = 0;
    for(char* endptr = s; ((0 == t && endptr == s) || 0 != errno || t < min || max < t) && i < 100; ++i){
        if (0 != fgets_wrap(s, 100, stdin, message_on_error)) return 0;//EOF
        if ('\n' == s[0]) continue;//数字が入っていないときはループ
        t = strtoul(s, &endptr, 10);
    }
    if (100 == i) exit(1);//無限ループ防止
    return ((unsigned int)(t));
}
/**
 * @brief 標準入力から入力を受け、double型に変換する
 * @details fgetsしてstrtolしている。max, minの条件に合わないかエラー時はループ
 * @param message 入力を受ける前にputsに渡す文字列。表示しない場合はnullptrか空白文字のみで構成された文字列へのポインタを渡す
 * @param message_on_error エラー時に表示してループする
 * @param max 入力値を制限する。最大値を指定
 * @param min 入力値を制限する。最小値を指定
 * @return 入力した数字、EOFのときは0
 */
static inline double input_double(const char* message, const char* RESTRICT message_on_error, const double max, const double min){
    if (str_has_char(message)) puts(message);
    char s[100];
    double t = 0;
    size_t i = 0;
    for(char* endptr = s; ((0 == t && endptr == s) || 0 != errno || t < min || max < t) && i < 100; ++i){
        if (0 != fgets_wrap(s, 100, stdin, message_on_error)) return 0;//EOF
        if ('\n' == s[0]) continue;//数字が入っていないときはループ
        t = strtod(s, &endptr);
    }
    if (100 == i) exit(1);//無限ループ防止
    return t;
}

ループで書き直して、ついでに100回リトライすると強制的にexitするようにした。restrictの使い方があっているかは少し不安。

長過ぎる入力に正しく対応する

上のだと長過ぎる入力にうまく対応できていない。言い換えるとfgetsの扱い方に問題がある。

まずfgetsは環境によっては長大な入力を受け取ったときにerrnoERANGE(34)に設定してくる。するとstrtol系関数のエラー捕捉のためにerrnoを見ていたが、これをbypassしないといけない。

また長大な入力のときにfgetsが戻り値NULLを通知すると勘違いしていたため、入力ストリームの整理部分が機能していなかった。

というわけで直した。

C言語の標準入力難しすぎでは?

C11
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <errno.h>
#include <stdbool.h>
#include <ctype.h>
#include <float.h>
#include <assert.h>
#ifndef static_assert
#   define NO_STDC_STATIC_ASSERT
#   define static_assert(...)
#endif
#if defined(_MSC_VER) || defined(__cplusplus)
#   define restrict
#endif
/**
 * @brief 文字列が文字を持っているか調べます。
 * @param str 対象文字列へのポインタ
 * @return false: nullptrか空白文字のみの文字列 true:それ以外
 */
static inline bool str_has_char(const char *str)
{
    if (NULL == str) return false;
    bool ret = false;
    for (; !ret && *str != '\0'; str++) ret = (*str != ' ');
    return ret;
}
/**
 * @brief 文字列が文字を持っているか調べます。
 * @param io 書き換えるbool型変数へのポインタ、呼び出し後はポインタが指す変数にnew_valueが代入される
 * @param new_value 新しい値
 * @return ioが指すbool変数がもともと持っていた値
 */
static inline bool exchange_bool(bool* restrict const io, const bool new_value)
{
    const bool tmp = *io;
    *io = new_value;
    return tmp;
}
/**
 * @brief fgetsで失敗したときにストリームをクリアしてループする関数
 * @param s ストリームから読み取った文字列を格納するための領域へのポインタ
 * @param buf_size ストリームから読み取った文字列を格納するための領域の大きさ
 * @param stream FILE構造体へのポインタかstdin
 * @param message_on_error エラー時に表示してループする
 * @return 成功時は0, new line at the end of fileのときは-1
 */
static inline int fgets_wrap(char* restrict const s, size_t buf_size, FILE* restrict const stream, const char* restrict message_on_error)
{
    size_t i = 0;
    for (bool first_flg = true; i < 100 && NULL == fgets(s, buf_size, stream); ++i) {
        if (feof(stdin)) return -1;
        if (!exchange_bool(&first_flg, false)) puts((message_on_error) ? message_on_error : "再入力してください");
    }
    if (100u == i) exit(1);//無限ループ防止
    if (feof(stdin)) return 0;
    //改行文字が入力を受けた配列にない場合、入力ストリームにごみがある
    const size_t len = strlen(s);
    //短すぎる入力
    if (0 == len || (1 == len && '\n' == s[0])) return 1;
    //長過ぎる入力
    if ('\n' != s[len - 1]) {
        //入力ストリームを掃除
        while (fgetc(stream) != '\n');
        return 2;
    }
    return 0;
}
/**
 * @brief 標準入力から入力を受け、int型に変換する
 * @details fgetsしてstrtoulしている。max, minの条件に合わないかエラー時はループ
 * @details errnoの値を書き換える
 * @param message 入力を受ける前にputsに渡す文字列。表示しない場合はnullptrか空白文字のみで構成された文字列へのポインタを渡す
 * @param message_on_error エラー時に表示してループする
 * @param max 入力値を制限する。最大値を指定
 * @param min 入力値を制限する。最小値を指定
 * @return 入力した数字、EOFのときは0
 */
static inline int input_int(const char* message, const char* restrict message_on_error, const int max, const int min)
{
    if (str_has_char(message)) puts(message);
    char s[30];
    static_assert(sizeof(int) < 8, "err");
    long t = 0;
    size_t i = 0;
    for (char* endptr = s; ((0 == t && endptr == s) || 0 != errno || t < min || max < t) && i < 100u; ++i) {
        //長過ぎる入力以降の無限ループ防止にerrnoをクリアする
        errno = 0;
        switch (fgets_wrap(s, sizeof(s), stdin, message_on_error)) {
        case -1: return 0;//EOF
        case 1://短すぎる入力
        case 2://長過ぎる入力
            endptr = s;//ループ制御フラグとして流用
            continue;
        default: break;
        }
        t = strtol(s, &endptr, 10);
    }
    if (100u == i) exit(1);//無限ループ防止
    return ((int)(t));
}
/**
 * @brief 標準入力から入力を受け、unsigned int型に変換する
 * @details fgetsしてstrtodしている。max, minの条件に合わないかエラー時はループ
 * @details errnoの値を書き換える
 * @param message 入力を受ける前にputsに渡す文字列。表示しない場合はnullptrか空白文字のみで構成された文字列へのポインタを渡す
 * @param message_on_error エラー時に表示してループする
 * @param max 入力値を制限する。最大値を指定
 * @param min 入力値を制限する。最小値を指定
 * @return 入力した数字、EOFのときは0
 */
static inline unsigned int input_uint(const char* message, const char* restrict message_on_error, const unsigned int max, const unsigned int min)
{
    if (str_has_char(message)) puts(message);
    char s[30];
    static_assert(sizeof(unsigned int) < 8, "err");
    unsigned long t = 0;
    size_t i = 0;
    for (char* endptr = s; ((0 == t && endptr == s) || 0 != errno || t < min || max < t) && i < 100u; ++i) {
        //長過ぎる入力以降の無限ループ防止にerrnoをクリアする
        errno = 0;
        switch (fgets_wrap(s, sizeof(s), stdin, message_on_error)) {
        case -1: return 0;//EOF
        case 1://短すぎる入力
        case 2://長過ぎる入力
            endptr = s;//ループ制御フラグとして流用
            continue;
        default: break;
        }
        t = strtoul(s, &endptr, 10);
    }
    if (100 == i) exit(1);//無限ループ防止
    return ((unsigned int)(t));
}
/**
 * @brief 標準入力から入力を受け、double型に変換する
 * @details fgetsしてstrtolしている。max, minの条件に合わないかエラー時はループ
 * @details errnoの値を書き換える
 * @param message 入力を受ける前にputsに渡す文字列。表示しない場合はnullptrか空白文字のみで構成された文字列へのポインタを渡す
 * @param message_on_error エラー時に表示してループする
 * @param max 入力値を制限する。最大値を指定
 * @param min 入力値を制限する。最小値を指定
 * @return 入力した数字、EOFのときは0
 */
static inline double input_double(const char* message, const char* restrict message_on_error, const double max, const double min)
{
    if (str_has_char(message)) puts(message);
    char s[100];
    double t = 0;
    size_t i = 0;
    for(char* endptr = s; ((0 == t && endptr == s) || 0 != errno || t < min || max < t) && i < 100; ++i){
        //長過ぎる入力以降の無限ループ防止にerrnoをクリアする
        errno = 0;
        switch (fgets_wrap(s, sizeof(s), stdin, message_on_error)) {
        case -1: return 0;//EOF
        case 1://短すぎる入力
        case 2://長過ぎる入力
            endptr = s;//ループ制御フラグとして流用
            continue;
        default: break;
        }
        t = strtod(s, &endptr);
    }
    if (100 == i) exit(1);//無限ループ防止
    return t;
}
#ifdef NO_STDC_STATIC_ASSERT
#   undef static_assert
#   undef NO_STDC_STATIC_ASSERT
#endif

使い方

C99
int main(void)
{
    const int in = input_int("値を入力してください", NULL, INT_MAX, INT_MIN);

    //do something

    for(int tmp; tmp = input_int("値を入力してください", NULL, INT_MAX, INT_MIN) || !feof(stdin);){
        printf("input > %d", tmp);
    }
    return 0;
}

結論

C言語で標準入出力ってラスボスの一つじゃないか説

追記: strtol系関数のエラーハンドリング

C11規格書によれば
http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf

7.22.1.4 The strtol, strtoll, strtoul, and strtoull functions
Synopsis

1

#include <stdlib.h>
long int strtol(
const char * restrict nptr,
char ** restrict endptr,
int base);
long long int strtoll(
const char * restrict nptr,
char ** restrict endptr,
int base);
unsigned long int strtoul(
const char * restrict nptr,
char ** restrict endptr,
int base);
unsigned long long int strtoull(
const char * restrict nptr,
char ** restrict endptr,
int base);

7 If the subject sequence is empty or does not have the expected form, no conversion is
performed; the value of nptr is stored in the object pointed to by endptr
, provided
that endptr is not a null pointer.
Returns

8 The strtol, strtoll, strtoul, and strtoull functions return the converted
value, if any. If no conversion could be performed, zero is returned. If the correct value
is outside the range of representable values, LONG_MIN, LONG_MAX, LLONG_MIN,
LLONG_MAX, ULONG_MAX, or ULLONG_MAX is returned (according to the return type
and sign of the value, if any), and the value of the macro ERANGE is stored in errno.

となっているから、errnoの値などを確認するだけでは不充分でendptrも渡して見ないといけないのか。知らなかった。

追記: strtodのエラーハンドリング

基本的には上の通りなのだが、underflowsが起こったかをportableに知る方法はない。何故かと言うと

//§7.22.1.3 (N1570)
If the result underflows (7.12.1)
(中略)
; whether errno acquires the value ERANGE is implementation-defined.

ERANGEになるかが実装依存だからだ。

類似の記事

今更ながらネタ被りしている記事を見つけてしまった。C言語タグではなくCタグしか見てなかったから気が付かなかった・・・

標準入力から安全に文字列を受け取る方法いろいろ

まあ結果的に同じアプローチになりますよね。