错误处理:errno与perror、strerror、自定义错误码、断言assert
说实话,错误处理是C语言里最容易被忽视、却又最致命的一环。我见过太多线上事故,根源就是「返回值没检查,错误码没处理」。你想想看,一个函数调用失败了,程序却假装没事继续往下跑——这就像开车时仪表盘报警灯亮了,你把它用胶带贴上继续开。迟早要出事。
这一章,我们就来聊聊C语言里那套经典的错误处理机制。errno、perror、strerror、自定义错误码,还有那个让人又爱又恨的assert。我会把我在项目中踩过的坑、积累的经验,一并倒给你。
errno:系统给你的错误暗号
每个线程都有一个全局变量叫 errno。当系统调用或库函数失败时,内核会往这个变量里塞一个整数,告诉你「兄弟,刚才出啥事了」。
比如你打开一个不存在的文件:
#include <stdio.h>
#include <errno.h>
int main() {
FILE *fp = fopen("no_such_file.txt", "r");
if (fp == NULL) {
printf("errno = %d\n", errno);
}
return 0;
}
输出:errno = 2。2 代表什么?ENOENT,No such file or directory。
这里有个坑,我当年就栽过:errno 只在函数失败时才有意义。如果函数成功了,errno 的值是未定义的。千万别在成功路径上读 errno,否则你会拿到上次失败留下的脏数据。
errno = 0; 然后再调用,最后检查。
perror 与 strerror:把错误码翻译成人话
errno 是个整数,正常人记不住 2 代表什么。所以标准库给了我们两个翻译官:
- perror:直接往 stderr 打印错误信息,你只需要传一个前缀字符串。
- strerror:返回错误码对应的字符串,你可以自己决定怎么输出。
看个例子:
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main() {
FILE *fp = fopen("no_such_file.txt", "r");
if (fp == NULL) {
// 方式一:perror
perror("fopen failed");
// 方式二:strerror
fprintf(stderr, "fopen failed: %s\n", strerror(errno));
}
return 0;
}
输出:
fopen failed: No such file or directory
fopen failed: No such file or directory
我个人习惯用 perror,因为它少写一行代码。但如果你需要把错误信息记录到日志文件里,或者做国际化处理,那 strerror 更灵活。
strerror_r(线程安全版本)。
自定义错误码:打造你自己的错误体系
系统提供的 errno 只有几十个,根本不够用。你写一个库、一个工具,肯定有自己的错误场景。比如「配置文件格式错误」「网络连接超时」「内存池耗尽」——这些系统 errno 可不会告诉你。
我的做法是:
- 定义一个枚举,把所有可能的错误码列出来。
- 留一个范围给系统错误(比如 0~255),自定义错误从 256 开始。
- 写一个类似 strerror 的函数,把自定义错误码翻译成字符串。
代码示例:
#include <stdio.h>
// 自定义错误码
enum my_error {
ERR_OK = 0,
ERR_CONFIG = 256, // 配置文件错误
ERR_NETWORK = 257, // 网络错误
ERR_MEMPOOL = 258, // 内存池耗尽
ERR_TIMEOUT = 259, // 操作超时
};
// 错误码翻译
const char* my_strerror(int err) {
switch (err) {
case ERR_OK: return "success";
case ERR_CONFIG: return "config file error";
case ERR_NETWORK: return "network error";
case ERR_MEMPOOL: return "memory pool exhausted";
case ERR_TIMEOUT: return "operation timeout";
default: return "unknown error";
}
}
int main() {
int ret = ERR_CONFIG;
printf("Error: %s\n", my_strerror(ret));
return 0;
}
这样设计的好处是:你的错误码和系统 errno 不会冲突。而且你可以把系统 errno 也纳入自己的体系——比如 ERR_SYSTEM_BASE + errno,统一管理。
断言 assert:调试时的守护神
assert 是个宏,定义在 <assert.h> 里。它的作用很简单:如果条件为假,就打印错误信息并终止程序。
#include <assert.h>
int divide(int a, int b) {
assert(b != 0); // 除数不能为0
return a / b;
}
assert 只在 NDEBUG 宏未定义时生效。发布版本里,通常会在编译时加上 -DNDEBUG,这样 assert 就变成空操作了。
嗯,这里要注意:assert 里不要写有副作用的代码。比如:
// 千万别这么写!
assert(ptr = malloc(100)); // 在 release 版本里,malloc 根本不会执行!
我曾经在一个项目里看到有人这么写,结果线上版本疯狂崩溃,查了两天才发现是 assert 把内存分配给吞了。从那以后,我定了个规矩:assert 里只放布尔表达式,不放函数调用。
SVG 知识结构图
下面这张图,把本章的核心知识点串起来了。你可以看到错误处理的完整链路:从错误发生,到 errno 记录,再到 perror/strerror 输出,以及自定义错误码和 assert 的定位。
实战建议:构建统一的错误处理策略
说了这么多,到底怎么用?我分享一套我自己在项目中沉淀下来的做法:
- 所有函数都返回错误码。成功返回 0,失败返回负值或正错误码。别搞什么「成功返回 1,失败返回 0」——那跟系统习惯反着来,容易乱。
- 错误码用枚举定义,注释写清楚每个码的含义。别用魔数。
- 提供统一的错误打印函数,内部调用 strerror 或 perror。这样整个项目的错误输出风格一致。
- assert 只用于「绝不可能发生」的情况。比如指针不应该为 NULL、除数不应该为 0。别拿 assert 做参数校验——那应该返回错误码。
- 日志里永远记录 errno 和对应的字符串。光记个数字,回头查问题还得翻头文件,太蠢了。
LOG_ERROR(fmt, ...) 宏,自动带上 __FILE__、__LINE__ 和 strerror(errno)。这样出问题一眼就能定位。
错误处理这件事,说白了就是「别让错误静默地溜走」。你每忽略一个错误码,就是在给未来的自己埋一颗雷。相信我,你迟早会踩上去的。
公众号:蓝海资料掘金营,微信deep3321