はじめに
先日に書いた記事で久々にC言語に触れた際に、コメントで教えて頂いたstrtol
関数やatoi
関数を使っていたところ、「atoi関数に整数に変換できない文字列を渡すと0が返される」ということに気が付きました。
一見すると便利に見えますが、atoi
関数の戻り値が0だった時に「引数が"0"だった」のか「引数が整数に変換不能な値だった」のかが区別出来ないのが難点です。
そこで整数に変換出来ない入力値をエラーとして扱う方法を、初心者なりに考えてみました。
atoi関数を使った処理
#include <stdio.h>
#include <stdlib.h>
int main(void){
const char* zero = "0";
const char* test = "test";
printf("変換前:%s-->変換後:%d\n", zero, atoi(zero));
printf("変換前:%s-->変換後:%d\n", test, atoi(test));
}
実行結果
変換前:0-->変換後:0
変換前:test-->変換後:0
学習環境
- 今回はpaiza.ioのC言語のエディタを使いました。
方法1:strtol関数を使う
-
こちらのページを見ると、「文字列を整数に変換する時には、エラーを出せる関数を使うのが望ましい」ということが書かれています。
- その中では、
atoi
関数やatol
関数の代わりにstrtol
関数を使うのが良いと書かれていました。
- その中では、
-
strtol
関数では、「第2引数のendptrがNULLでない場合は、最初に現れた不正な文字strtol
関数によって *endptrに保存される」と書かれており、その通りに実装すると不正な文字が含まれるパターンを検出できました。- @ligun さん、ご指摘ありがとうございました。
- ただし、
strtol
関数の戻り値はlong型になる点に注意が必要です。- int型として使う場合はキャストなどをする必要があります。
- さらに@ligun さんと@angel_p_57 さんのこめんとを踏まえて、コードを再度修正してみました。(2023/05/29)
- 引数にヌルバイトや「intに変換出来ない大きな値」が指定された場合でも、エラーコードで正しく変換されたかどうかが分かるようになりました。
strtol関数を使った処理
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <limits.h>
/**
* "123"のような文字列をint型に変換する。
* text: 変換対象の文字列。
* int_num: int型に変換された整数値。
* return: エラーコード。
*/
int toInt(const char* text, int *int_num) {
char* endptr;
if (text == NULL) {
fprintf(stderr, "入力値がNULLです。");
return EXIT_FAILURE;
}
const long long_num = strtol(text, &endptr, 10);
if (endptr == text) {
fprintf(stderr, "「%s」は整数値ではありません。\n", text);
}
else if (*endptr != '\0') {
fprintf(stderr, "「%s」の「%s」が不正な文字です。", text, endptr);
}
else if ((LONG_MIN == long_num || LONG_MAX == long_num) && ERANGE == errno) {
fprintf(stderr, "「%s」はlong型の範囲を超えています。\n", text);
}
else if (long_num > INT_MAX) {
fprintf(stderr, "「%ld」はint型の最大値より大きいです。\n", long_num);
}
else if (long_num < INT_MIN) {
fprintf(stderr, "「%ld」はint型の最小値未満です。\n", long_num);
}
else {
*int_num = (int)long_num;
return EXIT_SUCCESS;
}
return EXIT_FAILURE;
}
int main(void){
int num = 0;
char* text = "";
errno = 0;
int status = toInt(text, &num);
if (status==0) {
printf("変換前: %s ---> 変換後(int): %d\n", text, num);
} else {
printf("%sをint型に変換できませんでした。\n", text);
}
}
実行結果(char* text = {'\0'};)
入力値がNULLです。
実行結果(char* text = "12b34";)
「12b34」の「b34」が不正な文字です。
実行結果(char* text = "11111111111";)
「11111111111」はint型の最大値より大きいです。
実行結果(char* text = "11111111111111111111111111";)
「11111111111111111111111111」はlong型の範囲を超えています。
実行結果(char* text = "123;)
変換前: 123 ---> 変換後(int): 123
方法2:sscanf関数を使う
- こちらのページでは上記のstrtol関数だけでなく、sscanf関数を使った方法も紹介されていました。
- sscanf関数の戻り値は「変換が成功した回数」なので、これを使えば「整数に変換出来ない入力値」をエラーとして扱うことが出来そうです。
- ただし、入力値が「0test」のようなケースでは最初の「0」が正しく変換されてしまうため、「戻り値が1であれば正常に変換出来た」と判断するのは早計でしょう。
sscanf関数を使った処理
#include <stdio.h>
int main(void){
const char* zero = "0";
const char* test = "test";
const char* test0 = "0test";
const char* format = "%d";
int i;
int n = sscanf(zero, format, &i);
printf("変換前:%s-->変換後:%d (整数に変換した回数:%d)\n", zero, i, n);
n = sscanf(test, format, &i);
printf("変換前:%s-->変換後:%d (整数に変換した回数:%d)\n", test, i, n);
n = sscanf(test0, format, &i);
printf("変換前:%s-->変換後:%d (整数に変換した回数:%d)\n", test0, i, n);
}
実行結果
変換前:0-->変換後:0 (整数に変換した回数:1)
変換前:test-->変換後:0 (整数に変換した回数:0)
変換前:0test-->変換後:0 (整数に変換した回数:1)
方法3:isdigit関数とsscanf関数を使う
- C標準ヘッダの
ctype.h
で定義されているisdigit
という関数は、ある1文字が数字であるかを判定することができます。 - この
isdigit
関数を使って全ての文字が数字であるかをチェックした上で、全て数字で構成されていればsscanf関数で整数に変換するという関数(※以下のtoInteger
関数)を作ってみました。- もう少しスマートな方法がありそうな気もしますが、今の私に思いつくのはこの程度が限界です...
isdigit関数とsscanf関数を使った処理
#include <stdio.h>
#include <ctype.h>
#include <errno.h>
/*
* 文字列をint型に変換する。
* 数値以外の文字を含む場合は変換後の値が0となるが、合わせてerrno==1を返す。
*/
int toInteger(char* str) {
int isNumber = 0;
// 文字列中に整数に変換出来ないものがあるかを調べる。
for (int i=0; i<strlen(str); i++) {
if (!isdigit(str[i])) {
isNumber = -1;
break;
}
}
// 全て整数に変換出来る文字であれば、sscanf関数でint型に変換する。
if (isNumber==0) {
int result;
sscanf(str, "%d", &result);
errno = 0;
return result;
} else {
errno = 1;
return 0;
}
}
int main(void){
const char* zero = "0";
const char* test = "test";
const char* test0 = "0test";
errno = 0;
int i = toInteger(zero);
printf("変換前:%s-->変換後:%d (エラーコード:%d)\n", zero, i, errno);
errno = 0;
i = toInteger(test);
printf("変換前:%s-->変換後:%d (エラーコード:%d)\n", test, i, errno);
errno = 0;
i = toInteger(test0);
printf("変換前:%s-->変換後:%d (エラーコード:%d)\n", test0, i, errno);
}
実行結果
変換前:0-->変換後:0 (エラーコード:0)
変換前:test-->変換後:0 (エラーコード:1)
変換前:0test-->変換後:0 (エラーコード:1)