【C】初めてのC言語（25. atoi関数の罠）

# はじめに

[先日に書いた記事](https://qiita.com/nkojima/items/b58678ffe42ba841f5cd)で久々にC言語に触れた際に、コメントで教えて頂いた`strtol`関数や`atoi`関数を使っていたところ、「atoi関数に整数に変換できない文字列を渡すと0が返される」ということに気が付きました。

一見すると便利に見えますが、`atoi`関数の戻り値が0だった時に「引数が"0"だった」のか「引数が整数に変換不能な値だった」のかが区別出来ないのが難点です。
そこで整数に変換出来ない入力値をエラーとして扱う方法を、初心者なりに考えてみました。

```c: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));
}
```

```text:実行結果
変換前:0-->変換後:0
変換前:test-->変換後:0
```

# 学習環境

* 今回は[paiza.io](https://paiza.io/ja/projects/new)のC言語のエディタを使いました。

# 方法1：strtol関数を使う

* [こちらのページ](https://www.jpcert.or.jp/sc-rules/c-err07-c.html)を見ると、「文字列を整数に変換する時には、エラーを出せる関数を使うのが望ましい」ということが書かれています。
  * その中では、`atoi`関数や`atol`関数の代わりに`strtol`関数を使うのが良いと書かれていました。
* `strtol`関数では、「第2引数のendptrがNULLでない場合は、最初に現れた不正な文字`strtol`関数によって *endptrに保存される」と書かれており、その通りに実装すると不正な文字が含まれるパターンを検出できました。
  * @ligun さん、ご指摘ありがとうございました。
* ただし、`strtol`関数の戻り値はlong型になる点に注意が必要です。
  * int型として使う場合はキャストなどをする必要があります。
* さらに@ligun さんと@angel_p_57 さんのこめんとを踏まえて、コードを再度修正してみました。（2023/05/29）
 * 引数にヌルバイトや「intに変換出来ない大きな値」が指定された場合でも、エラーコードで正しく変換されたかどうかが分かるようになりました。

```c: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);
    }
}
```


```text:実行結果（char* text = {'\0'};）
入力値がNULLです。
```

```text:実行結果（char* text = "12b34";）
「12b34」の「b34」が不正な文字です。
```

```text:実行結果（char* text = "11111111111";）
「11111111111」はint型の最大値より大きいです。
```

```text:実行結果（char* text = "11111111111111111111111111";）
「11111111111111111111111111」はlong型の範囲を超えています。
```

```text:実行結果（char* text = "123;）
変換前: 123 ---> 変換後(int): 123
```

# 方法2：sscanf関数を使う

* [こちらのページ](https://marycore.jp/prog/c-lang/convert-string-to-number-in-c/)では上記のstrtol関数だけでなく、sscanf関数を使った方法も紹介されていました。
* sscanf関数の戻り値は「変換が成功した回数」なので、これを使えば「整数に変換出来ない入力値」をエラーとして扱うことが出来そうです。
  * ただし、入力値が「0test」のようなケースでは最初の「0」が正しく変換されてしまうため、「戻り値が1であれば正常に変換出来た」と判断するのは早計でしょう。

```c: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);
}
```

```text:実行結果
変換前:0-->変換後:0 (整数に変換した回数:1)
変換前:test-->変換後:0 (整数に変換した回数:0)
変換前:0test-->変換後:0 (整数に変換した回数:1)
```

# 方法3：isdigit関数とsscanf関数を使う

* C標準ヘッダの`ctype.h`で定義されている`isdigit`という関数は、ある1文字が数字であるかを判定することができます。
* この`isdigit`関数を使って全ての文字が数字であるかをチェックした上で、全て数字で構成されていればsscanf関数で整数に変換するという関数（※以下の`toInteger`関数）を作ってみました。
  * もう少しスマートな方法がありそうな気もしますが、今の私に思いつくのはこの程度が限界です...

```c: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);
}
```

```text:実行結果
変換前:0-->変換後:0 (エラーコード:0)
変換前:test-->変換後:0 (エラーコード:1)
変換前:0test-->変換後:0 (エラーコード:1)
```


# 参考URL

* [INT06-C. 文字列トークンを整数に変換するには strtol() 系の関数を使う](https://www.jpcert.or.jp/sc-rules/c-int06-c.html)
* [ERR07-C. よりよいエラー検査を行える関数を使用する](https://www.jpcert.or.jp/sc-rules/c-err07-c.html)
* [STRTOL - Linux Programmer's Manual (3)](https://linuxjm.osdn.jp/html/LDP_man-pages/man3/strtol.3.html)
* [【C言語】文字列を数値に変換する方法【危険なatoi関数、厳格なstrtol関数】](https://marycore.jp/prog/c-lang/convert-string-to-number-in-c/)
* [isdigit() - 文字が数値であるかどうか判定する](https://cgengo.sakura.ne.jp/isdigit.html)