3. 返回值检查
好,咱们接着聊错误处理。这一章我要讲的是最基础、也是最容易被忽视的一招——返回值检查。
你想想看,C语言不像Java有try-catch,也不像Python有异常机制。C语言怎么告诉你“我出错了”?说白了,就是靠返回值。函数执行完了,给你一个数字,或者一个指针,告诉你结果怎么样。你要是没接住这个信号,那后面的事就不好说了。
3.1 函数返回值约定:C语言的“潜规则”
C语言里,函数返回值的含义其实有一套约定俗成的规矩。我个人习惯把它们分成三类:
- 成功/失败指示器:返回0表示成功,非0表示失败。比如很多标准库函数就是这么干的。
- 数据值:返回计算结果本身。比如
getchar()返回读到的字符。 - 指针:返回内存地址。成功时返回有效指针,失败时返回
NULL。
嗯,这里要注意:同一个函数,返回值只能表达一种语义。你不能让一个函数既返回错误码,又返回计算结果。否则调用方会疯掉的。
核心原则:函数返回值的含义必须清晰、唯一、文档化。调用方看一眼函数签名,就应该知道怎么处理返回值。
3.2 检查标准库函数返回值:别偷懒
我在项目中遇到过最典型的bug,就是有人不检查fopen()的返回值。直接拿着文件指针就开始读写,结果程序崩溃了还不知道为什么。
标准库函数几乎都有返回值,而且大部分都会用返回值告诉你执行结果。我列几个常见的:
| 函数 | 成功返回值 | 失败返回值 | 错误信息 |
|---|---|---|---|
fopen() |
文件指针 | NULL |
errno |
malloc() |
分配的内存指针 | NULL |
errno (ENOMEM) |
fread() |
实际读取的字节数 | 小于请求的字节数 | feof()/ferror() |
printf() |
输出的字符数 | 负数 | errno |
scanf() |
成功匹配的项数 | EOF 或 0 |
errno |
你看,每个函数都有自己的一套返回值规则。不检查?那就是在赌运气。
避坑指南:我曾经接手过一个项目,里面有个模块用了上百次malloc(),但没有一次检查返回值。结果在内存紧张的环境下,程序莫名其妙地崩溃。后来我花了整整两天,才把所有malloc()的返回值检查补上。从那以后,我写代码的第一反应就是:任何可能失败的函数,返回值必须检查。
正确的做法是这样的:
FILE *fp = fopen("data.txt", "r");
if (fp == NULL) {
fprintf(stderr, "打开文件失败: %s\n", strerror(errno));
return -1;
}
char *buffer = (char *)malloc(1024);
if (buffer == NULL) {
fprintf(stderr, "内存分配失败\n");
fclose(fp);
return -1;
}
看到了吗?每一步都要检查。而且失败时要记得释放已经分配的资源——这就是后面要讲的资源管理。
3.3 自定义函数的错误返回设计
写自己的函数时,怎么设计返回值?我建议遵循一个原则:让调用方用起来舒服。
举个例子,假设你要写一个除法函数:
// 不好的设计:用返回值传结果,用参数传错误
int divide_bad(int a, int b, int *result) {
if (b == 0) {
return -1; // 返回错误码
}
*result = a / b;
return 0; // 返回成功
}
// 好的设计:用返回值传错误,用参数传结果
int divide_good(int a, int b, int *result) {
if (result == NULL) {
return -1; // 参数检查
}
if (b == 0) {
return -2; // 除零错误
}
*result = a / b;
return 0; // 成功
}
我个人习惯用返回值表示状态,用指针参数传出结果。这样调用方可以这样写:
int result;
int ret = divide_good(10, 0, &result);
if (ret != 0) {
// 处理错误
printf("除法失败,错误码: %d\n", ret);
} else {
printf("结果是: %d\n", result);
}
你看,逻辑是不是很清晰?
3.4 错误码枚举:给错误一个名字
直接返回数字有个问题——没人记得住-1、-2、-3分别代表什么。所以我们需要枚举。
枚举说白了就是给数字起个名字。比如:
typedef enum {
ERR_SUCCESS = 0, // 成功
ERR_NULL_POINTER = -1, // 空指针
ERR_DIVIDE_BY_ZERO = -2, // 除零
ERR_OUT_OF_MEMORY = -3, // 内存不足
ERR_FILE_NOT_FOUND = -4, // 文件未找到
ERR_INVALID_PARAM = -5 // 无效参数
} ErrorCode;
然后你的函数就可以这样写:
ErrorCode read_config(const char *filename, Config *config) {
if (filename == NULL || config == NULL) {
return ERR_NULL_POINTER;
}
FILE *fp = fopen(filename, "r");
if (fp == NULL) {
return ERR_FILE_NOT_FOUND;
}
// ... 读取配置 ...
fclose(fp);
return ERR_SUCCESS;
}
调用方看到ERR_FILE_NOT_FOUND,一眼就知道是文件没找到。比看到-4强多了,对吧?
小技巧:我习惯在枚举里把成功码放在最前面,值为0。这样if (func())就可以判断是否失败——因为成功时返回0,在C语言里0表示false。当然,这取决于你的个人风格,但保持一致性很重要。
最后,我画了一张图,帮你理清返回值检查的整个流程:
这张图其实就讲了一件事:调用函数 → 检查返回值 → 成功继续,失败处理。就这么简单,但能做到的人真不多。
好了,这一章就到这里。记住:返回值检查是C语言错误处理的第一道防线。别偷懒,别侥幸,该检查的一个都不能少。