はじめに
以前C言語で安全に標準入力から数値を取得するのは極めて困難であるという記事を執筆しました。
つまるところstrtol
系関数で文字列から数値への変換を行う必要があるわけですが、だいぶ面倒なので特に競技プログラミングなんかの文脈ではscanf
を普通に使ってしまうことと思います。
さて、競技プログラミングではよく次のような入力が与えられます。なおH
とW
の最大値、e
の最大最小値は与えられるものとします。
H W
e11 e12 ... e1W
e21 e22 ... e2W
...
eH1 eH2 ... eHW
ここでこれらの入力をscanf
で捌くのはあんまり効率的ではないのではないかという疑問が出てきます。
scanf関数は書式文字列を渡してパース方法を指定します。 $H \times W$ 回scnafするということは、その分の書式文字列パース作業が加算されるということです。
この記事ではstrtok
で文字列を分割してからatoi
で変換するということをやっていましたが、atoi
はscanf
と同じ問題を抱えている(安全ではない)うえに、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
の位置を指し示しています。
この性質を利用して文字列の分解と整数型への変換を一挙に行うことができます。
実装
というわけで取るべき戦略は、
- 標準入力からは
fgets
を用いて行ごとに読み込む - 読み込んだ文字列をいい感じにパースしていく
というわけでそのパースする関数を作りました。
# 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関数によってパースを試みます。失敗している条件はerrno
が0
ではないもしくは戻り値が0
で
かつendptr
が入力文字列と同じ時です。
次に区切り文字列を読み飛ばします。strchr
関数は文字列の中に任意の文字を含むか調べることができますが、これを用いて入力文字列を1文字づつ読み飛ばしていきます。