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