错误处理:errno全局变量,函数返回错误码的约定
写C程序这么多年,我有个很深的体会:代码写得再漂亮,错误处理没做好,照样是烂摊子。你想想看,一个函数调用完,你都不知道它到底成没成功,这代码谁敢用?
今天我们就来聊聊C语言里最经典的错误处理方式——errno全局变量和函数返回错误码的约定。说白了,就是一套“出事了怎么通知你”的规矩。
1. errno:那个全局的“告示牌”
errno是个全局整型变量,定义在<errno.h>里。当系统调用或库函数出错时,内核会往这个变量里写一个错误编号。你调用完函数,赶紧查一下errno,就知道具体出了什么岔子。
核心要点: errno只在上次出错时被设置。如果后续函数调用成功了,它不会被清零。所以,正确的做法是:先判断函数返回值是否表示失败,再去看errno。
举个例子,打开文件:
#include <stdio.h>
#include <errno.h>
#include <string.h>
int main() {
FILE *fp = fopen("nonexistent.txt", "r");
if (fp == NULL) {
// 只有返回NULL时,errno才有意义
printf("打开文件失败,错误码: %d\n", errno);
printf("错误描述: %s\n", strerror(errno));
perror("fopen");
}
return 0;
}
这里我用了strerror()把错误码转成可读的字符串,perror()则直接打印“你给的提示 + 错误描述”。我个人习惯用perror(),省事。
注意: 多线程环境下,errno是线程局部存储的。每个线程有自己的errno副本,不会互相干扰。这一点在Linux和POSIX系统上已经实现好了,你只管用就行。
2. 函数返回错误码的约定
除了errno,我们自己写的函数也得有个“出错了怎么告诉你”的规矩。C语言里没有异常机制,全靠返回值传信儿。
常见的约定有这么几种:
| 返回类型 | 成功约定 | 失败约定 | 典型例子 |
|---|---|---|---|
int |
返回 0 | 返回 -1,设置 errno |
POSIX 系统调用 |
int |
返回非负值 | 返回 -1 | open() 返回文件描述符 |
指针 |
返回有效指针 | 返回 NULL |
malloc(), fopen() |
void |
无返回值 | 通过参数或全局变量 | 某些嵌入式驱动 |
我自己写库函数时,喜欢用int返回值,0表示成功,负数表示各种错误码。这样调用方可以这样写:
int ret = do_something();
if (ret != 0) {
// 处理错误,ret就是错误码
printf("操作失败,错误码: %d\n", ret);
}
这种模式清晰、简单,而且容易扩展。你想想看,如果成功返回0,失败返回-1,那具体是什么错?你还得去看errno。不如直接让返回值本身携带错误信息。
3. 我踩过的坑:错误码被覆盖
我曾经在调试一个网络程序时,遇到一个诡异的bug。明明connect()失败了,我打印errno却显示“成功”。查了半天才发现,我在判断connect()返回值之前,先调用了printf()——而printf()内部也可能设置errno!
避坑指南: 一旦检测到函数调用失败,立即保存errno的值,或者立即打印/记录错误信息。不要等,不要中间插别的函数调用。
正确的做法:
int saved_errno;
if (connect(sockfd, ...) == -1) {
saved_errno = errno; // 立刻保存
// 现在可以安全地调用其他函数了
fprintf(stderr, "connect failed: %s\n", strerror(saved_errno));
}
4. 知识体系:错误处理的核心逻辑
下面这张图,是我自己总结的错误处理流程。你看一遍,基本就掌握了:
5. 实际项目中的错误码设计
在嵌入式项目里,我习惯自己定义一套错误码枚举。比如:
typedef enum {
ERR_OK = 0, // 成功
ERR_PARAM = -1, // 参数错误
ERR_TIMEOUT = -2, // 超时
ERR_BUSY = -3, // 资源忙
ERR_NOMEM = -4, // 内存不足
ERR_HARDWARE = -5, // 硬件故障
ERR_UNKNOWN = -99 // 未知错误
} error_code_t;
然后每个函数都返回error_code_t类型。调用方拿到返回值,直接跟枚举比较,清晰得很。
我的建议: 不管你是做嵌入式还是应用层,都不要偷懒。每个函数都要有明确的错误处理策略。哪怕你暂时不知道出错后该怎么办,至少打印一行日志,留个线索。否则,线上出问题了你连从哪查起都不知道。
6. 总结一下
errno是全局的“错误告示牌”,但记得先判断返回值再读它。- 函数返回错误码的约定要统一:0成功,负数失败,或者用枚举。
- 一旦检测到错误,立刻保存
errno,别被后续调用覆盖。 - 多线程下
errno是线程安全的,放心用。
嗯,错误处理这块儿,说白了就是“防患于未然”。你多花十分钟把错误处理写好,将来可能省下十个小时的调试时间。这笔账,划算。