第1章:返回值的艺术——函数返回值的约定俗成

做嵌入式开发这么多年,我见过太多因为返回值处理不当引发的惨案。说白了,函数返回值就是函数跟你之间的「暗号」。这个暗号怎么定,直接决定了代码好不好读、稳不稳定。

1.1 为什么是0表示成功?

这不是谁拍脑袋定的规矩。你想想看,在C语言里,0在逻辑判断中代表false,非0代表true。但函数返回值恰恰相反——我们用0表示「一切正常」,用非0表示「出问题了」。

为什么会这样?

因为错误类型太多了。一个函数可能因为参数非法失败,可能因为内存不足失败,也可能因为硬件无响应失败。如果用0表示失败,那成功该返回什么?1?2?还是-1?这显然不合理。

我个人习惯把返回值设计成错误码。0就是「没毛病」,其他数字各自代表一种错误场景。这样调用方拿到返回值,一眼就能看出问题出在哪。

核心原则: 0 = 成功,非0 = 失败,不同非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;
}
小技巧: 错误码最好从负数开始,正数留给扩展用。0永远表示成功。

1.4 返回值检查的「黄金法则」

我见过太多人写了这样的代码:

void *buf = malloc(1024);
// 直接用了,没检查返回值
memcpy(buf, data, len);

结果呢?malloc返回NULL,程序直接崩了。嵌入式系统资源有限,内存分配失败是家常便饭。不检查返回值,就是在给自己埋雷。

我总结了一条「黄金法则」:每个函数的返回值都必须被检查,除非你100%确定它不会失败。

但说实话,在嵌入式开发中,几乎没有「100%不会失败」的情况。所以我的做法是:

  • 系统初始化函数:必须检查返回值,失败就报错或重启
  • 资源分配函数:必须检查返回值,失败就回滚操作
  • 硬件操作函数:必须检查返回值,失败就重试或报错
  • 纯计算函数:可以酌情不检查(比如求绝对值)
警告: 千万别用「这个函数肯定不会失败」来麻痹自己。我在项目中吃过这个亏,一个「肯定不会失败」的GPIO配置函数,因为硬件引脚被复用,硬是返回了错误。从那以后,我再也不敢跳过返回值检查了。

1.5 返回值与全局错误码的取舍

有些库喜欢用全局错误码(比如errno)来传递错误信息。我个人不太推荐这种做法,原因有三:

  1. 线程不安全: 多线程环境下,一个线程的错误码可能被另一个线程覆盖
  2. 可读性差: 调用方得先检查返回值,再查全局错误码,两步操作容易漏
  3. 调试困难: 全局变量在调试时容易被意外修改

我建议尽量用返回值传递错误信息。如果确实需要传递额外信息,可以用输出参数:

// 不推荐
int do_something();
int get_last_error();  // 还得再调一个函数才能知道具体错误

// 推荐
int do_something(int *out_error_detail);  // 一次调用,拿到所有信息

1.6 本章知识体系

下面这张图总结了返回值设计的核心逻辑:

返回值设计核心逻辑 函数返回值 返回 0 (ERR_OK) 返回非0 (错误码) 返回 NULL 或地址 表示操作正常完成 不同值代表不同错误类型 成功返回有效地址 核心原则:每个返回值都必须被检查 0 = 成功,非0 = 失败,不同非0值代表不同错误

这张图把返回值设计的核心逻辑串起来了。你写代码的时候,脑子里过一遍这个图,基本就不会跑偏。

1.7 避坑指南

最后,我把自己踩过的坑整理一下,你写代码时多留个心眼:

  • 别混用正负: 要么全用正数表示错误,要么全用负数。混着用调试起来想死的心都有
  • 别用魔法数字: 代码里出现 return -1; 这种,三个月后你自己都看不懂
  • 别忽略返回值: 哪怕你觉得「这个函数肯定不会失败」,也请加上检查。嵌入式硬件太不可控了
  • 别用返回值传数据: 返回值就是用来传状态的,别用它传业务数据。数据用输出参数传

好了,关于返回值的艺术,今天就聊到这。记住一句话:清晰的返回值语义,是高质量C代码的基石。

可可精品资料库