前言
手寫代碼是面試過程常見的環節之一,但是一般都是手寫算法題,此次面試官要我手寫一個基本的 C 語言 atoi,內心一驚,這怎麼感覺像是校招…
先說一下 atoi 函數的功能,它是一個 C 標準庫函數,將給定的 C 風格字符串轉換爲 int。
本題雖然簡單,但是如果之前沒有練習書手寫 atoi,要想寫出一個讓面試官滿意的接近標準庫水準的 atoi 並非易事,因爲有不少實現細節需要考慮。
我遇到的問題
由於第一次手寫 atoi,有點猝不及防,內心還是有點慌亂的,因爲自己對 atoi 地認知也僅僅停留在知其作用的程度,對其實現細節並沒有深度研究過。就這樣,我在思考如何書寫 atoi 前遇到了不少細節問題。
(1)如果傳入的參數非法,比如並非是一個數字型字符串,函數該返回多少來表示參數異常呢?返回 -1 嗎?但是如果待轉換的字符串是 “-1”,那豈不是衝突了?
(2)如果待轉換的是負數,如果將最後的正數轉換爲負數呢?
(3)考慮的不夠全面,以爲 atoi 對入參要完全符合條件。事實上 atoi 比我想象中的容錯性更高。在找到第一個非空白字符之前,該函數首先根據需要丟棄儘可能多的空白字符(如 space 和 tab)。然後,從這個字符開始,取一個可選的正負號,後面跟着儘可能多的基數爲 10 的數字,並將它們解釋爲一個數值。字符串可以在構成整數的字符之後包含其他字符,這些字符被忽略,對此函數的行爲沒有任何影響;
(4)如果優雅地將數字字符轉換爲對應的數值,比如將字符 ‘0’ 轉爲數值 0;
(5)如果轉換的數值溢出了該返回什麼呢?
如果沒有意識到上面的問題,或者想到了但是沒法解決,那麼真的很難寫出一個讓面試滿意地 atoi。
標準庫的實現
下面看一下標準庫 atoi 地做法吧。
第一個問題,atoi 做法是入參字符串爲空或僅包含空白字符,則不執行轉換並返回零;
第二個問題,我想複雜了,實際上正數前加個減號即可變爲負數;
第三個問題,實現一個函數時,要考慮到入參的各種情況並儘可能地提供高容錯性的實現,就像 atoi 會跳過前面的空白字符以及丟棄尾部非數字字符。
第四個問題,最容易想到的是通過 switch case 枚舉出十個字符並返回對應的數字。這個很容易實現,但是不夠優雅。標準庫的做法是:
#ifdef USE_WIDE_CHAR
# define L_(Ch) L##Ch
#else
# define L_(Ch) Ch
#endif
if (c >= L_('0') && c <= L_('9'))
c -= L_('0');
這個做法非常簡潔精妙,對數字字符直接與字符 ‘0’ 相減即可得到其對應的數值。
第五個問題,標準庫的做法是返回 int 所能表示的最大正數和最小負數。int 能表示的最小負數爲 -0x8000 0000(-2,147,483,648),最大正數爲 0x7fff ffff(2,147,483,647)。
這裏有需要知道 atoi 是調用函數 strtol,strtol 再調用函數 __strtol_l 來完成轉換。因爲 strtol 返回類型是 long int,而 long int 在 32 位的程序和 64 位的程序中位寬度是不同的,所以底層實現時需要根據程序的位寬來返回不同的最大最小值。相關的宏定義如下:
#ifdef QUAD
# define STRTOL_LONG_MIN LONG_LONG_MIN
# define STRTOL_LONG_MAX LONG_LONG_MAX
#else
# define STRTOL_LONG_MIN LONG_MIN
# define STRTOL_LONG_MAX LONG_MAX
#endif
其中 LONG_LONG_MIN 和 LONG_LONG_MAX 我在標準庫源碼中並未找到其定義,原來其定義只存在於 GCC 版本低於 3.0,已被 LLONG_MIN 與 LLONG_MAX 取代。LLONG_MIN 與 LLONG_MAX 在 inclue/limits.h 頭文件中的定義如下:
# define LLONG_MAX 9223372036854775807LL
# define LLONG_MIN (-LLONG_MAX - 1LL)
另外 LONG_MIN 和 LONG_MAX 定義在文件 inclue/limits.h 頭文件中。
# if __WORDSIZE == 64
# define LONG_MAX 9223372036854775807L
# else
# define LONG_MAX 2147483647L
# endif
# define LONG_MIN (-LONG_MAX - 1L)
從這裏可以看出,標準庫是根據宏變量 __WORDSIZE 來判斷程序是 32 位還是 64 位,宏 __WORDSIZE 可以通過 gcc 命令的編譯選項 -m32 或者 -m64 來控制。
如果是自己實現的話,可以根據指針變量的位寬來判斷程序是 32bits 還是 64bits:
#define IS64BIT ((sizeof(NULL)==8))
如果面試時能在短時間內考慮到上面的問題並想到對應的解決辦法,那麼寫出讓面試官滿意的代碼也八九不離十了,最後就是展現碼代碼的基本功了。
大家先欣賞一下 GNU C 的實現吧,以 glic-2.31 爲例。
下載好源碼包後對其解壓,在目錄 stdlib 下 atoi.c 中找到 atoi 的定義。
//
// atoi.c
//
/* Convert a string to an int. */
int
atoi (const char *nptr)
{
return (int) strtol (nptr, (char **) NULL, 10);
}
libc_hidden_def (atoi)
可見 atoi 是調用了 strtol 函數,繼續尋找 strtol 的定義,最終在 strtol.c 中找到了其定義。
//
// strtol.c
//
INT
__strtol (const STRING_TYPE *nptr, STRING_TYPE **endptr, int base)
{
return INTERNAL (__strtol_l) (nptr, endptr, base, 0, _NL_CURRENT_LOCALE);
}
weak_alias (__strtol, strtol)
libc_hidden_weak (strtol)
weak_alias (__strtol, strtol) 表明 strtol 的別稱是 __strtol。這裏可以看出,__strtol 也並非直接實現轉換功能,而是調用 __strtol_l 函數實現轉換。下面繼續尋找 __strtol_l 函數的定義,其定義在 strtol_l.c 文件中。
//
// strtol_l.c
//
/* Convert NPTR to an `unsigned long int' or `long int' in base BASE.
If BASE is 0 the base is determined by the presence of a leading
zero, indicating octal or a leading "0x" or "0X", indicating hexadecimal.
If BASE is < 2 or > 36, it is reset to 10.
If ENDPTR is not NULL, a pointer to the character after the last
one converted is stored in *ENDPTR. */
INT
INTERNAL (__strtol_l) (const STRING_TYPE *nptr, STRING_TYPE **endptr,
int base, int group, locale_t loc)
{
int negative;
unsigned LONG int cutoff;
unsigned int cutlim;
unsigned LONG int i;
const STRING_TYPE *s;
UCHAR_TYPE c;
const STRING_TYPE *save, *end;
int overflow;
#ifndef USE_WIDE_CHAR
size_t cnt;
#endif
#ifdef USE_NUMBER_GROUPING
struct __locale_data *current = loc->__locales[LC_NUMERIC];
/* The thousands character of the current locale. */
# ifdef USE_WIDE_CHAR
wchar_t thousands = L'\0';
# else
const char *thousands = NULL;
size_t thousands_len = 0;
# endif
/* The numeric grouping specification of the current locale,
in the format described in <locale.h>. */
const char *grouping;
if (__glibc_unlikely (group))
{
grouping = _NL_CURRENT (LC_NUMERIC, GROUPING);
if (*grouping <= 0 || *grouping == CHAR_MAX)
grouping = NULL;
else
{
/* Figure out the thousands separator character. */
# ifdef USE_WIDE_CHAR
# ifdef _LIBC
thousands = _NL_CURRENT_WORD (LC_NUMERIC,
_NL_NUMERIC_THOUSANDS_SEP_WC);
# endif
if (thousands == L'\0')
grouping = NULL;
# else
# ifdef _LIBC
thousands = _NL_CURRENT (LC_NUMERIC, THOUSANDS_SEP);
# endif
if (*thousands == '\0')
{
thousands = NULL;
grouping = NULL;
}
# endif
}
}
else
grouping = NULL;
#endif
if (base < 0 || base == 1 || base > 36)
{
__set_errno (EINVAL);
return 0;
}
save = s = nptr;
/* Skip white space. */
while (ISSPACE (*s))
++s;
if (__glibc_unlikely (*s == L_('\0')))
goto noconv;
/* Check for a sign. */
negative = 0;
if (*s == L_('-'))
{
negative = 1;
++s;
}
else if (*s == L_('+'))
++s;
/* Recognize number prefix and if BASE is zero, figure it out ourselves. */
if (*s == L_('0'))
{
if ((base == 0 || base == 16) && TOUPPER (s[1]) == L_('X'))
{
s += 2;
base = 16;
}
else if (base == 0)
base = 8;
}
else if (base == 0)
base = 10;
/* Save the pointer so we can check later if anything happened. */
save = s;
#ifdef USE_NUMBER_GROUPING
if (base != 10)
grouping = NULL;
if (__glibc_unlikely (grouping != NULL))
{
# ifndef USE_WIDE_CHAR
thousands_len = strlen (thousands);
# endif
/* Find the end of the digit string and check its grouping. */
end = s;
if (
# ifdef USE_WIDE_CHAR
*s != thousands
# else
({ for (cnt = 0; cnt < thousands_len; ++cnt)
if (thousands[cnt] != end[cnt])
break;
cnt < thousands_len; })
# endif
)
{
for (c = *end; c != L_('\0'); c = *++end)
if (((STRING_TYPE) c < L_('0') || (STRING_TYPE) c > L_('9'))
# ifdef USE_WIDE_CHAR
&& (wchar_t) c != thousands
# else
&& ({ for (cnt = 0; cnt < thousands_len; ++cnt)
if (thousands[cnt] != end[cnt])
break;
cnt < thousands_len; })
# endif
&& (!ISALPHA (c)
|| (int) (TOUPPER (c) - L_('A') + 10) >= base))
break;
# ifdef USE_WIDE_CHAR
end = __correctly_grouped_prefixwc (s, end, thousands, grouping);
# else
end = __correctly_grouped_prefixmb (s, end, thousands, grouping);
# endif
}
}
else
#endif
end = NULL;
/* Avoid runtime division; lookup cutoff and limit. */
cutoff = cutoff_tab[base - 2];
cutlim = cutlim_tab[base - 2];
overflow = 0;
i = 0;
c = *s;
if (sizeof (long int) != sizeof (LONG int))
{
unsigned long int j = 0;
unsigned long int jmax = jmax_tab[base - 2];
for (;c != L_('\0'); c = *++s)
{
if (s == end)
break;
if (c >= L_('0') && c <= L_('9'))
c -= L_('0');
#ifdef USE_NUMBER_GROUPING
# ifdef USE_WIDE_CHAR
else if (grouping && (wchar_t) c == thousands)
continue;
# else
else if (thousands_len)
{
for (cnt = 0; cnt < thousands_len; ++cnt)
if (thousands[cnt] != s[cnt])
break;
if (cnt == thousands_len)
{
s += thousands_len - 1;
continue;
}
if (ISALPHA (c))
c = TOUPPER (c) - L_('A') + 10;
else
break;
}
# endif
#endif
else if (ISALPHA (c))
c = TOUPPER (c) - L_('A') + 10;
else
break;
if ((int) c >= base)
break;
/* Note that we never can have an overflow. */
else if (j >= jmax)
{
/* We have an overflow. Now use the long representation. */
i = (unsigned LONG int) j;
goto use_long;
}
else
j = j * (unsigned long int) base + c;
}
i = (unsigned LONG int) j;
}
else
for (;c != L_('\0'); c = *++s)
{
if (s == end)
break;
if (c >= L_('0') && c <= L_('9'))
c -= L_('0');
#ifdef USE_NUMBER_GROUPING
# ifdef USE_WIDE_CHAR
else if (grouping && (wchar_t) c == thousands)
continue;
# else
else if (thousands_len)
{
for (cnt = 0; cnt < thousands_len; ++cnt)
if (thousands[cnt] != s[cnt])
break;
if (cnt == thousands_len)
{
s += thousands_len - 1;
continue;
}
if (ISALPHA (c))
c = TOUPPER (c) - L_('A') + 10;
else
break;
}
# endif
#endif
else if (ISALPHA (c))
c = TOUPPER (c) - L_('A') + 10;
else
break;
if ((int) c >= base)
break;
/* Check for overflow. */
if (i > cutoff || (i == cutoff && c > cutlim))
overflow = 1;
else
{
use_long:
i *= (unsigned LONG int) base;
i += c;
}
}
/* Check if anything actually happened. */
if (s == save)
goto noconv;
/* Store in ENDPTR the address of one character
past the last character we converted. */
if (endptr != NULL)
*endptr = (STRING_TYPE *) s;
#if !UNSIGNED
/* Check for a value that is within the range of
`unsigned LONG int', but outside the range of `LONG int'. */
if (overflow == 0
&& i > (negative
? -((unsigned LONG int) (STRTOL_LONG_MIN + 1)) + 1
: (unsigned LONG int) STRTOL_LONG_MAX))
overflow = 1;
#endif
if (__glibc_unlikely (overflow))
{
__set_errno (ERANGE);
#if UNSIGNED
return STRTOL_ULONG_MAX;
#else
return negative ? STRTOL_LONG_MIN : STRTOL_LONG_MAX;
#endif
}
/* Return the result of the appropriate sign. */
return negative ? -i : i;
noconv:
/* We must handle a special case here: the base is 0 or 16 and the
first two characters are '0' and 'x', but the rest are no
hexadecimal digits. This is no error case. We return 0 and
ENDPTR points to the `x`. */
if (endptr != NULL)
{
if (save - nptr >= 2 && TOUPPER (save[-1]) == L_('X')
&& save[-2] == L_('0'))
*endptr = (STRING_TYPE *) &save[-1];
else
/* There was no number to convert. */
*endptr = (STRING_TYPE *) nptr;
}
return 0L;
}
damm it!這簡直是老太婆的裹腳布,又臭又長,難怪我寫不出讓面試官滿意的 atoi,原來上面纔是面試官想要的答案。還是冷靜下來,細細品味標準庫的魅力。
第一部分是定義了函數中用到的局部變量。
第二部分是對字符串分組的處理,比如對於很長的數字,一般會使用逗號按照 3 個數字進行分組,例如 123,456,789。
第三部分是對數值基數的判斷,如果非法,設置全局環境變量 errno 爲 EINVAL(參數非法)並返回 0。
第四部分跳過空白字符。
第五部分檢查數值符號,判斷是否是負數。
第六部分檢查數值前綴,0 表示八進制,0x 或 0X 表示 16 進制,否則爲 10 進制。
第七部分開始執行轉換邏輯。
以上大致是函數 __strtol_l 的內容,__strtol_l 事無鉅細,面面俱到,完成數值型字符串到數值的轉換,包括對八進制和十六進制數值的轉換。atoi 實現的是十進制數值的轉換,只是 __strtol_l 功能的一部分。
適合面試手寫的 atoi 實現
如果只是應對面試,書寫上面的代碼不合適,因爲使用了大量的宏變量且包括了寬字符與數值分組的特殊處理,短時間內寫出面面俱到的函數是不現實的,下面結合我們上面考慮到的幾個問題點,給出適合手寫的簡易版本。
#include <stdio.h>
#include <ctype.h>
#include <limits.h>
// @brief: atoi converts a decimal value string to the corresponding value
int myatoi(const char* s){
// check null pointer
if (s == NULL) return 0;
int i,sign;
unsigned int n;
// skip white space
for (i = 0; isspace(s[i]); i++);
// get sign and skip
sign = (s[i]=='-')?-1:1;
if(s[i]=='+' || s[i]=='-') i++;
// convert numeric characters to numeric value
for(n=0; isdigit(s[i]); i++){
n = 10 * n + (s[i] - '0');
}
// overflow judgment
if (sign == 1 && n > INT_MAX) return INT_MAX;
if (sign == -1 && n -1 > INT_MAX) return INT_MIN;
return sign * n;
}
其中函數 isspace() 與 isdigit() 是 C 標準庫函數,申明於頭文件 ctype.h。宏 INT_MAX 與 INT_MIN 定義在頭文件 limits.h,定義如下:
# define INT_MIN (-INT_MAX - 1)
# define INT_MAX 2147483647
功能驗證:
int main() {
// normal positive int value with white-space character in front
const char* s0 = "\t1";
printf("1=%d\n", myatoi(s0));
// normal negtive int value with white-space character in front
const char* s1 = "\t-1";
printf("-1=%d\n", myatoi(s1));
// null pointer
const char* s3 = NULL;
printf("NULL=%d\n", myatoi(s3));
// invalid decimal value string
const char* s4 = "a123b";
printf("a123b=%d\n", myatoi(s4));
// out of max range
const char* s5 = "2147483648";
printf("2147483648=%d\n", myatoi(s5));
// out of min range
const char* s6 = "-2147483649";
printf("-2147483649=%d\n", myatoi(s6));
}
運行輸出:
1=1
-1=-1
NULL=0
a123b=0
2147483648=2147483647
-2147483649=-2147483648
看似簡單的函數正真寫起來並不容易,對每一道面試題保持敬畏之心。
參考文獻
[1] cplusplus.atoi
[2] Index of /gnu/glibc
[3] Range of Type (The GNU C Library)
[4] 博客園.C語言itoa()函數和atoi()函數詳解(整數轉字符C實現)