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实现)

©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页