atoi(c89)

來源:互聯網
上載者:User

相關文章:

  • atoi函數的實現二: 測試各實現的正確性
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:
  1. v_JULY_v:
    程式員編程藝術第三十~三十一章:字串轉換成整數,萬用字元字串匹配
  2. Nut/OS API
  3. Linux Cross Reference

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.