<errno.h> -- 宏
阅读:0       作者:严长生

errno 记录最后的错误代码

errno 用来保存最后的错误代码,它是一个宏,被展开后是一个 int 类型的数据(在单线程程序中可以认为是一个全局变量),并且当前程序拥有读取和写入的权限。

错误代码的概念

程序在运行过程中会产生各种各样的错误,我们可以给每种类型的错误分配一个唯一的编号,就像给班里的学生分配学号一样,在C语言中,我们将此称为错误代码

错误代码仅仅是一个数字,并没有额外的结构,要想获取具体的错误信息,一般有两种方案:
  • 使用 perror() 将错误信息(文本)打印到标准输出设备;
  • 使用 strerror() 将错误代码转换成对应的文本信息。

<errno.h> 头文件中有一个 errno 宏,它就用来存储错误代码,当系统调用或者函数调用发生错误时,就会将错误代码写入到 errno 中,再次读取 errno 就可以知道发生了什么错误。

errno的使用

程序刚刚启动的时候,errno 被设置为 0;程序在运行过程中,任何一个函数发生错误都有可能修改 errno 的值,让其变为一个非零值,用以告知用户发生了特定类型的错误。
标准库中的函数只会将 errno 设置为一个用以表明具体错误类型的非零值,不会再将 errno 设置回零值。
再次设置 errno 的值会覆盖以前 errno 的值,如果想得知当前函数发生了什么错误,必须在函数调用结束后立即读取 errno 的值,因为后面的函数依然有可能出错,这样就会再次修改 errno 的值,将之前 errno 的值覆盖掉,我们就再也无法得知之前的函数发生了什么错误。

另外,在调用函数前最好将 errno 的值重新设置为 0。因为之前的函数很有可能已经设置了 errno 的值,如果我们没有将 errno 设置回 0,那么即使当前的函数没有出错,我们也会读取到一个非零值,而误以为是当前函数出错,这显然是不靠谱的。

实例

获取 fopen() 函数的错误信息。
#include <stdio.h>
#include <string.h>
#include <errno.h>

int main ()
{
    errno = 0;  //将 errno 设置回 0 值
    FILE *fp = fopen("D:/demo.txt", "r");
    if(fp){
        printf("Congratulations, the file opens successfully!n");
    }else{
        printf("Error no.%d: %sn", errno, strerror(errno));
    }
    return 0;
}
如果 D 盘下没有 demo.txt 文件,那么运行结果为:
Error no.2: No such file or directory

再谈错误代码

早期的C标准(C89和C99)规定,<errno.h> 中至少还要定义 EDOM、ERANGE、EILSEQ 三个宏,它们用来表示具体的错误代码(每个宏都会被展开为一个整数)。
说明
EDOM 域错误(Domain error)。某些数学函数仅针对一定范围内的数值有意义,我们将这个范围称为域(Domain)。例如,sqrt() 函数仅能对非负数求平方根,如果我们给 sqrt() 传递一个负数,那么 sqrt() 就会将 errno 设置为 EDOM。
ERANGE 范围错误(Range error)。一个变量可以表示的数值范围是有限的,数值过大或者过小都有溢出的风险。例如,pow() 用来求一个数的次方,它的结果极有可能超出浮点类型变量的表示范围;再如,strtod() 用来将字符串转换成浮点数,也有可能会超出范围。在这些情况下,errno 都会被设置为 ERANGE。
EILSEQ 非法序列(Illegal sequence)。源自于不合法的字符序列(可以理解为字符串),当使用 mbrtowc()、wcrtomb() 等函数在多字节字符和宽字符之间进行转换时,如果遇到无效的字符,就会将 errno 设置为 EILSEQ。例如wcrtomb(buffer, L'xfffff' ,&mbs);语句中,L'xfffff'就是一个超出范围的宽字符,就是非法字符序列,wcrtomb() 就会将 errno 设置为 EILSEQ。

程序可能发生的错误有成百上千种,<errno.h> 实际定义的宏也不止上面三个,上面三个宏是C语言标准强制要求的;没有强制要求的宏随不同的平台、不同的库实现而有所不同,依据这些宏编写的代码不具有跨平台性。

对 Windows 下 errno 的说明请猛击:https://wenku.#.com/view/7df9aa51ad02de80d4d840b1.html

对 Linux 下 errno 的说明请猛击:http://blog.csdn.net/kofiory/article/details/5790409

C11 标准

三个宏还是太少了,没有太大的实用价值,C标准委员会也意识到了这个问题,所以在最新的 C11 标准中,将可移植的宏的个数扩展到了 78 个,其中包含了很多在 POSIX 环境中已经存在的名称。

多线程安全

有些教程说 errno 展开后是一个 int 类型的全局变量,如下所示:
extern int _errno;
#define errno _errno
很多单线程库确实也是这么做的;但是这种方案仅适用于单线程程序,不适用于多线程程序。
全局变量的作用范围是整个程序,是所有的源文件,而不仅限于当前的源文件。
在多线程程序中,线程之间是存在竞争的,各个线程交替使用CPU时间,将 errno 设置为全局变量会导致一个严重的问题:线程A中的函数修改了 errno 的值,还没来得及读取就挂起了,线程B获得CPU时间后又修改了 errno 的值,等到线程A“苏醒”后再读取 errno 的值时,已经找不到原来的值了,只能读取线程B设置的值,而线程A对此一无所知。

要解决这个问题,就不能定义全局范围内的 errno,而要针对每个线程定义自己的 errno,所以很多支持多线程的库都将 errno 实现为一个函数。如下所示:
#if define _MULTI_THREAD
#define errno (*_errno_func())
#endif
_MULTI_THREAD 是开启多线程后定义的宏,通过 _errno_func() 函数可以得到线程内部 errno 变量的地址,在前面加*就能够得到 errno 变量本身。
这段代码重在演示原理,使用的宏名和函数名都是假定的,真实的库实现并不使用这些名字。