错误处理: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. 知识体系:错误处理的核心逻辑

下面这张图,是我自己总结的错误处理流程。你看一遍,基本就掌握了:

C语言错误处理核心流程 调用函数 返回值 表示失败? 立即保存 errno 或记录错误 继续正常 执行流程 打印/日志/恢复 正常使用结果 结束

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是线程安全的,放心用。

嗯,错误处理这块儿,说白了就是“防患于未然”。你多花十分钟把错误处理写好,将来可能省下十个小时的调试时间。这笔账,划算。


公众号:蓝海资料掘金营,微信deep3321