数値を文字列から安全に取り出すのは初心者がつまずきやすいポイントです。
atoiやatofでは誤入力やオーバーフローを見逃しやすく、バグの温床になります。
本記事ではstrtol
とstrtod
の正しい使い方とエラー検出の勘所を、実行例とともに丁寧に解説します。
C言語の数値変換(strtol,strtod)入門
何を解決するか
文字列から数値へ変換する場面では、以下のような問題が発生します。
入力の一部だけが数値で末尾にゴミが付いている、空文字や空白しかない、範囲外でオーバーフローする、符号や基数を間違う、といったケースです。
strtolとstrtodは、どこまで読めたかを示すポインタと、範囲エラー通知であるerrnoを用いて、これらを正確に検出できます。
これが安全な入力検証の土台になります。
atoi/atofとの違い
最大の違いはエラー検出能力です。
atoi
やatof
は失敗時の区別がつかず、末尾のゴミも無視されます。
一方strtol
とstrtod
はendptr
とerrno
で詳細に判断できます。
観点 | atoi/atof | strtol/strtod |
---|---|---|
エラー検出 | 失敗を区別できない。0との区別不能 | endptrとerrnoで詳細判定 |
末尾の未変換 | 無視して返す | endptrで未変換位置がわかる |
オーバーフロー | 未定義挙動または不明確 | errnoにERANGE、値は飽和 |
符号と空白 | 受け付ける | 受け付ける |
基数指定 | 不可 | base指定可、0で自動判別 |
特に「0の入力」と「変換失敗で0が返る」区別がつかないのが致命的です。
これがatoi/atof
を避けるべき最大理由です。
変換の流れ
基本パターンは「errno初期化→strtoX実行→endptrで未変換確認→errnoのERANGE確認→末尾にゴミがないか確認」です。
整数でも小数でも同じ流れで考えます。
- 先頭の空白は自動でスキップされます。
- 符号は省略可能で1つのみ有効です。
base
が0なら0
始まりは8進、0x
/0X
は16進、それ以外は10進が選ばれます。errno == ERANGE
なら範囲外です。strtol
はLONG_MAX
/LONG_MIN
に張り付きます。strtod
はHUGE_VAL
などになります。
エラー処理の要点
endptrで未変換をチェック
endptrが入力先頭と同じなら、数字として解釈できた文字が1つもないことを意味します。
さらに、最後まで読めているかを確認すると「末尾のゴミ」を見逃しません。
/* endptrの使い方デモ: 未変換や末尾のゴミを検出する */
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <ctype.h>
static void demo(const char *s) {
errno = 0;
char *end = NULL;
long v = strtol(s, &end, 10); /* 10進として解釈 */
printf("input=\"%s\"\n", s);
if (end == s) {
/* 1文字も数字として認識されなかった */
printf(" -> no digits found\n\n");
return;
}
printf(" value=%ld, stopped at index %ld\n", v, (long)(end - s));
/* 末尾に空白以外の文字が残っていないかを確認 */
while (*end != '/* endptrの使い方デモ: 未変換や末尾のゴミを検出する */
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <ctype.h>
static void demo(const char *s) {
errno = 0;
char *end = NULL;
long v = strtol(s, &end, 10); /* 10進として解釈 */
printf("input=\"%s\"\n", s);
if (end == s) {
/* 1文字も数字として認識されなかった */
printf(" -> no digits found\n\n");
return;
}
printf(" value=%ld, stopped at index %ld\n", v, (long)(end - s));
/* 末尾に空白以外の文字が残っていないかを確認 */
while (*end != '\0' && isspace((unsigned char)*end)) {
end++;
}
if (*end == '\0') {
printf(" -> OK: only whitespace remained after number\n\n");
} else {
printf(" -> NG: leftover=\"%s\"\n\n", end);
}
}
int main(void) {
demo("123abc");
demo("abc");
demo(" 42 ");
return 0;
}
' && isspace((unsigned char)*end)) {
end++;
}
if (*end == '/* endptrの使い方デモ: 未変換や末尾のゴミを検出する */
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <ctype.h>
static void demo(const char *s) {
errno = 0;
char *end = NULL;
long v = strtol(s, &end, 10); /* 10進として解釈 */
printf("input=\"%s\"\n", s);
if (end == s) {
/* 1文字も数字として認識されなかった */
printf(" -> no digits found\n\n");
return;
}
printf(" value=%ld, stopped at index %ld\n", v, (long)(end - s));
/* 末尾に空白以外の文字が残っていないかを確認 */
while (*end != '\0' && isspace((unsigned char)*end)) {
end++;
}
if (*end == '\0') {
printf(" -> OK: only whitespace remained after number\n\n");
} else {
printf(" -> NG: leftover=\"%s\"\n\n", end);
}
}
int main(void) {
demo("123abc");
demo("abc");
demo(" 42 ");
return 0;
}
') {
printf(" -> OK: only whitespace remained after number\n\n");
} else {
printf(" -> NG: leftover=\"%s\"\n\n", end);
}
}
int main(void) {
demo("123abc");
demo("abc");
demo(" 42 ");
return 0;
}
input="123abc"
value=123, stopped at index 3
-> NG: leftover="abc"
input="abc"
-> no digits found
input=" 42 "
value=42, stopped at index 6
-> OK: only whitespace remained after number
実務では「最後まで読めたか」を必ず確認し、途中までしか読めない入力はエラー扱いにするのが安全です。
errnoとERANGEを確認
呼び出し前にerrno = 0
を必ず行い、呼び出し後にerrno == ERANGE
かを確認します。
範囲外の数値でオーバーフローやアンダーフローが発生した場合に検知できます。
/* 範囲エラー検出: strtol と strtod の ERANGE */
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <limits.h>
#include <math.h>
int main(void) {
const char *s1 = "999999999999999999999999999999";
errno = 0;
char *end = NULL;
long v = strtol(s1, &end, 10);
printf("strtol overflow: input=%s\n", s1);
printf(" endptr advanced? %s\n", end != s1 ? "yes" : "no");
if (errno == ERANGE) {
printf(" errno=ERANGE, value=%ld (clamped to LONG_MAX or LONG_MIN)\n", v);
} else {
printf(" errno=%d, value=%ld\n", errno, v);
}
errno = 0;
const char *s2 = "1e309"; /* 2進64bitの倍精度でオーバーフロー */
double d = strtod(s2, &end);
printf("\nstrtod overflow: input=%s\n", s2);
printf(" endptr advanced? %s\n", end != s2 ? "yes" : "no");
if (errno == ERANGE) {
printf(" errno=ERANGE, value=%f (HUGE_VAL with sign)\n", d);
} else {
printf(" errno=%d, value=%f\n", errno, d);
}
return 0;
}
strtol overflow: input=999999999999999999999999999999
endptr advanced? yes
errno=ERANGE, value=9223372036854775807 (clamped to LONG_MAX or LONG_MIN)
strtod overflow: input=1e309
endptr advanced? yes
errno=ERANGE, value=inf (HUGE_VAL with sign)
strtodのアンダーフローではerrno==ERANGE
かつ非常に小さい数、または0.0
が返ることがあります。
空文字・数字なしの判定
空白は自動でスキップされますが、空文字のみや、符号のみは「数字なし」となりendptr == nptr
になります。
これは入力エラーとみなすべきです。
/* 空文字・数字なしの判定例 */
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
static void check(const char *s) {
errno = 0;
char *end = NULL;
long v = strtol(s, &end, 10);
printf("input=\"%s\": ", s);
if (end == s) {
printf("no conversion\n");
} else {
printf("converted=%ld\n", v);
}
}
int main(void) {
check("");
check(" "); /* 空白のみ */
check("+"); /* 符号のみ */
check("-"); /* 符号のみ */
check("+ 1"); /* 符号の直後が数字でない */
return 0;
}
input="": no conversion
input=" ": no conversion
input="+": no conversion
input="-": no conversion
input="+ 1": no conversion
符号(+/-)と空白の扱い
strtol
やstrtod
は先頭の空白を読み飛ばし、+
または-
の1文字の符号を受け付けます。
符号の直後には数字が必要で、そうでない場合は「数字なし」と判断されます。
複数の符号や途中の空白を許容しません。
範囲外とオーバーフロー
範囲外の検出はerrno==ERANGE
で行い、整数ではLONG_MAX
/LONG_MIN
へ張り付きます。
浮動小数ではHUGE_VAL
や0.0
が返る可能性があります。
末尾に未変換文字が残るケースは別問題で、endptr
の確認で対処します。
状況 | 確認すべきこと | 代表的な戻り値 |
---|---|---|
数字がない | endptr == nptr | 値は未定義に頼らずエラー扱い |
途中までしか読めない | *endptr != '\0' のまま | 部分変換の値だがエラー扱いが安全 |
整数のオーバーフロー | errno == ERANGE | LONG_MAX またはLONG_MIN |
浮動小数のオーバーフロー | errno == ERANGE | HUGE_VAL の符号付き |
浮動小数のアンダーフロー | errno == ERANGE | 非常に小さい数または0.0 |
ポイントは「範囲エラー」と「未変換の残り」を別々にチェックすることです。
使い方の基本コード
整数の変換
厳密に整数へ変換するユーティリティ関数を用意すると、呼び出し元が簡潔になります。
/* 文字列を厳密にlongへ変換: 末尾のゴミを許さず、範囲エラーも検出 */
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <ctype.h>
#include <stdbool.h>
bool parse_long_strict(const char *s, int base, long *out) {
if (s == NULL || out == NULL) return false;
errno = 0;
char *end = NULL;
long v = strtol(s, &end, base);
/* 少なくとも1文字は変換できたか */
if (end == s) return false;
/* 範囲エラーか */
if (errno == ERANGE) return false;
/* 末尾は空白のみ許容する例 */
while (*end != '/* 文字列を厳密にlongへ変換: 末尾のゴミを許さず、範囲エラーも検出 */
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <ctype.h>
#include <stdbool.h>
bool parse_long_strict(const char *s, int base, long *out) {
if (s == NULL || out == NULL) return false;
errno = 0;
char *end = NULL;
long v = strtol(s, &end, base);
/* 少なくとも1文字は変換できたか */
if (end == s) return false;
/* 範囲エラーか */
if (errno == ERANGE) return false;
/* 末尾は空白のみ許容する例 */
while (*end != '\0' && isspace((unsigned char)*end)) end++;
if (*end != '\0') return false;
*out = v;
return true;
}
int main(void) {
const char *tests[] = { "42", " -17", "123abc", "99999999999999999999", "0x10", NULL };
for (int i = 0; tests[i]; ++i) {
long v = 0;
bool ok = parse_long_strict(tests[i], 10, &v); /* ここでは10進固定 */
printf("input=%-24s -> %s", tests[i], ok ? "OK" : "NG");
if (ok) printf(", value=%ld", v);
printf("\n");
}
return 0;
}
' && isspace((unsigned char)*end)) end++;
if (*end != '/* 文字列を厳密にlongへ変換: 末尾のゴミを許さず、範囲エラーも検出 */
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <ctype.h>
#include <stdbool.h>
bool parse_long_strict(const char *s, int base, long *out) {
if (s == NULL || out == NULL) return false;
errno = 0;
char *end = NULL;
long v = strtol(s, &end, base);
/* 少なくとも1文字は変換できたか */
if (end == s) return false;
/* 範囲エラーか */
if (errno == ERANGE) return false;
/* 末尾は空白のみ許容する例 */
while (*end != '\0' && isspace((unsigned char)*end)) end++;
if (*end != '\0') return false;
*out = v;
return true;
}
int main(void) {
const char *tests[] = { "42", " -17", "123abc", "99999999999999999999", "0x10", NULL };
for (int i = 0; tests[i]; ++i) {
long v = 0;
bool ok = parse_long_strict(tests[i], 10, &v); /* ここでは10進固定 */
printf("input=%-24s -> %s", tests[i], ok ? "OK" : "NG");
if (ok) printf(", value=%ld", v);
printf("\n");
}
return 0;
}
') return false;
*out = v;
return true;
}
int main(void) {
const char *tests[] = { "42", " -17", "123abc", "99999999999999999999", "0x10", NULL };
for (int i = 0; tests[i]; ++i) {
long v = 0;
bool ok = parse_long_strict(tests[i], 10, &v); /* ここでは10進固定 */
printf("input=%-24s -> %s", tests[i], ok ? "OK" : "NG");
if (ok) printf(", value=%ld", v);
printf("\n");
}
return 0;
}
input=42 -> OK, value=42
input= -17 -> OK, value=-17
input=123abc -> NG
input=99999999999999999999 -> NG
input=0x10 -> NG
この例は10進のみ許容する方針です。
16進や8進も受け付けたい場合はbase=0を使うか、次節のように明示の基数を指定します。
基数(base)の指定
base
を使うと、2進〜36進まで対応できます。
base=0
では自動判別になりますが、先頭0による8進解釈を避けたい場合は10を指定しましょう。
/* 基数指定の例: base=0の自動判別と明示的な基数 */
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
static void print_as(const char *s, int base) {
errno = 0;
char *end = NULL;
long v = strtol(s, &end, base);
printf("str=\"%s\", base=%d -> value=%ld, endptr_advanced=%s, errno=%d\n",
s, base, v, end != s ? "yes" : "no", errno);
}
int main(void) {
print_as("0x1f", 0); /* 16進として31 */
print_as("077", 0); /* 8進として63 */
print_as("1011", 2); /* 2進として11 */
print_as("1f", 16); /* 16進として31 */
print_as("077", 10); /* 10進固定なら77として解釈 */
return 0;
}
str="0x1f", base=0 -> value=31, endptr_advanced=yes, errno=0
str="077", base=0 -> value=63, endptr_advanced=yes, errno=0
str="1011", base=2 -> value=11, endptr_advanced=yes, errno=0
str="1f", base=16 -> value=31, endptr_advanced=yes, errno=0
str="077", base=10 -> value=77, endptr_advanced=yes, errno=0
先頭0で8進になる罠があるため、ユーザー入力を想定するなら10進固定が安全です。
小数の変換
浮動小数はstrtod
を用います。
INFやNANを許可するかは要件に応じて決めてください。
/* 文字列を厳密にdoubleへ変換: INF/NANの許容を切り替え可能 */
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <ctype.h>
#include <stdbool.h>
#include <math.h>
bool parse_double_strict(const char *s, bool allow_inf_nan, double *out) {
if (s == NULL || out == NULL) return false;
errno = 0;
char *end = NULL;
double v = strtod(s, &end);
if (end == s) return false; /* 数字なし */
if (errno == ERANGE) return false; /* 範囲エラー */
/* 末尾空白のみ許容 */
while (*end != '/* 文字列を厳密にdoubleへ変換: INF/NANの許容を切り替え可能 */
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <ctype.h>
#include <stdbool.h>
#include <math.h>
bool parse_double_strict(const char *s, bool allow_inf_nan, double *out) {
if (s == NULL || out == NULL) return false;
errno = 0;
char *end = NULL;
double v = strtod(s, &end);
if (end == s) return false; /* 数字なし */
if (errno == ERANGE) return false; /* 範囲エラー */
/* 末尾空白のみ許容 */
while (*end != '\0' && isspace((unsigned char)*end)) end++;
if (*end != '\0') return false;
/* INF/NANの可否 */
if (!allow_inf_nan) {
if (!isfinite(v)) return false; /* inf, -inf, nan を拒否 */
}
*out = v;
return true;
}
int main(void) {
const char *tests[] = { "3.14159", "1.2e-3", "nan", "INF", "1,23", " -0.0 ", NULL };
for (int i = 0; tests[i]; ++i) {
double d = 0.0;
bool ok = parse_double_strict(tests[i], false, &d); /* INF/NANを拒否する設定 */
printf("input=%-10s -> %s", tests[i], ok ? "OK" : "NG");
if (ok) printf(", value=%g", d);
printf("\n");
}
return 0;
}
' && isspace((unsigned char)*end)) end++;
if (*end != '/* 文字列を厳密にdoubleへ変換: INF/NANの許容を切り替え可能 */
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <ctype.h>
#include <stdbool.h>
#include <math.h>
bool parse_double_strict(const char *s, bool allow_inf_nan, double *out) {
if (s == NULL || out == NULL) return false;
errno = 0;
char *end = NULL;
double v = strtod(s, &end);
if (end == s) return false; /* 数字なし */
if (errno == ERANGE) return false; /* 範囲エラー */
/* 末尾空白のみ許容 */
while (*end != '\0' && isspace((unsigned char)*end)) end++;
if (*end != '\0') return false;
/* INF/NANの可否 */
if (!allow_inf_nan) {
if (!isfinite(v)) return false; /* inf, -inf, nan を拒否 */
}
*out = v;
return true;
}
int main(void) {
const char *tests[] = { "3.14159", "1.2e-3", "nan", "INF", "1,23", " -0.0 ", NULL };
for (int i = 0; tests[i]; ++i) {
double d = 0.0;
bool ok = parse_double_strict(tests[i], false, &d); /* INF/NANを拒否する設定 */
printf("input=%-10s -> %s", tests[i], ok ? "OK" : "NG");
if (ok) printf(", value=%g", d);
printf("\n");
}
return 0;
}
') return false;
/* INF/NANの可否 */
if (!allow_inf_nan) {
if (!isfinite(v)) return false; /* inf, -inf, nan を拒否 */
}
*out = v;
return true;
}
int main(void) {
const char *tests[] = { "3.14159", "1.2e-3", "nan", "INF", "1,23", " -0.0 ", NULL };
for (int i = 0; tests[i]; ++i) {
double d = 0.0;
bool ok = parse_double_strict(tests[i], false, &d); /* INF/NANを拒否する設定 */
printf("input=%-10s -> %s", tests[i], ok ? "OK" : "NG");
if (ok) printf(", value=%g", d);
printf("\n");
}
return 0;
}
input=3.14159 -> OK, value=3.14159
input=1.2e-3 -> OK, value=0.0012
input=nan -> NG
input=INF -> NG
input=1,23 -> NG
input= -0.0 -> OK, value=-0
小数点の記号はロケールに依存します。
通常のCロケールでは.
ですが、地域によっては,
となることがあります。
ロケール依存を避けるにはsetlocale
で"C"
に固定するか、プラットフォームに応じてstrtod_l
などを検討します。
エラー時の戻り値と処理
「失敗なら既定値を使う」というポリシーを関数に閉じ込めると呼び出し側が簡潔になります。
ただしログやメッセージで失敗を必ず記録しましょう。
/* 失敗時にデフォルト値を返し、理由をstderrへ出す */
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <ctype.h>
#include <stdbool.h>
long str_to_long_or_default(const char *s, int base, long def) {
if (s == NULL) {
fprintf(stderr, "[parse] NULL input, use default=%ld\n", def);
return def;
}
errno = 0;
char *end = NULL;
long v = strtol(s, &end, base);
if (end == s) {
fprintf(stderr, "[parse] no digits: \"%s\", use default=%ld\n", s, def);
return def;
}
if (errno == ERANGE) {
fprintf(stderr, "[parse] ERANGE: \"%s\", use default=%ld\n", s, def);
return def;
}
while (*end && isspace((unsigned char)*end)) end++;
if (*end != '/* 失敗時にデフォルト値を返し、理由をstderrへ出す */
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <ctype.h>
#include <stdbool.h>
long str_to_long_or_default(const char *s, int base, long def) {
if (s == NULL) {
fprintf(stderr, "[parse] NULL input, use default=%ld\n", def);
return def;
}
errno = 0;
char *end = NULL;
long v = strtol(s, &end, base);
if (end == s) {
fprintf(stderr, "[parse] no digits: \"%s\", use default=%ld\n", s, def);
return def;
}
if (errno == ERANGE) {
fprintf(stderr, "[parse] ERANGE: \"%s\", use default=%ld\n", s, def);
return def;
}
while (*end && isspace((unsigned char)*end)) end++;
if (*end != '\0') {
fprintf(stderr, "[parse] trailing chars: \"%s\", use default=%ld\n", s, def);
return def;
}
return v;
}
int main(void) {
long a = str_to_long_or_default("123", 10, -1);
long b = str_to_long_or_default("123abc", 10, -1);
printf("a=%ld, b=%ld\n", a, b);
return 0;
}
') {
fprintf(stderr, "[parse] trailing chars: \"%s\", use default=%ld\n", s, def);
return def;
}
return v;
}
int main(void) {
long a = str_to_long_or_default("123", 10, -1);
long b = str_to_long_or_default("123abc", 10, -1);
printf("a=%ld, b=%ld\n", a, b);
return 0;
}
[parse] trailing chars: "abc", use default=-1
a=123, b=-1
安全なデフォルト値は、後続の処理で不正状態に気づける値にしましょう。
例えばIDなら-1
、サイズなら0
など、意味的に区別できる値が有効です。
ユーザー入力の検証
ユーザー入力はfgetsで1行単位に読み取り、末尾の改行を取り除いた上で厳密変換するのが定石です。
以下は関数化した例です。
/* 1行の文字列を厳密にlongへ検証する例: fgetsで読んだ後に使う */
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <ctype.h>
#include <stdbool.h>
#include <string.h>
bool validate_int_line(const char *line, long *out) {
if (line == NULL || out == NULL) return false;
/* 改行の除去 */
size_t len = strcspn(line, "\r\n");
char buf[256];
if (len >= sizeof(buf)) return false; /* 長すぎる行は拒否 */
memcpy(buf, line, len);
buf[len] = '/* 1行の文字列を厳密にlongへ検証する例: fgetsで読んだ後に使う */
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <ctype.h>
#include <stdbool.h>
#include <string.h>
bool validate_int_line(const char *line, long *out) {
if (line == NULL || out == NULL) return false;
/* 改行の除去 */
size_t len = strcspn(line, "\r\n");
char buf[256];
if (len >= sizeof(buf)) return false; /* 長すぎる行は拒否 */
memcpy(buf, line, len);
buf[len] = '\0';
errno = 0;
char *end = NULL;
long v = strtol(buf, &end, 10);
if (end == buf) return false;
if (errno == ERANGE) return false;
while (*end && isspace((unsigned char)*end)) end++;
if (*end != '\0') return false;
*out = v;
return true;
}
/* 実運用では:
char line[256];
if (fgets(line, sizeof(line), stdin) && validate_int_line(line, &value)) { ... } */
';
errno = 0;
char *end = NULL;
long v = strtol(buf, &end, 10);
if (end == buf) return false;
if (errno == ERANGE) return false;
while (*end && isspace((unsigned char)*end)) end++;
if (*end != '/* 1行の文字列を厳密にlongへ検証する例: fgetsで読んだ後に使う */
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <ctype.h>
#include <stdbool.h>
#include <string.h>
bool validate_int_line(const char *line, long *out) {
if (line == NULL || out == NULL) return false;
/* 改行の除去 */
size_t len = strcspn(line, "\r\n");
char buf[256];
if (len >= sizeof(buf)) return false; /* 長すぎる行は拒否 */
memcpy(buf, line, len);
buf[len] = '\0';
errno = 0;
char *end = NULL;
long v = strtol(buf, &end, 10);
if (end == buf) return false;
if (errno == ERANGE) return false;
while (*end && isspace((unsigned char)*end)) end++;
if (*end != '\0') return false;
*out = v;
return true;
}
/* 実運用では:
char line[256];
if (fgets(line, sizeof(line), stdin) && validate_int_line(line, &value)) { ... } */
') return false;
*out = v;
return true;
}
/* 実運用では:
char line[256];
if (fgets(line, sizeof(line), stdin) && validate_int_line(line, &value)) { ... } */
この関数は「長すぎる行」「数字なし」「範囲外」「末尾のゴミ」を全て排除します。
不正な入力を上流で止めることで、後段のロジックが単純化します。
初心者が避けたい落とし穴とコツ
atoi/atofを使わない理由
atoi/atofはエラーを報告しません。
たとえば"abc"
も"0"
もatoi
は0を返すため、失敗と正当な0の区別ができません。
#include <stdio.h>
#include <stdlib.h>
int main(void){
printf("atoi(\"abc\")=%d, atoi(\"0\")=%d\n", atoi("abc"), atoi("0"));
return 0;
}
atoi("abc")=0, atoi("0")=0
結果だけで失敗を判定できない関数は入力検証に使うべきではありません。
endptrをNULLにしない
endptr
をNULL
で呼ぶと未変換の残りを検出できません。
常にendptrを受け取り、どこまで読めたかを調べる習慣をつけましょう。
LONG_MAX/LONG_MINに注意
longのサイズは環境依存です。
Linuxの多くでは64bit、Windowsでは32bitが一般的です。
64bit整数が必要ならstrtoll
とlong long
、LLONG_MAX
/LLONG_MIN
の利用を検討してください。
末尾のゴミを見逃さない
「123abc」のような入力を受け入れてしまうと、意図しない挙動に繋がります。
endptr
で末尾まで完全に消費できたか、空白以外の文字が残っていないかを必ず確認しましょう。
安全なデフォルト値
失敗時に返す既定値は、後続で異常を確実に検知できるものを選びます。
IDやインデックスなら-1
、サイズなら0
などが一般的です。
ログ出力と併用することで原因追跡が容易になります。
まとめ
文字列→数値変換を安全に行うには、strtol/strtodの「endptr」と「errno」を正しく使い、末尾のゴミと範囲エラーを確実に検出することが肝心です。
初心者のうちはatoi/atofを使用しないと決めてしまうのが最も安全です。
以下のポイントを常に意識してください。
- 呼び出し前に
errno = 0
、呼び出し後にerrno == ERANGE
を確認する。 endptr
で「数字なし」かどうかと、末尾の未変換文字の有無を確認する。- 必要に応じて
base
を指定し、10進固定ならbase=10
を使う。 - 浮動小数では
INF/NAN
の扱いとロケール依存性に注意する。
最後に、厳密変換ラッパー関数を自前で用意しておくと、プログラム全体の安全性と可読性が飛躍的に高まります。
今日紹介したパターンをテンプレート化し、入力検証の基盤として活用してください。