errno 记录最后的错误代码
errno 用来保存最后的错误代码,它是一个宏,被展开后是一个 int 类型的数据(在单线程程序中可以认为是一个全局变量),并且当前程序拥有读取和写入的权限。
错误代码仅仅是一个数字,并没有额外的结构,要想获取具体的错误信息,一般有两种方案:
<errno.h> 头文件中有一个 errno 宏,它就用来存储错误代码,当系统调用或者函数调用发生错误时,就会将错误代码写入到 errno 中,再次读取 errno 就可以知道发生了什么错误。
另外,在调用函数前最好将 errno 的值重新设置为 0。因为之前的函数很有可能已经设置了 errno 的值,如果我们没有将 errno 设置回 0,那么即使当前的函数没有出错,我们也会读取到一个非零值,而误以为是当前函数出错,这显然是不靠谱的。
Error no.2: No such file or directory
程序可能发生的错误有成百上千种,<errno.h> 实际定义的宏也不止上面三个,上面三个宏是C语言标准强制要求的;没有强制要求的宏随不同的平台、不同的库实现而有所不同,依据这些宏编写的代码不具有跨平台性。
对 Windows 下 errno 的说明请猛击:https://wenku.baidu.com/view/7df9aa51ad02de80d4d840b1.html
对 Linux 下 errno 的说明请猛击:http://blog.csdn.net/kofiory/article/details/5790409
要解决这个问题,就不能定义全局范围内的 errno,而要针对每个线程定义自己的 errno,所以很多支持多线程的库都将 errno 实现为一个函数。如下所示:
错误代码的概念
程序在运行过程中会产生各种各样的错误,我们可以给每种类型的错误分配一个唯一的编号,就像给班里的学生分配学号一样,在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: %s\n", 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.baidu.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 变量本身。
这段代码重在演示原理,使用的宏名和函数名都是假定的,真实的库实现并不使用这些名字。