0
0

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 1 year has passed since last update.

【C】初めてのC言語(25. atoi関数の罠)

Last updated at Posted at 2023-04-30

はじめに

先日に書いた記事で久々に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)

参考URL

0
0
6

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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?