第1章:返回值的艺术——函数返回值的约定俗成
做嵌入式开发这么多年,我见过太多因为返回值处理不当引发的惨案。说白了,函数返回值就是函数跟你之间的「暗号」。这个暗号怎么定,直接决定了代码好不好读、稳不稳定。
1.1 为什么是0表示成功?
这不是谁拍脑袋定的规矩。你想想看,在C语言里,0在逻辑判断中代表false,非0代表true。但函数返回值恰恰相反——我们用0表示「一切正常」,用非0表示「出问题了」。
为什么会这样?
因为错误类型太多了。一个函数可能因为参数非法失败,可能因为内存不足失败,也可能因为硬件无响应失败。如果用0表示失败,那成功该返回什么?1?2?还是-1?这显然不合理。
我个人习惯把返回值设计成错误码。0就是「没毛病」,其他数字各自代表一种错误场景。这样调用方拿到返回值,一眼就能看出问题出在哪。
1.2 返回值语义设计的三个层次
我在项目中遇到过不少返回值设计混乱的情况。有的函数返回-1表示失败,有的返回1表示失败,还有的返回NULL表示失败。这要是混在一起用,调试起来简直要命。
我建议把返回值语义分成三个层次来设计:
| 层次 | 返回值类型 | 典型场景 |
|---|---|---|
| 第一层 | int(错误码) | 初始化、配置、状态查询 |
| 第二层 | 指针(成功返回有效地址,失败返回NULL) | 内存分配、资源获取 |
| 第三层 | bool(成功true,失败false) | 简单判断、开关操作 |
嗯,这里要注意:第三层只适用于「要么成功要么失败」的简单场景。但凡有第三种可能,就别用bool。
3.3 错误码的命名规范
我曾经在一个项目里看到过这样的代码:
if (func() == 0) {
// 成功了?
} else if (func() == -1) {
// 失败了?
} else {
// 这又是啥?
}
这种写法最大的问题在于:没人知道-1代表什么。我建议用枚举或者宏定义把错误码「命名化」:
typedef enum {
ERR_OK = 0, // 成功
ERR_PARAM = -1, // 参数错误
ERR_MEMORY = -2, // 内存不足
ERR_TIMEOUT = -3, // 超时
ERR_HARDWARE = -4, // 硬件错误
ERR_BUSY = -5 // 资源忙
} ErrorCode;
这样调用方就能写出可读性很好的代码:
ErrorCode ret = sensor_init(&sensor);
if (ret != ERR_OK) {
// 根据ret的值,就知道具体是哪里出了问题
return ret;
}
1.4 返回值检查的「黄金法则」
我见过太多人写了这样的代码:
void *buf = malloc(1024);
// 直接用了,没检查返回值
memcpy(buf, data, len);
结果呢?malloc返回NULL,程序直接崩了。嵌入式系统资源有限,内存分配失败是家常便饭。不检查返回值,就是在给自己埋雷。
我总结了一条「黄金法则」:每个函数的返回值都必须被检查,除非你100%确定它不会失败。
但说实话,在嵌入式开发中,几乎没有「100%不会失败」的情况。所以我的做法是:
- 系统初始化函数:必须检查返回值,失败就报错或重启
- 资源分配函数:必须检查返回值,失败就回滚操作
- 硬件操作函数:必须检查返回值,失败就重试或报错
- 纯计算函数:可以酌情不检查(比如求绝对值)
1.5 返回值与全局错误码的取舍
有些库喜欢用全局错误码(比如errno)来传递错误信息。我个人不太推荐这种做法,原因有三:
- 线程不安全: 多线程环境下,一个线程的错误码可能被另一个线程覆盖
- 可读性差: 调用方得先检查返回值,再查全局错误码,两步操作容易漏
- 调试困难: 全局变量在调试时容易被意外修改
我建议尽量用返回值传递错误信息。如果确实需要传递额外信息,可以用输出参数:
// 不推荐
int do_something();
int get_last_error(); // 还得再调一个函数才能知道具体错误
// 推荐
int do_something(int *out_error_detail); // 一次调用,拿到所有信息
1.6 本章知识体系
下面这张图总结了返回值设计的核心逻辑:
这张图把返回值设计的核心逻辑串起来了。你写代码的时候,脑子里过一遍这个图,基本就不会跑偏。
1.7 避坑指南
最后,我把自己踩过的坑整理一下,你写代码时多留个心眼:
- 别混用正负: 要么全用正数表示错误,要么全用负数。混着用调试起来想死的心都有
- 别用魔法数字: 代码里出现 return -1; 这种,三个月后你自己都看不懂
- 别忽略返回值: 哪怕你觉得「这个函数肯定不会失败」,也请加上检查。嵌入式硬件太不可控了
- 别用返回值传数据: 返回值就是用来传状态的,别用它传业务数据。数据用输出参数传
好了,关于返回值的艺术,今天就聊到这。记住一句话:清晰的返回值语义,是高质量C代码的基石。