在 C 语言中,对于存放错误码的全局变量 errno,相信大家都不陌生。为防止和正常的返回值混淆,系统调用一般并不直接返回错误码,而是将错误码(是一个整数值,不同的值代表不同的含义)存入一个名为 errno 的全局变量中,errno 不同数值所代表的错误消息定义在 <errno.h> 文件中。如果一个系统调用或库函数调用失败,可以通过读出 errno 的值来确定问题所在,推测程序出错的原因,这也是调试程序的一个重要方法。
配合 perror 和 strerror 函数,还可以很方便地查看出错的详细信息。其中:
需要特别强调的是,并不是所有的库函数都适合使用 errno 全局变量。就 errno 而言,库函数一般分为如下 4 种类型。
函 数 | 返回值 | errno 值 |
---|---|---|
fgetwc、fputwc | WEOF | EILSEQ |
strtol、wcstol | LONG_MIN 或 LONG_MAX | ERANGE |
stitoll、wcstoll | LLONG_MIN 或 LLONG_MAX | ERANGE |
stitoul、wcstoul | ULONG_MAX | ERANGE |
stitoull、wcstoull | ULLONG_MAX | ERANGE |
stitoumax、wcstoumax | UINTMAX_MAX | ERANGE |
strtod、wcstod | 0 或 ±HUGE_VAL | ERANGE |
strtof、wcstof | 0 或 ±HUGE_VALF | ERANGE |
strtold、wcstold | 0 或 ±HUGE_VALL | ERANGE |
stitoimax、wcstoimax | INTMAX_MIN、INTMAX_MAX | ERANGE |
如表 1 所示,这些函数将设置 errno,并返回一个带内“In-Band”错误指示符。例如,函数 strtoul 发生错误时将返回 ULONG_MAX,并将 errno 的值设置为 ERANGE。这里就需要注意了,由于 ULONG_MAX 也是一个合法的返回值,因此必须使用 errno 来检查是否发生错误。与此同时,对于这类函数,必须在调用这些库函数之前将 errno 的值设置为 0,然后在调用库函数之后检查 errno 的值。
函 数 | 返回值 | errno 值 |
---|---|---|
ftell() | -1L | positive |
fgetpos()、fsetpos() | nonzero | positive |
mbitowc()、mbsrtowcs() | (size_t)(-1) | EILSEQ |
signal() | SIG_ERR | positive |
wcrtomb()、wcsitombs() | (size_t)(-1) | EILSEQ |
mbrtocl6()、mbitoc32() | (size_t)(-1) | EILSEQ |
c16rtomb()、cr32rtomb() | (size_t)(-1) | EILSEQ |
如表 2 所示,对于这类函数,应该先检查它的返回值,之后如果确实需要再继续检查 errno 的值。
例如,setlocale 函数在发送错误时将返回 NULL,但却不能保证一定会设置 errno 的值。因此,在调用这类函数时,不应完全依赖于 errno 的值来确定是否发生了错误。与此同时,该函数可能会设置 errno 的值,就算是这样也不能够保证 errno 会正确地提示错误的信息。
有些函数在不同的标准中对 errno 有不同的定义。例如,fopen 函数就是一个典型的例子。在 C99 中,并没有在描述 fopen 时提到 errno,但是,POSIX.1 却声明了当 fopen 遇到一个错误时将返回 NULL,并且为 errno 设置一个值以提示这个错误。
在 C 语言中,如果系统调用或库函数被正确地执行,那么 errno 的值不会被清零。换句话说,errno 的值只有在一个库函数调用发生错误时才会被设置,当库函数调用成功运行时,errno 的值不会被修改,当然也不会主动被置为 0。也正因为如此,在实际编程中进行错误诊断会有不少问题。
例如,在一段示例代码中,首先执行函数 A 的调用,如果函数 A 在执行中发生了错误,那么 errno 的值将被修改。接下来,在不对 errno 的值做任何处理的情况下,继续直接执行函数 B 的调用,如果函数 B 被正确地执行,那么 errno 将还保留着函数 A 发生错误时被设置的值。也正是这个原因,我们不能通过测试 errno 的值来判断是否存在错误。
由此可见,在调用 errno 之前,应该首先对函数的返回值进行判断,通过对返回值的判断来检查函数的执行是否发生了错误。如果通过检查返回值确认函数调用发生了错误,那么再继续利用 errno 的值来确认究竟是什么原因导致了错误。
但是,如果一个函数调用无法从其返回值上判断是否发生了错误时,那么将只能通过 errno 的值来判断是否出错以及出错的原因。对于这种情况,必须在调用函数之前先将 errno 的值手动清零,否则,errno 的值将有可能够发生上面示例所展示的情况。
例如,当调用 fopen 函数发生错误时,它将会去修改 errno 的值,这样外部的代码就可以通过判断 errno 的值来区分 fopen 内部执行时是否发生错误,并且根据 errno 值的不同来确定具体的错误类型。如下面的示例代码所示:
int main(void)
{
/*调用errno之前必须先将其清零*/
errno=0;
FILE *fp = fopen("test.txt","r");
if(errno!=0)
{
printf("errno值: %d\n",errno);
printf("错误信息: %s\n",strerror(errno));
}
}
在这里,假设“test.txt”是一个根本不存在的文件。因此,在调用 fopen 函数尝试打开一个并不存在的文件时将发生错误,同时修改 errno 的值。这时,fopen 函数会将 errno 指向的值修改为 2。我们通过 stderror 函数可以看到错误代码“2”的意思是“No such file or directory”,如图 3 所示。
从上面的示例可以看出,使用 errno 来报告错误看起来似乎非常简单完美,但其实情况并非如此。前面也阐述过,在 C99 中,并没有在描述 fopen 时提到 errno。但是,POSIX.1 却声明了当 fopen 遇到一个错误时,它将返回 NULL,并且为 errno 设置一个值以提示这个错误,这就暗示一个遵循了 C99 但不遵循 POSIX 的程序不应该在调用 fopen 之后再继续检查 errno 的值。因此,下面的写法完全合乎要求:
int main(void)
{
FILE *fp = fopen("test.txt","r");
if(fp==NULL)
{
/*...*/
}
}
但是,上面也说过,在 POSIX 标准中,当 fopen 遇到一个错误的时候将返回 NULL,并且为 errno 设置一个值以提示这个错误。因此,在遵循 POSIX 标准中,应该首先检查 fopen 是否返回 NULL 值,如果返回,再继续检查 errno 的值以确认产生错误的具体信息,如下面的代码所示:
int main(void)
{
/*调用errno之前必须先将其清零*/
errno=0;
FILE *fp = fopen("test.txt","r");
if(fp==NULL)
{
if(errno!=0)
{
printf("errno值: %d\n",errno);
printf("错误信息:%s\n",strerror(errno));
}
}
}
其实,即使系统调用或者库函数正确执行,也不能够保证 errno 的值不会被改变。因此,在没有发生错误的情况下,fopen 也有可能修改的 errno 值。先检查 fopen 的返回值,再检查 errno 的值才是正确的做法。
除此之外,建议在使用 errno 的值之前,必须先将其值赋给另外一个变量保存起来,因为很多函数(如 fprintf)自身就可能会改变 errno 的值。
对于 errno,它是一个由 ISO C 与 POSIX 标准定义的符号。早些时候,POSIX 标准曾经将 errno 定义成“extern int errno”这种形式,但现在这种定义方式比较少见了,那是因为这种形式定义的 errno 对多线程来说是致命的。
在多线程环境下,errno 变量是被多个线程共享的,这样就可能引发如下情况:线程 A 发生某些错误而改变了 errno 的值,那么线程 B 虽然没有发生任何错误,但是当它检测 errno 的值时,线程 B 同样会以为自己发生了错误。
我们知道,在多线程环境中,多个线程共享进程地址空间,因此就要求每个线程都必须有属于自己的局部 errno,以避免一个线程干扰另一个线程。其实,现在的大多部分编译器都是通过将 errno 设置为线程局部变量的实现形式来保证线程之间的错误原因不会互相串改。
例如,在 Linux 下的 GCC 编译器中,标准的 errno 在“/usr/include/errno.h”中的定义如下:
其中,errno在“/usr/include/bits/errno.h”文件中的具体实现如下:
这样,通过“extern int*__errno_location(void)__THROW__attribute__((__const__));”与“#define errno(*__errno_location())”定义,使每个线程都有自己的 errno,不管哪个线程修改 errno 都是修改自己的局部变量,从而达到线程安全的目的。
除此之外,如果要在多线程环境下正确使用 errno,首先需要确保 __ASSEMBLER__ 没有被定义,同时 _LIBC 没被定义或定义了 _LIBC_REENTRANT。可以通过下面的程序来在自己的开发环境中测试这几个宏的设置:
int main(void)
{
#ifndef __ASSEMBLER__
printf( "__ASSEMBLER__ is not defined!\n" );
#else
printf( "__ASSEMBLER__ is defined!\n" );
#endif
#ifndef __LIBC
printf( "__LIBC is not defined\n" );
#else
printf( "__LIBC is defined!\n" );
#endif
#ifndef _LIBC_REENTRANT
printf( "_LIBC_REENTRANT is not defined\n" );
#else
printf( "_LIBC_REENTRANT is defined!\n" );
#endif
return 0;
}
该程序的运行结果为:
__ASSEMBLER__ is not defined!
__LIBC is not defined
_LIBC_REENTRANT is not defined
由此可见,在使用 errno 时,只需要在程序中简单地包含它的头文件“errno.h”即可,千万不要多此一举,在程序中重新定义它。如果在程序中定义了一个名为 errno 的标识符,其行为是未定义的。
上面已经阐述过,在 POSIX 标准中,可以通过 errno 值来检查 fopen 函数调用是否发生了错误。但是,对特定文件流操作是否出错的检查则必须使用 ferror 函数,而不能够使用 errno 进行文件流错误检查。如下面的示例代码所示:
int main(void)
{
FILE* fp=NULL;
/*调用errno之前必须先将其清零*/
errno=0;
fp = fopen("Test.txt","w");
if(fp == NULL)
{
if(errno!=0)
{
/*处理错误*/
}
}
else
{
/*错误地从fp所指定的文件中读取一个字符*/
fgetc(fp);
/*判断是否读取出错*/
if(ferror(fp))
{
/*处理错误*/
clearerr(fp);
}
fclose(fp);
return 0;
}
}