sscanf和atoi将字符串转换为整数有什么区别?

69

gcc 4.4.4 c89

如何将字符串转换为整数值更好?

我已经尝试了两种不同的方法:atoi和sscanf。两者都能按照预期工作。

char digits[3] = "34";
int device_num = 0;

if(sscanf(digits, "%d", &device_num) == EOF) {
    fprintf(stderr, "WARNING: Incorrect value for device\n");
    return FALSE;
}

或者使用atoi函数

device_num = atoi(digits);

我认为使用sscanf更好,因为你可以检查错误。然而,atoi没有进行任何检查。


可能是将字符串转换为整数C的重复问题。 - Ciro Santilli OurBigBook.com
6个回答

118
你有三种选择:
1. atoi 这可能是最快的方法,如果你在性能关键代码中使用它,但它不提供错误报告。如果字符串不以整数开头,则返回0。如果该字符串包含整数后面的其他字符,则只会转换初始部分并忽略其余部分。如果数字太大而无法适应 int,则行为未指定。
2. sscanf 提供一些错误报告,并且你可以灵活地选择要存储的类型(char/short/int/long/long long/size_t/ptrdiff_t/intmax_t 的有符号/无符号版本)。
返回值是成功转换的次数,因此扫描 "%d" 会返回0,如果字符串不以整数开头。你可以使用 "%d%n" 将读取的整数之后的第一个字符的索引存储在另一个变量中,从而检查是否已将整个字符串转换或是否有其他字符。然而,与 atoi 一样,对于整数溢出的行为是未指定的。
3. strtol 及其相关函数 提供健壮的错误报告,前提是在调用之前将 errno 设置为 0。溢出时指定了返回值,并设置了 errno。你可以选择任何进制数,从2到36,或将基数指定为 0,以自动解释前导 "0x" 和 "0" 分别作为十六进制和八进制。可选的转换类型是 signed/unsigned 版本的 long/long long/intmax_t。
如果需要更小的类型,你可以将结果存储在一个临时的 long 或 unsigned long 变量中,并自行检查溢出。
由于这些函数接受指向指针的参数,因此你还会获得指向转换整数后的第一个字符的指针,从而可以免费获得整个字符串是否为整数或者如果需要解析字符串中的后续数据。
个人而言,我会建议对于大多数情况使用 strtol 系列函数。如果你只是做一些快速简短的事情,atoi 可能满足你的需求。
顺便说一下,在某些情况下,我发现需要解析数字,其中不应接受前导空格、符号等。在这种情况下,编写其自己的 for 循环相当容易。
for (x=0; (unsigned)*s-'0'<10; s++) 
    x=10*x+(*s-'0');

或者你可以使用以下代码(提高鲁棒性):

if (isdigit(*s))
    x=strtol(s, &s, 10);
else /* error */ 

strtol 中的 errno 是实现特定的功能,如 strtol(3) 手册中所述。为了正确验证,您应该传递 endptr。如果 strtol 后 **endptr 为 '\0',则字符串将被解析为整体并且有效(或其长度为零)。 - Zouppen
1
@Zouppen:不知道你从哪里得到这个信息,但是它是错误的。“strtol、strtoll、strtoul和strtoull函数返回转换后的值(如果有)。如果无法进行转换,则返回零。如果正确的值超出了可表示值的范围,则根据值的返回类型和符号返回LONG_MIN、LONG_MAX、LLONG_MIN、LLONG_MAX、ULONG_MAX或ULLONG_MAX,并将宏ERANGE的值存储在errno中。”(C99 7.20.1.4第8段) - R.. GitHub STOP HELPING ICE
然而,你是对的,你需要检查其他条件。只有溢出才是“错误”。如果没有进行任何转换,应该通过endptr进行检测,如果你坚持要消耗整个字符串,你也应该检查一下。 - R.. GitHub STOP HELPING ICE
你说得对。只有EINVAL的行为有些实现特定。 - Zouppen

10

*scanf() 函数族返回被转换的值的数量。因此,在你的情况下,你应该检查 sscanf() 是否返回了 1。当出现“输入失败”时,EOF 被返回,这意味着 sscanf() 永远不会返回 EOF

对于 sscanf(),函数必须解析格式字符串,然后解码整数。而 atoi() 则没有这样的开销。两者都存在一个问题,即超出范围的值会导致未定义的行为。

您应该使用 strtol()strtoul() 函数,它们提供更好的错误检测和校验。它们还可以让您知道整个字符串是否被消耗。

如果你想要一个 int,你总是可以使用 strtol(),然后检查返回值是否在 INT_MININT_MAX 之间。


如果您将strtol等函数的base设置为0,则可以自动选择八进制、十进制或十六进制输入的转换,这是一个额外的好处。 - Jens Gustedt
使用基数0的一个潜在问题是,以“0”开头的字符串将被解释为基数8(八进制)。这种行为对有知识的用户来说是可以预料的,但太多人不了解八进制,并惊讶地发现“012”变成了10,“019”变成了1,因为转换由于非八进制数字9而停止。 - chux - Reinstate Monica

4

给@R..:

我认为在strtol调用中,仅检查errno并不足以进行错误检测。

long strtol (const char *String, char **EndPointer, int Base)

您还需要检查EndPointer的错误。

2
当不需要考虑无效字符串输入或范围问题时,请使用最简单的方法:atoi()
否则,具有最佳错误/范围检测的方法既不是atoi(),也不是sscanf()这个好答案已经详细说明了atoi()的缺乏错误检查和sscanf()的一些错误检查。 strtol()是将字符串转换为int的最严格函数。但这只是一个开始。以下是详细的示例,以展示正确的用法,以及在接受的答案之后回答的原因。
// Over-simplified use
int strtoi(const char *nptr) {
  int i = (int) strtol(nptr, (char **)NULL, 10);
  return i; 
}

这类似于 atoi(),但忽略了使用 strtol() 的错误检测功能。
要完全使用 strtol(),有各种功能需要考虑:
  1. Detection of no conversion: Examples: "xyz", or "" or "--0"? In these cases, endptr will match nptr.

    char *endptr;
    int i = (int)strtol(nptr, &endptr, 10);
    if (nptr == endptr) return FAIL_NO_CONVERT;
    
  2. Should the whole string convert or just the leading portion: Is "123xyz" OK?

    char *endptr;
    int i = (int)strtol(nptr, &endptr, 10);
    if (*endptr != '\0') return FAIL_EXTRA_JUNK;
    
  3. Detect if value was so big, the the result is not representable as a long like "999999999999999999999999999999".

    errno = 0;
    long L = strtol(nptr, &endptr, 10);
    if (errno == ERANGE) return FAIL_OVERFLOW;
    
  4. Detect if the value was outside the range of than int, but not long. If int and long have the same range, this test is not needed.

    long L = strtol(nptr, &endptr, 10);
    if (L < INT_MIN || L > INT_MAX) return FAIL_INT_OVERFLOW;
    
  5. Some implementations go beyond the C standard and set errno for additional reasons such as errno to EINVAL in case no conversion was performed or EINVAL The value of the Base parameter is not valid.. The best time to test for these errno values is implementation dependent.

将所有这些放在一起:(根据您的需要进行调整)
#include <errno.h>
#include <stdlib.h>

int strtoi(const char *nptr, int *error_code) {
  char *endptr;
  errno = 0;
  long i = strtol(nptr, &endptr, 10);

  #if LONG_MIN < INT_MIN || LONG_MAX > INT_MAX
  if (errno == ERANGE || i > INT_MAX || i < INT_MIN) {
    errno = ERANGE;
    i = i > 0 : INT_MAX : INT_MIN;
    *error_code = FAIL_INT_OVERFLOW;
  }
  #else
  if (errno == ERANGE) {
    *error_code = FAIL_OVERFLOW;
  }
  #endif

  else if (endptr == nptr) {
    *error_code = FAIL_NO_CONVERT;
  } else if (*endptr != '\0') {
    *error_code = FAIL_EXTRA_JUNK;
  } else if (errno) {
    *error_code = FAIL_IMPLEMENTATION_REASON;
  }
  return (int) i;
}

注意:所有提到的函数都允许前导空格、一个可选的前导“符号”字符,并且会受到“区域设置”更改的影响。如果需要更严格的转换,需要编写额外的代码。
注意:非 OP 的标题更改导致了强调的偏差。这个回答更适用于原始标题“将字符串转换为整数 sscanf 或 atoi”。

2

结合R..和PickBoy的回答,简洁明了地回答问题:

long strtol (const char *String, char **EndPointer, int Base)

// examples
strtol(s, NULL, 10);
strtol(s, &s, 10);

0

如果用户输入34abc,然后你将它们传递给atoi函数,它将返回34。 如果你想要验证输入的值,那么你必须迭代地使用isdigit函数在输入的字符串上。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接