在 C 语言中解析整数的危险性

将一个简单的字符串解析为整数似乎是一项微不足道的工作——这是任何编程语言最基本的要求之一。然而,在 C 标准库中,这项操作充满了未定义行为、静默溢出和违反直觉的逻辑。对于处理不受信任输入或需要严格验证的开发者来说,内置选项往往不足甚至极其危险。

要被认为是“正确”的,一个整数解析器应该确保字符串中的数值在结果数据类型中得到准确表示,确保整个字符串都被消耗(没有尾随垃圾字符),并且任何失败(无论是由于溢出还是无效字符)都能作为错误明确报告。

标准 C 函数的缺陷

atol():遗留陷阱

atol() 可能是这一组函数中最危险的一个。它无法检测错误;如果它遇到一个不以数字开头的字符串,它只会简单地返回 0。如果输入是一个空字符串或仅包含空格,它也会返回 0

更关键的是,C 和 POSIX 标准规定,如果数值无法表示,其行为是未定义的。虽然大多数现代实现只是将数值限制在 LONG_MAX,但未定义行为的理论可能性使得 atol() 对于任何处理不受信任输入的应用程序都是不可用的。

strtol():罕见的成功(带有注意事项)

strtol() 是唯一可以被正确使用的标准 C 函数,但仅限于有符号整数,并且需要大量的样板代码。为了安全地使用它,开发者必须:

  1. 手动检查空字符串。
  2. 在调用前将 errno 重置为 0。
  3. 在调用后检查 errno 以检测范围错误。
  4. 验证 endptr 以确保没有剩余的非数字字符。

strtoul()sscanf():无符号整数的噩梦

当涉及到无符号整数时,标准库的表现非常糟糕。strtoul()(及其 64 位兄弟 strtoull())允许前导负号。如果传入一个负数,它会取反结果并将其转换为无符号值。例如,在 64 位系统上,向 strtoul() 传递 -1 会返回 18446744073709551615 ($2^{64}-1$)。

sscanf() 也同样存在问题。它缺乏区分有效 ULONG_MAX 输入和超出范围溢出的机制。这种歧义使得无法验证解析出的值是否真的是字符串中提供的数值。

C++ 的替代方案

对于使用 C++ 的人来说,std::stoul()std::istringstream 遭受着与 C 语言对应函数类似的缺陷,特别是在处理无符号上下文中的负号时。

然而,std::from_chars(在 C++17 中引入)提供了一个现代且健壮的解决方案。它不允许无符号类型使用负号,并返回一个包含指向第一个未解析字符的指针和错误代码的结构化结果。这符合专业解析器所期望的“正确”行为。

C 语言中的实际解决方法

如果你受限于 C 语言且无法使用 C++17,你有两个主要选择:

1. “安全”的有符号路径

如果你不需要 unsigned long 范围的上半部分,你可以使用 strtol(),检查结果是否小于零,然后将结果转换为 unsigned long

2. 自定义包装器

为了真正支持无符号整数的全范围且不接受负数输入,你必须实现一个包装器。建议的方法是首先使用 strtol() 来检测并拒绝负值,然后调用 strtoul() 进行实际转换:

inline bool strtoul_noneg(unsigned long* out, const char *nptr, char **restrict endptr, int base)
{
  if (!*nptr || isspace(*nptr)) return false;
  if (strtol(nptr, endptr, base) < 0) return false;
  
  errno = 0;
  *out = strtoul(nptr, endptr, base);
  if (**endptr || errno) return false;
  
  return true;
}

社区观点

关于 C 语言字符串处理的争论经常将开发者分为两派。一些人认为,标准库的缺陷是导致语言感觉“破碎”的遗留负担。

"I thought it was pretty well known that everything related to strings in C stdlib... is bad. You just need to bring in your own string library."

其他人则认为,C 语言的简洁性是其优点,而标准库仅仅是工具的集合,而非语言本身。从这个角度来看,编写自定义解析器是一个微不足道的练习,允许开发者针对其特定需求进行优化。

无论哲学分歧如何,技术现实依然存在:在 C 语言中解析整数时,默认工具往往是不够的用的。无论是通过自定义包装器还是现代 C++ 替代方案,显式验证是确保程序稳定性和安全性的唯一途径。

Sources