3 年大廠工作經驗面試竟然要我手寫 atoi 函數

前言

手寫代碼是面試過程常見的環節之一,但是一般都是手寫算法題,此次面試官要我手寫一個基本的 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實現)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章