atoi(c89)

C89中的说明

头文件

stdlib.h

函数原型

int atoi(const char *nptr);

nptr: 指向待转换的字符串的指针

返回值

字串的整型形式, 必须以NULL结尾.

说明

atoi函数跳过字串开头的所有空白字符, 转换接下来的数字字符, 遇到第一个非数字字符停止

c11中的相关说明

C11的标准中关于atoi的描述为:

当遇到错误时, atoi不需要(need not)改变errno的值, 当值的结果无法表示时, 行为是未定义的.

除了错误处理外, 它等价于(equivalent): (int)strtol(nptr, (char **)NULL, 10)

关于strtol函数, 我会单独介绍(文章的占位符在这里).

我的实现

第一次尝试: 菜鸟版

实现"字符串转换成整数"函数, 始于庞果网上的一道题. 题目并没有太多的文字描述, 不过它给了一系列输入及预期的输出(筛选其中的一部分), 条件是在32位的系统上:

输入             输出
""               0
"1"              1
"-1"             -1
"123"            123
"-123"           -123
"010"            10
"+00131204"      131204
"2147483647"     2147483647
"2147483648"     2147483647
"-2147483648"    -2147483648
"-2147483649"    -2147483648
"23a8f"          23
"  +4488 "       4488
"abc"            0
" - 321"         0
" ++1"           0

从以上数据, 可以分析出以下几点:

1. 空字串("")或非法字串(如"abc"), 输出0;

2. 可以有符号(如"123"), 也可以没有(如"-123"), 如果有符号, 则符号后面必须是数字, 符号与数字之间不能有空格;

3. 开头的空格将被过滤, 末尾的空格也不会管;

4. 数字前面的字符'0'将被过滤(如"010");

5. 如果超过最大值(如"2147483648"), 则输出最大值2147483647(32位的最大int值); 如果超过最小值(如"-2147483649"), 则输出最小值-2147483648(32位的最小int值);

个人觉得以上的例子不太充分, 于是举出另一批例子对gcc的atoi函数进行测试, 以下为结果:

输入                      输出
"\n\r\t\v 123\n\r\t\v "  123
"0+123"                  0

于是可以对上面的第3条和第4条进行补充:

3.1 不只是空格, 开头和末尾的所有空白字符(isspace)都将被过滤.

4.1 开头的字符'0'并不会被当作空白字符那样被过滤. 可以想像到, 读到任意数字(如这里的'0')之后的非数字(如这里的'+'), 都将停止.

以下是当时提交的代码:

//atoi version1
int StrToInt(const char* str)
{
    static const int MAX = (int)((unsigned)~0 >> 1);
    static const int MIN = -(int)((unsigned)~0 >> 1) - 1;
    unsigned int n = 0;
    int sign = 1;

    while (isspace(*str))
        ++str;
    if (*str == '+' || *str == '-')
    {
        if (*str == '-')
            sign = -1;
        ++str;
    }
    while (isdigit(*str))
    {
        n = n * 10 + (*str-'0');
        ++str;
    }
    if (sign > 0 && (unsigned)n > (unsigned)MAX)
    {
        n = MAX;
    }
    else if (sign < 0)
    {
        if ((unsigned)n > (unsigned)MIN)
            n = MIN;
        else
            n = -n;
    }
    return n;
}

测试结果:

input                      output
"1"                        1
"-1"                       -1
"123"                      123
"-123"                     -123
"010"                      10
"+00131204"                131204
"2147483647"               2147483647
"2147483648"               2147483647
"-2147483648"              -2147483648
"-2147483649"              -2147483648
"23a8f"                    23
"  +4488 "                 4488
"abc"                      0
" - 321"                   0
" ++1"                     0
"\n\r\t\v 123\n\r\t\v "    0

貌似测试结果没有啥错误(最后一行输出为0呀! fixme!). 但是如果一个很大的数字让语句"n = n * 10 + (*str-'0');"溢出, 会怎么样呢?

input             output
"10522545459"     1932610867
"-10522545459"    -1932610867

bingo! 读完倒数第二个数字5后, 还没有溢出, n的值为1052254545(0x3EB8 2151), 但是乘以10再加9呢? 溢出了(0x2 7331 4D2A), n的值为1932610858(丢掉溢出的高位后变成:0x7331 4D2A), 1932610858是小于MAX的, 程序认为没有溢出.

联想到另一个问题: 判断两个正整数相加是否溢出, 一般可以用以下的方法(假设a和b是int类型变量):

if ((unsigned)a + (unsigned)b > INT_MAX)
    complain();

如果将加法转换成减法, 可以不用将a和b转换成unsigned:

if (a > INT_MAX - b)
    complain();

那么, 这里的乘法是否也可以用相同的方法进行改进呢? 请君思考.

第二次尝试: 提高溢出处理的健壮性,除法代替乘法

在拜读了该网站的作者v_JULY_v君的文章《程序员编程艺术第三十~三十一章:字符串转换成整数,通配符字符串匹配》后, 对于溢出的处理, 我觉得可以作如下的改进:

既然一个数括大10倍, 有可能溢出, 而且很难判断是否溢出, 为什么不用除法呢? 与其将n扩大10倍, 冒着溢出的风险, 再与MAX进行比较(如果已经溢出, 则比较的结果没有意义), 不如先用n与MAX/10进行比较: 若n>MAX/10(还要考虑n=MAX/10的情况), 说明将要溢出了, 此时可以很明智地下结论: 溢出, 然后进行溢出处理(如返回最大值).

以下为实现代码:

(实现前的说明)

1. MAX不用2147483647的原因: 有可能将来int类型不是4字节, 即使扩展成8个字节, 程序也应该正常运行.

2. 请允许我把函数名改为StrToDecInt, 因为本函数只处理10进制整数.(即使字串中包含"081"这种类型, 也认为是十进制的81, 而不是八进制的081)

//atoi version2:replace multiplication with division
int StrToDecInt(const char* str)
{
    static const int MAX = (int)((unsigned)~0 >> 1);
    static const int MIN = -(int)((unsigned)~0 >> 1) - 1;
    int n = 0;
    int sign = 1;
    int c;

    while (isspace(*str))
        ++str;
    if (*str == '+' || *str == '-')
    {
        if (*str == '-')
            sign = -1;
        ++str;
    }
    while (isdigit(*str))
    {
        c = *str - '0';
        if (sign > 0 && (n > MAX/10 || (n == MAX/10 && c >= MAX%10)))
        {
            n = MAX;
            break;
        }
        else if (sign < 0 && (n > (unsigned)MIN/10 
                              || (n == (unsigned)MIN/10 && c >= (unsigned)MIN%10)))
        {
            n = MIN;
            break;
        }
        n = n * 10 + c;
        ++str;
    }
    return sign > 0 ? n : -n;
}

测试结果(vs2010):

input                      output
"1"                        1
"-1"                       -1
"123"                      123
"-123"                     -123
"010"                      10
"+00131204"                131204
"2147483647"               2147483647
"2147483648"               2147483647
"10522545459"              2147483647
"-2147483648"              -2147483648
"-2147483649"              -2147483648
"-10522545459"             -2147483648
"23a8f"                    23
"  +4488 "                 4488
"abc"                      0
" - 321"                   0
" ++1"                     0
"\n\r\t\v 123\n\r\t\v "    0

(最后一行输出为0呀! fixme!)

第三次尝试: 修复隐藏的bug

第二次尝试中的代码是完美的吗? No. 感谢Apostate(他的空间)在v_JULY_v君的文章《程序员编程艺术第三十~三十一章:字符串转换成整数,通配符字符串匹配》中的评论, 他指出:

1. 如果n为MIN, 则-n是溢出的(虽然溢出, 输出为什么还是正确的呢? fixme!).

我觉得还有以下可以改进的地方:

2. MAX和MIN变量是多余的, 可以直接使用limits.h中的INT_MAX和INT_MIN.

3. 可以考虑将MAX/10, MAX%10, MIN/10和MIN%10保存为临时变量. (有必要吗? your advice?)

4. 减少不必要的赋值: 可以考虑用char类型的变量sign来保存第一个非空格字符, 来表示数字的符号, 用sign跟'-'比较.

修改后代码:

//atoi version3: improving version2
int atoi_mjn(const char* str) {
    int n = 0;
    char sign;
    int c;

    while (isspace(*str))
        ++str;

    sign = *str;
    if (sign == '+' || sign == '-')
        ++str;

    while (isdigit(*str))
    {
        c = *str - '0';
        if (sign != '-' && (n > INT_MAX/10 || (n == INT_MAX/10 && c >= INT_MAX%10)))
        {
            return INT_MAX;
        }
        else if (sign == '-' && (n > (unsigned)INT_MIN/10 
                              || (n == (unsigned)INT_MIN/10 && c >= (unsigned)INT_MIN%10)))
        {
            return INT_MIN;
        }
        n = n * 10 + c;
        ++str;
    }
    return sign == '-' ? -n : n;
}

测试结果与尝试二的相同, 不再贴出.

atoi的实现

参考wikibooks:http://en.wikibooks.org/wiki/C_Programming/C_Reference/stdlib.h/atoi

Nut/OS的实现

atoi函数调用strtol函数, 源码如下(或见原页面):

int atoi(CONST char *str)
{
    return ((int) strtol(str, (char **) NULL, 10));
}

strtol实现的思想跟我写的第二段代码有点像(除法代替乘法). 如下(或见 原页面):

long strtol(CONST char *nptr, char **endptr, int base)
{
    register CONST char *s;
    register long acc, cutoff;
    register int c;
    register int neg, any, cutlim;

    /*
     * Skip white space and pick up leading +/- sign if any.
     * If base is 0, allow 0x for hex and 0 for octal, else
     * assume decimal; if base is already 16, allow 0x.
     */
    s = nptr;
    do {
        c = (unsigned char) *s++;
    } while (isspace(c));
    if (c == '-') {
        neg = 1;
        c = *s++;
    } else {
        neg = 0;
        if (c == '+')
            c = *s++;
    }
    if ((base == 0 || base == 16) && c == '0' && (*s == 'x' || *s == 'X')) {
        c = s[1];
        s += 2;
        base = 16;
    }
    if (base == 0)
        base = c == '0' ? 8 : 10;

    /*
     * Compute the cutoff value between legal numbers and illegal
     * numbers.  That is the largest legal value, divided by the
     * base.  An input number that is greater than this value, if
     * followed by a legal input character, is too big.  One that
     * is equal to this value may be valid or not; the limit
     * between valid and invalid numbers is then based on the last
     * digit.  For instance, if the range for longs is
     * [-2147483648..2147483647] and the input base is 10,
     * cutoff will be set to 214748364 and cutlim to either
     * 7 (neg==0) or 8 (neg==1), meaning that if we have accumulated
     * a value > 214748364, or equal but the next digit is > 7 (or 8),
     * the number is too big, and we will return a range error.
     *
     * Set any if any `digits' consumed; make it negative to indicate
     * overflow.
     */
    cutoff = neg ? LONG_MIN : LONG_MAX;
    cutlim = cutoff % base;
    cutoff /= base;
    if (neg) {
        if (cutlim > 0) {
            cutlim -= base;
            cutoff += 1;
        }
        cutlim = -cutlim;
    }
    for (acc = 0, any = 0;; c = (unsigned char) *s++) {
        if (isdigit(c))
            c -= '0';
        else if (isalpha(c))
            c -= isupper(c) ? 'A' - 10 : 'a' - 10;
        else
            break;
        if (c >= base)
            break;
        if (any < 0)
            continue;
        if (neg) {
            if ((acc < cutoff || acc == cutoff) && c > cutlim) {
                any = -1;
                acc = LONG_MIN;
                errno = ERANGE;
            } else {
                any = 1;
                acc *= base;
                acc -= c;
            }
        } else {
            if ((acc > cutoff || acc == cutoff) && c > cutlim) {
                any = -1;
                acc = LONG_MAX;
                errno = ERANGE;
            } else {
                any = 1;
                acc *= base;
                acc += c;
            }
        }
    }
    if (endptr != 0)
        *endptr = (char *) (any ? s - 1 : nptr);
    return (acc);
}

linux内核的实现

atoi是C语言标准库的函数, 但是系统内核也有这种需求(源文件见这里, 关于此函数的测试, 请见另一篇文章"字符串转换成整数: linux内核atoi函数的测试"):

/*
 *  ======== atoi ========
 *  Purpose:
 *      This function converts strings in decimal or hex format to integers.
 */
static s32 atoi(char *psz_buf)
{
        char *pch = psz_buf;
        s32 base = 0;

        while (isspace(*pch))
                pch++;

        if (*pch == '-' || *pch == '+') {
                base = 10;
                pch++;
        } else if (*pch && tolower(pch[strlen(pch) - 1]) == 'h') {
                base = 16;
        }

        return simple_strtoul(pch, NULL, base);
}
其中s32是signed int的别名. atoi调用了simple_strtoul, 其返回值是unsigned long, atoi在返回时, 强制转换成int, 而不管结果是否正确(很显然这个atoi不安全). 现在看一下 simple_strtoul函数:

/**
 * simple_strtoul - convert a string to an unsigned long
 * @cp: The start of the string
 * @endp: A pointer to the end of the parsed string will be placed here
 * @base: The number base to use
 *
 * This function is obsolete. Please use kstrtoul instead.
 */
unsigned long simple_strtoul(const char *cp, char **endp, unsigned int base)
{
        return simple_strtoull(cp, endp, base);
}
直接进入 simple_strtoull:

/**
 * simple_strtoull - convert a string to an unsigned long long
 * @cp: The start of the string
 * @endp: A pointer to the end of the parsed string will be placed here
 * @base: The number base to use
 *
 * This function is obsolete. Please use kstrtoull instead.
 */
unsigned long long simple_strtoull(const char *cp, char **endp, unsigned int base)
{
        unsigned long long result;
        unsigned int rv;

        cp = _parse_integer_fixup_radix(cp, &base);
        rv = _parse_integer(cp, base, &result);
        /* FIXME */
        cp += (rv & ~KSTRTOX_OVERFLOW);

        if (endp)
                *endp = (char *)cp;

        return result;
}
函数 _parse_integer_fixup_radix主要用来设置基数(8,10,16进制?), 且过滤前面的"0x"(如果有的话), 函数的注释也明确说明了: 该函数已废弃, 请使用 kstrtoull. 转换工作主要在 _parse_integer, 转换的整数在参数result中, 函数返回数字字符的个数:
/*
 * Convert non-negative integer string representation in explicitly given radix
 * to an integer.
 * Return number of characters consumed maybe or-ed with overflow bit.
 * If overflow occurs, result integer (incorrect) is still returned.
 *
 * Don't you dare use this function.
 */
unsigned int _parse_integer(const char *s, unsigned int base, unsigned long long *p)
{
        unsigned long long res;
        unsigned int rv;
        int overflow;

        res = 0;
        rv = 0;
        overflow = 0;
        while (*s) {
                unsigned int val;

                if ('0' <= *s && *s <= '9')
                        val = *s - '0';
                else if ('a' <= _tolower(*s) && _tolower(*s) <= 'f')
                        val = _tolower(*s) - 'a' + 10;
                else
                        break;

                if (val >= base)
                        break;
                /*
                 * Check for overflow only if we are within range of
                 * it in the max base we support (16)
                 */
                if (unlikely(res & (~0ull << 60))) {
                        if (res > div_u64(ULLONG_MAX - val, base))
                                overflow = 1;
                }
                res = res * base + val;
                rv++;
                s++;
        }
        *p = res;
        if (overflow)
                rv |= KSTRTOX_OVERFLOW;
        return rv;
}

unlikely是一个没有实际作用的宏:

#define unlikely(cond) (cond)

正如它的字面意思, 告诉看代码的人: 这里不太可能发生(或不太经常发生), 因为要输入大于等于1152921504606846976(16进制为0x1000000000000000)的数, if条件才会成立.

x86的CPU, unsigned long long占8个字节, 表示式~0ull << 60的值为0xf000000000000000, 该函数最大支持的基数是16, 所以下次要左移4位, 对于0x10000000000000001(注意这里多了一位)这个数字, 在读取到最后一个数字'1'的时候, 程序会检测溢出, 但是程序继续执行, 结果是错误的.

我的实现与linux内核的atoi函数的实现, 都有一个共同的问题: 即使出错, 函数也返回了一个值, 导致调用者误认为自己传入的参数是正确的, 但是可能会导致程序的其他部分产生莫名的错误且很难调试.

其他实现

waterloo | cheriton school of computer science: atoi

References:

  1. v_JULY_v: 程序员编程艺术第三十~三十一章:字符串转换成整数,通配符字符串匹配
  2. Nut/OS API
  3. Linux Cross Reference

你可能感兴趣的:(c,String,atoi)