相關文章:
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 version1int 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 divisionint 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 versioin2int 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:
- v_JULY_v:
程式員編程藝術第三十~三十一章:字串轉換成整數,萬用字元字串匹配
- Nut/OS API
- Linux Cross Reference