为什么不能只检查 errno 是否等于 ERANGE?

9
我一直在尝试使用 strtol 正确地将 char 数组转换为 long,检查是否存在溢出或下溢,并对 long 进行 int 强制转换。在此过程中,我注意到有很多代码看起来像这样。
if ((result == LONG_MAX || result == LONG_MIN) && errno == ERANGE)
{
   // Handle the error
}

为什么不能直接说

if(errno == ERANGE)
{
    // Handle the error
}

据我了解,如果发生下溢或上溢,errno在两种情况下都会被设置为ERANGE。那么前者是否真的必要呢?仅检查ERANGE是否会有问题?

目前我的代码如下所示:

 char *endPtr;
 errno = 0;
 long result = strtol(str, &endPtr, 10);

 if(errno == ERANGE)
 {
     // Handle Error
 }
 else if(result > INT_MAX || result < INT_MIN)
 {
    // Handle Error
 }
 else if(endPtr == str || *endPtr != '\0')
 {
     // Handle Error
 }

 num = (int)result;
 return num;

如果有前者的原因,请告诉我。

1
我从未见过一个好的解释,为什么需要同时检查LONG_MAX/LONG_MINERANGE。除了手册将其作为示例显示之外。我能想到的唯一合理的用例是区分溢出和下溢。我也很想知道是否还有其他原因。 - kaylum
@kaylum 我不知道,我觉得我的例子大多数是正确的,因为我不打算区分溢出或下溢是否发生,并且在两种情况下 errno 都设置为 ERANGE。如果其中任何一种情况发生,则结果无效。 - Luis Averhoff
@LuisAverhoff 也许我没有表达清楚,但我同意您的错误检查版本。 - kaylum
@kaylum 抱歉,我搞错了。 - Luis Averhoff
@kaylum 我忘记了一个有效的检查,而且我认为甚至手册上也提到了,那就是如果 errno != 0 && result == 0。虽然说实话,我不确定会发生什么情况,因为如果 strtol 返回0,那么我确信 strtol 没有将 errno 设置为任何值,假设它最初设为零的话。 - Luis Averhoff
你的第二段代码片段比两个答案更正确。在我看来,它是正确和最优的,除了我只会使用 if(errno) - chux - Reinstate Monica
2个回答

8
第一段代码片段是错误的,稍后我会解释原因,但是首先我们需要一些背景知识。
"errno"是一个线程本地变量。当系统调用或某些库函数失败时,它被设置为非零值。当系统调用成功时,它保持不变。所以它始终包含上次调用失败的错误号码。
这意味着你有两个选择。要么在每次调用前将"errno"设置为0,要么使用标准的"errno"习语。下面是标准习语的伪代码。
if ( foo() == some_value_that_indicates_that_an_error_occurred )
    then the value in errno applies to foo
else
    foo succeeded and the errno must be ignored because it could be anything

大多数程序员会使用标准惯用语,因为在每个系统调用之前设置errno为0是令人烦恼和重复的。更不用说你可能会忘记在实际重要的地方设置errno为0。


回到第一个代码片段。它是错误的,因为没有从 strtol 返回值可以明确地表明 strtol 失败。如果 strtol 返回 LONG_MAX,可能是发生了错误,也可能是字符串实际上包含数字 LONG_MAX。无法知道 strtol 调用成功还是失败。这意味着标准习语(第一个代码片段试图实现的内容)不能与 strtol 一起使用。
要正确使用 strtol,需要在调用之前将 errno 设置为 0,像这样:
errno = 0;
result = strtol( buffer, &endptr, 10 );
if ( errno == ERANGE )
{
    // handle the error
    // ERANGE is the only error mentioned in the C specification
}
else if ( endptr == buffer )
{
    // handle the error
    // the conversion failed, i.e. the input string was empty,
    // or only contained whitespace, or the first non-whitespace 
    // character was not valid
}

请注意,一些实现为errno定义了其他非零值。有关详细信息,请参阅适用的手册页面。

1
好的回答,但有一点需要指出:严格来说,strtol是一个库函数,而不是系统调用。它的不寻常之处在于它确实操作了errno。大多数(全部?)系统调用在失败时都会设置errno,但很少有库函数这样做。 - Steve Summit
感谢您的积极回应。如果您不介意我问一下,errno != 0 && result == 0 是可能的吗?我看过一些代码,它们会这样写:if((result == LONG_MAX || result == LONG_MIN) && errno == ERANGE),就像在调用 strol 将 A123 转换时,您将 errno 设置为零。您期望结果为 0,而 errno 保持为零。那么,在这种情况下,什么原因会导致 errno 不为零呢? - Luis Averhoff
@SteveSummit 这是真的,但我不确定如何将其纳入答案中,而不引入不必要的复杂性。我希望将其作为评论留下是否可以。 - user3386109
@LuisAverhoff 如果无法进行转换,则 strtol 将返回 0。在这种情况下,errno 中的值是实现定义的。C 规范没有指定在这种情况下应存储什么值。因此,是的,您可能会得到一个返回值为 0 和非零 errno 的情况。 - user3386109
1
@SteveSummit:大多数库函数在C标准中没有规定设置 errno,但是其中许多函数仍会这样做(或者必须按照POSIX要求这样做)。fopen就是其中之一的例子。 - Keith Thompson
显示剩余4条评论

3
如果您调用
result = strtol("-2147483648", NULL, 0);

或者

result = strtol("2147483647", NULL, 0);

在32位机器上,即使没有出错,result 中也会得到 LONG_MIN 或者 LONG_MAX。根据用户3386109的解释,检测从strtol返回的错误的一个方法是首先将 errno设置为0。另一种方法是让它给你一个结束指针并查看它,有三或四种情况:
char *endptr;
long int result = strtol(str, &endptr, 10);
if(*str == '\0') {
    /* str was empty */
} else if(endptr == str) {
    /* str was completely invalid */
} else if(*endptr != '\0') {
    /* numeric result followed by trailing nonnumeric character(s) */
} else {
    /* str was a completely valid number (perhaps with leading whitespace) */
}

根据您的需要,前两种或三种情况可能会合并在一起。然后,您可能需要担心(a)“完全有效的数字”是否可以表示(可以使用errno进行测试),以及(b)任何“尾随非数字字符”的是否是无害的空格(不幸的是,strtol 不会为您检查,所以如果您关心的话,您需要自己检查)。

所以说,如果在int类型的计算中发生了溢出或下溢,结果总是等于INT_MAX或INT_MIN,那么说if(result > INT_MAX || result < INT_MIN)就没有意义了,对吧?我应该只检查结果是否等于其中之一。 - Luis Averhoff
@LuisAverhoff 抱歉,我之前说的是 INT_MAX,实际上应该是 LONG_MAX。(已经修正)但总的来说,通常没有必要显式地将 strtol 的返回值与任何最小或最大值进行比较。(唯一的例外可能是如果您将该值存储到普通 int 中,需要与 INT_MININT_MAX 进行比较。) - Steve Summit
是的,我正要将值存储到普通整数中。 - Luis Averhoff
@SteveSummit 我来挑毛病了 ;) 没有去除尾随空格,所以 *endptr 可能指向空格字符或换行符。具体来说,最后一条注释中的“尾随”一词并不准确。 - user3386109
@user3386109 谢谢。我记得有一个版本可以去除尾随空格,自从那时起一直在我的脑海中,但你是对的,文档没有提到它。已修复。 - Steve Summit
如果字符串为空,则if(*str == '\0')作为特殊情况并不是真正需要的,因为空字符串将被捕获在if(endptr == str)中。 - chux - Reinstate Monica

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