11
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

C言語で安全に数値を複数含む文字列を分解しつつ整数型に変換して読み込む方法

Last updated at Posted at 2021-08-08

はじめに

以前C言語で安全に標準入力から数値を取得するのは極めて困難であるという記事を執筆しました。

つまるところstrtol系関数で文字列から数値への変換を行う必要があるわけですが、だいぶ面倒なので特に競技プログラミングなんかの文脈ではscanfを普通に使ってしまうことと思います。

さて、競技プログラミングではよく次のような入力が与えられます。なおHWの最大値、eの最大最小値は与えられるものとします。

H W
e11 e12 ... e1W
e21 e22 ... e2W
...
eH1 eH2 ... eHW

ここでこれらの入力をscanfで捌くのはあんまり効率的ではないのではないかという疑問が出てきます。

scanf関数は書式文字列を渡してパース方法を指定します。 $H \times W$ 回scnafするということは、その分の書式文字列パース作業が加算されるということです。

この記事ではstrtokで文字列を分割してからatoiで変換するということをやっていましたが、atoiscanfと同じ問題を抱えている(安全ではない)うえに、strtokは入力文字列を書き換えるため扱いにくいわけです。

strtol系関数はパースに使えます

long int strtol(
const char * restrict nptr,
char ** restrict endptr,
int base);

strtol関数の第2引数が今回の主役です。

ここにはchar*型変数へのポインタを渡します。そしてstrtol関数は処理が終わると第1引数に与えた文字列のどこまでパースしたかを伝えるべく、パース終了地点へのポインタを第2引数経由で渡されているchar*型変数に書き込みます。例を見てみましょう(errnoはエラーハンドリングに必要ですがその処理はここでは省略します)。

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

int main(void)
{
    const char* str = "111 222";
    char* endptr;
    //errno = 0;
    const long re = strtol(str, &endptr, 10);
    assert(endptr == str + 3);
    printf(
        "re = %d\n"
        "endptr = %p\n"
        "str + 3 = %p\n"
        "endptr(rest string): \"%s\"",
        (int)re, endptr, str + 3, endptr
    );
    return 0;
}

実行例
re = 111
endptr = 0x40068b
str + 3 = 0x40068b
endptr(rest string): " 222"

strを見たとき、111の部分が整数型へ変換できる部分です。直後の が最初に現れる整数型へ変換できない部分です。この位置をポインタを使って表すとstr + 3の位置となります(&str[3]でも同じ意味です)。

strtol関数実行後のendptrはまさしくstr + 3の位置を指し示しています。

この性質を利用して文字列の分解と整数型への変換を一挙に行うことができます。

実装

というわけで取るべき戦略は、

  1. 標準入力からはfgetsを用いて行ごとに読み込む
  2. 読み込んだ文字列をいい感じにパースしていく

というわけでそのパースする関数を作りました。

# include <stdio.h>
# include <stdlib.h>
# include <string.h>
# include <errno.h>
# include <assert.h>
# include <stdbool.h>
# include <errno.h>
# include <ctype.h>
/**
 * @brief 区切り文字列によって区切られている数値文字列をパースする
 * @param dest 結果を受け取る配列へのポインタ
 * @param dest_size `dest`の配列の大きさであり、`input`から数値を抜き出す回数
 * @param input 入力文字列
 * @param endptr_out パース終了時の残りの文字列の先頭へのポインタを格納する`const char*`型変数領域へのポインタ
 * @param delims 区切り文字列
 */
bool parse_line_l(long* dest, size_t dest_size, const char* input, const char** const endptr_out, int radix, const char* delims)
{
    if (dest == NULL || input == NULL || delims == NULL) return false;
    const char* endptr = input;
    const char* s = input;
    for (size_t i = 0; i < dest_size; ++i, s = endptr) {
        errno = 0;
        const long re = strtol(s, (char**)&endptr, radix);
        if (errno != 0 || (re == 0 && s == endptr)) return false;
        while (strchr(delims, endptr[0])) ++endptr;
        if (endptr_out != NULL) *endptr_out = endptr;
        dest[i] = re;
    }
    return true;
}
int main(void)
{
    const char* input = "111 222, 333, 444";
    const char* endptr;
    long arr[3];
    if (!parse_line_l(arr, sizeof(arr)/sizeof(*arr), input, &endptr, 10, ", ")) return 1;
    printf("%ld, %ld, %ld, rest: \"%s\"", arr[0], arr[1], arr[2], endptr);
    return 0;
}

まずstrtol関数によってパースを試みます。失敗している条件はerrno0ではないもしくは戻り値が0
かつendptrが入力文字列と同じ時です。

次に区切り文字列を読み飛ばします。strchr関数は文字列の中に任意の文字を含むか調べることができますが、これを用いて入力文字列を1文字づつ読み飛ばしていきます。

11
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?