strtol的正确用法

41
下面的程序将字符串转换为长整型,但据我理解它也会返回一个错误。我依赖于这样一个事实,即如果strtol成功地将字符串转换为长整型,则strtol的第二个参数应该等于NULL。当我使用55运行下面的应用程序时,我会得到以下消息。
./convertToLong 55
Could not convert 55 to long and leftover string is: 55 as long is 55

我如何能够成功地从 strtol 中检测错误?在我的应用程序中,零是一个有效的值。

代码:

#include <stdio.h>
#include <stdlib.h>

static long parseLong(const char * str);

int main(int argc, char ** argv)
{
    printf("%s as long is %ld\n", argv[1], parseLong(argv[1]));
    return 0;
 }

static long parseLong(const char * str)
{
    long _val = 0;
    char * temp;

    _val = strtol(str, &temp, 0);

    if(temp != '\0')
            printf("Could not convert %s to long and leftover string is: %s", str, temp);

    return _val;
}

1
重新阅读文档;你还应该处理像溢出这样的错误。 - Kerrek SB
1
此外,对于 strto* 函数的适当错误检查并不是通过检查输出指针来完成的。应该通过检查零返回值和设置 errno 来完成。 - user529758
2
为什么你不在C++中使用std::stoi?(你添加了C++标签) - BatchyX
@chris:你可以使用std::stoi完全做同样的事情。实际上,stoi的原型与strtol几乎相同,但在应该使用异常而不是错误返回值和全局错误变量hackery时使用异常。 - BatchyX
@BatchyX,随便怎么做都可以。我只是惊讶它一开始没有那个功能,所以你必须添加自己的代码。 - chris
显示剩余3条评论
5个回答

79

请注意,以下划线开头的名称由实现保留;最好避免在您的代码中使用这些名称。因此,_val 应该改为 val

对于 strtol() 及其相关函数的完整错误处理规范在初次接触时可能会非常复杂。你做得非常正确的一件事是使用函数来调用 strtol();在代码中“生硬”使用它可能不正确。

由于该问题同时被标记为 C 和 C++,我将引用 C2011 标准;您可以自行在 C++ 标准中找到适当的措辞。

ISO/IEC 9899:2011 §7.22.1.4 函数 strtolstrtollstrtoulstrtoull

long int strtol(const char * restrict nptr, char ** restrict endptr, int base);

¶2 [...] 首先, 它们将输入字符串分解为三个部分:一个初始(可能为空)的空白字符序列(如 isspace 函数所指定的),一个类似于在基数中表示的整数的主体序列,而这个基数由基数的值决定,并且具有一个或多个无法识别的字符的最终字符串,包括输入字符串的终止空字符。[...]

¶7 如果主体序列为空或没有预期的形式,则不执行转换;如果 endptr 不是 null 指针,则将 nptr 的值存储在所指向的对象中。

返回值

¶8 函数 strtolstrtollstrtoulstrtoull 返回转换后的值(如果有)。如果无法执行任何转换,则返回零。如果正确的值超出了可表示值的范围,则返回 LONG_MIN、LONG_MAX、LLONG_MIN、LLONG_MAX、ULONG_MAX 或 ULLONG_MAX(根据值的返回类型和符号而定),并将宏 ERANGE 的值存储在 errno 中。

请记住,标准的 C 库函数永远不会将 errno 设置为 0。因此,要可靠地使用,您必须在调用 strtol() 前将 errno 设置为零。

因此,您的 parseLong() 函数可能如下所示:

static long parseLong(const char *str)
{
    errno = 0;
    char *temp;
    long val = strtol(str, &temp, 0);

    if (temp == str || *temp != '\0' ||
        ((val == LONG_MIN || val == LONG_MAX) && errno == ERANGE))
        fprintf(stderr, "Could not convert '%s' to long and leftover string is: '%s'\n",
                str, temp);
        // cerr << "Could not convert '" << str << "' to long and leftover string is '"
        //      << temp << "'\n";
    return val;
}
请注意,如果出现错误,此函数会返回0或LONG_MIN或LONG_MAX,具体取决于strtol()的返回值。如果您的调用代码需要知道转换是否成功,您需要不同的函数接口-请参见下文。还要注意,错误应该打印到stderr而不是stdout,错误消息应以换行符\n结束;如果没有,它们不能保证及时显示。
现在,在库代码中,您可能不希望任何打印输出,而您的调用代码可能想知道转换是否成功,因此您可能需要修改接口以返回成功/失败指示:
bool parseLong(const char *str, long *val)
{
    char *temp;
    bool rc = true;
    errno = 0;
    *val = strtol(str, &temp, 0);

    if (temp == str || *temp != '\0' ||
        ((*val == LONG_MIN || *val == LONG_MAX) && errno == ERANGE))
        rc = false;

    return rc;
}

你可以像这样使用:

if (parseLong(str, &value))
    …conversion successful…
else
    …handle error…
如果你需要区分‘尾随垃圾’、‘无效数值字符串’、‘值过大’和‘值过小’(以及‘无错误’),则应该使用整数或枚举而不是布尔型返回代码。如果允许尾随空格但不允许其他字符,或者不允许任何前导空格,则需要在函数中做更多的工作。此代码允许八进制、十进制和十六进制;如果你想要严格的十进制,则需要将调用strtol()的0更改为10。
如果你的函数要伪装成标准库的一部分,它们就不应该永久地将errno设置为0,所以你需要包装代码以保留errno
int saved = errno;  // At the start, before errno = 0;

…rest of function…

if (errno == 0)     // Before the return
    errno = saved;

1
感谢您的详细回答!但是,为什么您要明确检查 "errno == ERANGE" 而不是 "errno != 0" 呢?如果用户可以指定转换的基数,errno 也可能被设置为 EINVAL... 此外,“man strtol”(http://linux.die.net/man/3/strtol)使用以下代码进行错误检查,我真的不明白这样做的原因:“if ((errno == ERANGE && (val == LONG_MAX || val == LONG_MIN)) || (errno != 0 && val == 0)){ error }”。为什么这不是一个简单的 "errno != 0" 呢? - oliver
4
这个标准没有提及对于 base 值为除了 0236 之外的其他值,将 errno 设置为 EINVAL,但这是一个合理的做法。一般来说,你应该谨慎地尝试使用 errno 来检测错误条件,而不是依赖函数的返回值;即使函数成功执行,库也可能将 errno 设置为非零值(在 Solaris 上,如果输出不是终端,则会在成功操作之后找到 errno == ENOTTY)。理论上,strtol() 可以将 "1" 转换为 1 并将 errno 设置为非零值,这是合法但是反常的(并且成功的)情况。 - Jonathan Leffler
2
无论 strtol 是否返回 LONG_MIN/LONG_MAX,是否有理由无条件检查 errno == ERANGE?(根据您在评论中提供的原因,库函数可能会在成功时设置 errno。) - mafso
3
不同意“即使函数成功,库也可以将errno设置为非零值。” C11 §7.5 3讨论了这一点,但这并不适用于strtol(),因为“只要在函数的描述中未记录使用errno”,而strtol()确实有。 if(temp == str || * temp!= '\ 0' || errno == ERANGE)就足够了。我认为if(temp == str || * temp!= '\ 0' || errno)更好,因为它可以捕获一些ID扩展。(* val == LONG_MIN || * val == LONG_MAX)是不必要的。 - chux - Reinstate Monica
2
@JonathanLeffler 对于 EINVAL 的看法以及建议的 temp == str || *temp != '\0' || errno 我表示赞同 - 我们在这方面意见一致。然而,评论是关于需要 *val == LONG_MIN || *val == LONG_MAX,但考虑到其他可能的 errno,这并没有得到加强。如果 errno == ERANGE 为真,则即使在某些独角兽机器上 *val == LONG_MIN || *val == LONG_MAX 为假,strtol() 仍应被视为失败。 - chux - Reinstate Monica
显示剩余9条评论

22

你已经快成功了。temp 本身不会是 null,但如果整个字符串被转换完毕,它将指向一个空字符,因此你需要对其进行解引用:

if (*temp != '\0')

5
需要进行额外的检查来处理溢出和解析空字符串。请参考Jonathan Leffler的答案。 - 0xF

7
如何成功从strtol中检测错误?
static long parseLong(const char * str) {
    int base = 0;
    char *endptr;
    errno = 0;
    long val = strtol(str, &endptr, base);

标准C库支持/指定的3个测试:

  1. Any conversion done?

     if (str == endptr) puts("No conversion.");
    
  2. In range?

     // Best to set errno = 0 before the strtol() call.
     else if (errno == ERANGE) puts("Input out of long range.");
    
  3. Tailing junk?

     else if (*endptr) puts("Extra junk after the numeric text.");
    

成功

    else printf("Success %ld\n", val);

如输入类似于str == NULLbase不是0,则[2到36]之间的输入为未定义行为。各种实现(C库的扩展)提供定义行为并通过errno报告。我们可以添加第四个测试。
    else if (errno) puts("Some implementation error found.");

或者与 errno == ERANGE 测试结合使用。


简洁的示例代码,还利用了常见的实现扩展。

long my_parseLong(const char *str, int base, bool *success) {
    char *endptr = 0;
    errno = 0;
    long val = strtol(str, &endptr, base);
   
    if (success) {
      *success = endptr != str && errno == 0 && endptr && *endptr == '\0';
    }
    return val;
}

4
你缺少了一个间接层。你想要检查的是字符是否是终止符NUL,而不是指针是否为NULL
if (*temp != '\0')

顺便说一下,这并不是一个好的错误检查方法。使用 strto* 函数族的正确错误检查方法不是通过将输出指针与字符串结尾进行比较来完成的,而是通过检查返回值是否为零并获取 errno 的返回值来完成的。

1

你应该进行检查

*temp != '\0'

根据这个,调用strotol后你还应该能够检查errno的值:

RETURN VALUES
     The strtol(), strtoll(), strtoimax(), and strtoq() functions return the result
     of the conversion, unless the value would underflow or overflow.  If no conver-
     sion could be performed, 0 is returned and the global variable errno is set to
     EINVAL (the last feature is not portable across all platforms).  If an overflow
     or underflow occurs, errno is set to ERANGE and the function return value is
     clamped according to the following table.


       Function       underflow     overflow
       strtol()       LONG_MIN      LONG_MAX
       strtoll()      LLONG_MIN     LLONG_MAX
       strtoimax()    INTMAX_MIN    INTMAX_MAX
       strtoq()       LLONG_MIN     LLONG_MAX

如果你不说“下表”在哪里,引用“下表”就没有意义。 - Roland Illig
你是自己写的这份文档呢,还是只是忘了提及你从哪里抄袭来的? - Roland Illig
不,这是一个 man 手册页面。在任何基于 Unix 的系统上只需输入“man strtol”。 - spartygw
我只是在问,因为NetBSD的手册看起来非常不同,尽管它是一个类UNIX系统。 - Roland Illig
1
此外,该问题被标记为“C、C++”,因此适当的参考应来自于C或C++标准,而不是特定硬件架构上的特定实现。 - Roland Illig

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