第27讲:错误处理——errno 和 perror 的使用
说实话,C语言里有个让新手特别头疼的问题——错误处理。
很多语言都有异常机制,try-catch一包,出错了直接跳转。但C语言呢?没有。它把错误处理的责任全丢给了程序员。你写个文件操作、网络通信、内存分配,哪个不得检查返回值?
我刚开始做嵌入式那会儿,就吃过这个亏。有一次写一个串口通信程序,fopen 返回了 NULL,我没检查,直接往下写数据。结果呢?程序跑飞了,整个系统死机。后来查了半天,才发现是文件路径写错了。
嗯,从那以后,我养成了一个习惯:任何系统调用,必须检查返回值。而且,不光要检查,还要知道错在哪。这就引出了今天的主角——errno 和 perror。
什么是 errno?
errno 是一个全局整型变量,定义在 <errno.h> 里。当系统调用或库函数出错时,内核会设置这个变量,告诉你具体的错误原因。
比如,你打开一个不存在的文件,errno 会被设为 ENOENT(2)。你试图往一个只读文件里写数据,errno 就是 EACCES(13)。
每个错误码都对应一个宏定义,比如:
#define EPERM 1 /* Operation not permitted */
#define ENOENT 2 /* No such file or directory */
#define EINTR 4 /* Interrupted system call */
#define EIO 5 /* I/O error */
#define EACCES 13 /* Permission denied */
#define EEXIST 17 /* File exists */
这些宏都在 <errno.h> 里。你直接拿来用就行。
perror 是什么?
perror 是标准库提供的一个函数,原型在 <stdio.h> 里:
void perror(const char *s);
它的作用很简单:把 errno 对应的错误信息打印出来。你传一个字符串进去,它会先打印这个字符串,再打印冒号和空格,最后打印错误描述。
比如:
perror("open file failed");
如果 errno 是 ENOENT,输出就是:
open file failed: No such file or directory
你看,一目了然。比你自己去查错误码方便多了。
一个完整的例子
咱们写个实际点的代码。打开一个文件,如果失败,打印错误信息:
#include <stdio.h>
#include <errno.h>
int main() {
FILE *fp = fopen("/tmp/nonexistent.txt", "r");
if (fp == NULL) {
perror("fopen failed");
printf("errno = %d\n", errno);
return 1;
}
fclose(fp);
return 0;
}
运行结果:
fopen failed: No such file or directory
errno = 2
我个人习惯,在嵌入式开发中,perror 用得特别多。因为嵌入式设备往往没有图形界面,调试全靠串口打印。你打印一行清晰的错误信息,比看一堆十六进制错误码强多了。
strerror 函数
除了 perror,还有一个 strerror 函数,它把错误码转换成字符串:
#include <string.h>
char *strerror(int errnum);
用法:
printf("Error: %s\n", strerror(errno));
和 perror 的区别?perror 直接打印到 stderr,strerror 返回字符串,你可以自己决定怎么处理。比如写到日志文件里:
fprintf(log_file, "Error: %s\n", strerror(errno));
常见陷阱
我见过不少新手犯的错,这里列几个:
- 忘记包含头文件:
errno需要<errno.h>,perror需要<stdio.h>。少一个,编译就报错。 - 在成功调用后检查 errno:errno 不会自动清零。你上一个函数失败了,errno 被设为 EACCES。下一个函数成功了,errno 还是 EACCES。你检查 errno,以为又出错了。其实没有。
- 多线程环境:标准 C 里,errno 是全局变量。多线程下,一个线程的 errno 可能被另一个线程覆盖。好在现代系统(Linux、Windows)都支持线程局部存储,每个线程有自己的 errno。但如果你用老旧的嵌入式 RTOS,就得小心了。
知识体系图
下面这张图,帮你理清 errno 和 perror 的关系:
实际项目中的用法
我在做嵌入式 Linux 项目时,经常写这样的错误处理宏:
#include <stdio.h>
#include <errno.h>
#include <string.h>
#define CHECK(cond, msg) \
do { \
if (!(cond)) { \
fprintf(stderr, "%s:%d: ", __FILE__, __LINE__); \
perror(msg); \
exit(EXIT_FAILURE); \
} \
} while(0)
int main() {
FILE *fp = fopen("/dev/ttyS0", "r");
CHECK(fp != NULL, "open serial port");
// 正常使用...
fclose(fp);
return 0;
}
这个宏的好处是:出错时,不仅打印错误信息,还打印文件名和行号。调试时一眼就能定位问题。
你想想看,如果项目里有几百个文件,每个文件都有几十处系统调用。没有这种统一的错误处理,出错了你都不知道从哪查起。
避坑指南
我曾经在一个项目中,遇到一个特别隐蔽的 bug。程序跑着跑着,突然打不开文件了。我检查了文件路径、权限,都没问题。后来用 perror 一打,发现错误是 EMFILE(24)——打开的文件太多。
原来,程序里有个循环,每次迭代都 fopen 但不 fclose。文件描述符泄漏了。如果没有 perror,我可能还在傻乎乎地检查文件路径呢。
所以,我的建议是:每个系统调用后面,都跟一个错误检查。哪怕你觉得“这不可能出错”,也要检查。因为现实往往比你想的更复杂。
- errno 记录最后一次系统调用的错误码
- perror 把 errno 转成可读字符串并打印到 stderr
- strerror 把错误码转成字符串,灵活处理
- 永远用函数返回值判断是否出错,不要依赖 errno
- 多线程下注意 errno 的线程安全性
好了,关于 errno 和 perror 就讲这么多。记住一句话:错误处理不是可有可无的装饰,而是程序的护城河。你多写一行检查,可能就少熬一个通宵的调试。
公众号:蓝海资料掘金营,微信deep3321