errno 是什么?
errno (error number) 是一个在 C 语言标准库中广泛使用的全局整型变量,它的作用是:当一个库函数调用失败时,该函数会将 errno 设置为一个特定的非零整数值,以表示错误的具体类型。
你可以把它想象成一个“错误代码本”,当一个函数(比如打开文件、分配内存等)执行失败时,它不会返回一个有用的结果,而是会往这本“代码本”里记下一个错误编号,你的程序在函数返回后,可以检查这个“代码本”,就知道到底发生了什么问题。
errno 的关键特性
a. 它是一个全局变量
errno 通常被定义为一个宏,展开为一个可以被修改的整数,在大多数实现中,它被声明在头文件 <errno.h> 中。
// 在 <errno.h> 中通常是这样定义的(简化版) extern int errno;
extern 关键字表示它是一个外部变量,定义在别处(通常是 C 标准库的实现文件中)。
b. 它的初始值是 0
在程序开始执行时,errno 的初始值通常是 0。0 在 errno 的上下文中代表“没有错误”。
c. 成功时,errno 不会被自动重置为 0
这是一个非常重要的点!如果一个函数调用成功了,它不会去修改 errno 的值,这意味着,errno 的值可能来自上一次的调用。在检查 errno 之前,你必须确保你刚刚调用的函数可能失败了。
d. 失败时,函数会设置 errno
如果一个库函数报告失败,它负责将 errno 设置为一个非零值,不同的错误对应不同的宏定义。
如何使用 errno?
使用 errno 的标准流程如下:
- 包含头文件:
#include <errno.h> - 调用可能失败的函数。
- 检查函数的返回值:如果返回值表示失败(
fopen返回NULL),则继续下一步。 - 检查
errno的值:使用if (errno != 0)来确认确实发生了错误。 - 解释错误:通过
<errno.h>中定义的宏(如ENOENT,ENOMEM)来判断具体的错误类型。 - 处理错误:打印错误信息、进行清理、退出程序等。
- (可选但推荐)重置
errno:在处理完错误后,可以手动将errno重置为 0,以避免它影响后续的错误检查。
一个完整的例子:fopen
这个例子展示了如何使用 errno 来处理文件打开失败的情况。
#include <stdio.h>
#include <errno.h> // 1. 包含 errno.h
#include <string.h> // 用于 strerror()
int main() {
FILE *fp;
// 2. 尝试打开一个不存在的文件
fp = fopen("non_existent_file.txt", "r");
// 3. 检查返回值
if (fp == NULL) {
// 4. 检查 errno 是否被设置
if (errno != 0) {
// 5. 解释错误
printf("错误:无法打开文件 'non_existent_file.txt',\n");
printf("错误代码 (errno): %d\n", errno);
// 使用 perror() 或 strerror() 获取可读的错误信息
// perror() 会自动打印 "错误:无法打开文件..." 然后加上系统错误信息
perror("perror 输出");
// strerror() 直接返回错误信息的字符串
printf("strerror 输出: %s\n", strerror(errno));
}
return 1; // 返回非零表示程序出错
}
// 如果文件成功打开,这里会执行
printf("文件打开成功!\n");
fclose(fp);
return 0;
}
编译并运行这个程序,你会得到类似以下的输出:
错误:无法打开文件 'non_existent_file.txt'。
错误代码 (errno): 2
perror 输出: No such file or directory
strerror 输出: No such file or directory
在这个例子中,fopen 失败并返回 NULL,我们检查到 fp 为 NULL,然后发现 errno 的值是 2。errno.h 中定义了 #define ENOENT 2,这个宏代表 "No such file or directory"(没有那个文件或目录)。perror 和 strerror 函数帮助我们将这个数字转换成了人类可读的字符串。
常见的 errno 宏定义
<errno.h> 定义了大量的错误码宏,以下是一些最常见和最重要的:
| 宏 | 值 ( | 描述 |
|---|---|---|
ENOENT |
2 | No such file or directory (文件或目录不存在) |
ENOMEM |
12 | Out of memory (内存不足) |
EINVAL |
22 | Invalid argument (无效参数) |
EAGAIN |
11 | Resource temporarily unavailable (资源暂时不可用,常用于非阻塞 I/O) |
EACCES |
13 | Permission denied (权限被拒绝) |
EBADF |
9 | Bad file number (坏的文件描述符) |
ENOMEM |
12 | Out of memory (内存不足) |
EEXIST |
17 | File exists (文件已存在,creat 创建一个已存在的文件) |
重要注意事项和最佳实践
-
检查返回值,再检查
errno:errno只在函数报告失败时才有意义,如果一个函数成功,errno的值是未定义的(可能还是上一次的错误)。 -
线程安全:在多线程程序中,
errno是一个全局变量,这会导致严重的竞态条件(Race Condition),如果两个线程同时调用一个可能设置errno的函数,一个线程的错误可能会被另一个线程覆盖。- 解决方案:现代 C 标准(C11)引入了
errno的线程安全版本。errno被定义为一个宏,它指向一个线程局部存储的变量,在支持 C11 的编译器和库中,你通常不需要做任何额外的事情,但在旧环境中,你可能需要使用errno的线程安全替代品,或者使用perror(它本身是线程安全的)。
- 解决方案:现代 C 标准(C11)引入了
-
不要依赖
errno的值:只有文档明确说明某个函数会在失败时设置errno,你才能去检查它,不要随意猜测。 -
perror()是你的好朋友:perror()函数会打印你传入的字符串,然后跟着一个冒号和空格,最后是errno对应的错误信息(就像上面的例子一样),它比手动printf和strerror更方便、更标准。 -
重置
errno:在某些复杂的逻辑中,你可能想在处理一个错误后重置它,以防止它干扰后续的错误检查,这很简单:errno = 0;。
| 特性 | 描述 |
|---|---|
| 是什么 | 一个全局整型变量,用于存储最近一次库函数调用的错误代码。 |
| 何时使用 | 在一个函数调用失败后,通过检查 errno 来确定具体的错误原因。 |
| 如何使用 | 包含 <errno.h> -> 调用函数 -> 检查返回值 -> 检查 errno -> 使用 perror() 或 strerror() 获取信息。 |
| 核心原则 | 成功的函数不会修改 errno;失败的函数会设置 errno。 |
| 多线程 | 在旧环境中不安全;在现代 C11 环境中通常是线程安全的。 |
errno 是 C 语言进行错误处理的基础工具,理解它的正确用法对于编写健壮、可维护的 C 程序至关重要。
