错误处理与调试:errno全局变量、perror函数、strerror函数、常见文件操作错误
文件操作,说白了就是跟操作系统打交道。你想想看,磁盘可能满了、文件可能被删了、权限可能不够——这些情况在代码里随时可能发生。我做了这么多年嵌入式开发,见过太多程序因为没处理文件错误而崩溃的案例。
嗯,这一章我们就来聊聊C语言里怎么优雅地处理文件操作中的错误。核心就是三个东西:errno、perror、strerror。
errno:系统留给你的错误码
每次系统调用失败,内核都会往一个全局变量里塞一个整数,这个变量就是errno。它定义在<errno.h>里。
我个人习惯是,每次调用fopen、fread这类函数后,立刻检查返回值。如果返回NULL或者出错,马上看errno。
#include <stdio.h>
#include <errno.h>
int main() {
FILE *fp = fopen("nonexist.txt", "r");
if (fp == NULL) {
printf("errno = %d\n", errno);
return 1;
}
fclose(fp);
return 0;
}
运行结果会打印一个数字,比如2。但2代表什么?没人记得住。所以我们需要下面两个函数。
perror:直接打印错误信息
perror是我用得最多的调试函数。它会把errno对应的错误描述打印到标准错误输出(stderr)。
FILE *fp = fopen("config.ini", "r");
if (fp == NULL) {
perror("打开配置文件失败");
return -1;
}
输出类似:打开配置文件失败: No such file or directory。你看,一目了然。
小技巧:perror的参数是你自己加的前缀,建议写清楚是哪个操作、哪个文件。我在项目中见过有人只写个"Error",排查起来特别痛苦。
strerror:把错误码转成字符串
如果你不想直接打印,想把错误信息存到日志里,或者做更精细的处理,那就用strerror。
#include <string.h>
FILE *fp = fopen("/dev/ttyUSB0", "r");
if (fp == NULL) {
char buf[256];
snprintf(buf, sizeof(buf), "打开串口失败: %s", strerror(errno));
write_log(buf); // 假设你有个写日志的函数
return -1;
}
strerror返回一个字符串指针,内容就是错误码对应的描述。注意,这个函数不是线程安全的,但在单线程场景下完全够用。
注意:errno的值在函数调用成功后不会被清零。所以一定要在检测到错误后立刻读取errno,不要等太久。我曾经踩过这个坑——先调了fopen,又调了printf,再去查errno,结果已经不是原来的值了。
常见文件操作错误一览
我把嵌入式开发中经常遇到的错误码整理了一下,你对照着看会方便很多。
| errno值 | 宏定义 | 含义 | 典型场景 |
|---|---|---|---|
| 2 | ENOENT | 文件或目录不存在 | fopen一个不存在的文件 |
| 13 | EACCES | 权限不足 | 以只读方式打开一个只写文件 |
| 16 | EBUSY | 设备或资源忙 | 打开一个已被占用的串口 |
| 22 | EINVAL | 参数无效 | fseek时传入非法偏移 |
| 24 | EMFILE | 进程打开的文件太多 | 忘记关闭文件描述符 |
| 28 | ENOSPC | 设备上没有空间 | 磁盘写满时写入文件 |
| 30 | EROFS | 只读文件系统 | 在只读分区上尝试写入 |
核心原则:每次文件操作后,先检查返回值,再决定是否读取errno。不要假设操作一定会成功——尤其是在嵌入式设备上,SD卡随时可能被拔出。
一个完整的错误处理示例
下面是我在实际项目里常用的写法,你可以直接套用这个模式。
#include <stdio.h>
#include <errno.h>
#include <string.h>
int read_config(const char *path) {
FILE *fp = fopen(path, "r");
if (fp == NULL) {
fprintf(stderr, "[ERROR] 打开文件 %s 失败: %s (errno=%d)\n",
path, strerror(errno), errno);
return -1;
}
char line[256];
while (fgets(line, sizeof(line), fp)) {
// 处理配置...
}
if (ferror(fp)) {
fprintf(stderr, "[ERROR] 读取文件时发生错误: %s\n",
strerror(errno));
fclose(fp);
return -1;
}
fclose(fp);
return 0;
}
你看,每个可能出错的地方我都做了检查。fopen失败、fgets过程中出错、ferror检测——一个都没落下。
避坑指南:我曾经犯过的错
我曾经在一个数据采集项目里,连续打开多个文件,但忘记检查fclose的返回值。结果有一次磁盘写满了,fclose返回了EOF,但我的程序完全没察觉。数据丢了不说,还导致后续的分析全错了。
从那以后,我给自己定了个规矩:所有文件操作函数,只要它有返回值,就必须检查。fclose也不例外。
if (fclose(fp) == EOF) {
perror("关闭文件失败");
// 这里可以记录日志,或者尝试重新关闭
}
个人经验:在嵌入式Linux环境下,我习惯把errno和当前文件名、行号一起打印出来。这样出问题时,看一眼日志就知道是哪个文件的哪一行出了什么错。调试效率能提高不少。
知识体系图
下面这张图把本章的核心内容串起来了,你可以对照着理解。
嗯,这一章的内容就这些了。记住:错误处理不是可有可无的装饰,而是程序健壮性的基石。每次写文件操作,顺手加上错误检查,养成习惯后你会发现,调试时间能省下一大半。
公众号:蓝海资料掘金营,微信deep3321