第27讲:错误处理——errno 和 perror 的使用

说实话,C语言里有个让新手特别头疼的问题——错误处理。

很多语言都有异常机制,try-catch一包,出错了直接跳转。但C语言呢?没有。它把错误处理的责任全丢给了程序员。你写个文件操作、网络通信、内存分配,哪个不得检查返回值?

我刚开始做嵌入式那会儿,就吃过这个亏。有一次写一个串口通信程序,fopen 返回了 NULL,我没检查,直接往下写数据。结果呢?程序跑飞了,整个系统死机。后来查了半天,才发现是文件路径写错了。

嗯,从那以后,我养成了一个习惯:任何系统调用,必须检查返回值。而且,不光要检查,还要知道错在哪。这就引出了今天的主角——errnoperror

什么是 errno?

errno 是一个全局整型变量,定义在 <errno.h> 里。当系统调用或库函数出错时,内核会设置这个变量,告诉你具体的错误原因。

比如,你打开一个不存在的文件,errno 会被设为 ENOENT(2)。你试图往一个只读文件里写数据,errno 就是 EACCES(13)。

每个错误码都对应一个宏定义,比如:

#define EPERM   1   /* Operation not permitted */
#define ENOENT  2   /* No such file or directory */
#define EINTR   4   /* Interrupted system call */
#define EIO     5   /* I/O error */
#define EACCES  13  /* Permission denied */
#define EEXIST  17  /* File exists */

这些宏都在 <errno.h> 里。你直接拿来用就行。

注意:errno 的值只在出错时才有意义。如果函数调用成功,errno 不会被清零。所以,永远不要用 errno 来判断是否出错,而应该用函数的返回值。

perror 是什么?

perror 是标准库提供的一个函数,原型在 <stdio.h> 里:

void perror(const char *s);

它的作用很简单:把 errno 对应的错误信息打印出来。你传一个字符串进去,它会先打印这个字符串,再打印冒号和空格,最后打印错误描述。

比如:

perror("open file failed");

如果 errnoENOENT,输出就是:

open file failed: No such file or directory

你看,一目了然。比你自己去查错误码方便多了。

一个完整的例子

咱们写个实际点的代码。打开一个文件,如果失败,打印错误信息:

#include <stdio.h>
#include <errno.h>

int main() {
    FILE *fp = fopen("/tmp/nonexistent.txt", "r");
    if (fp == NULL) {
        perror("fopen failed");
        printf("errno = %d\n", errno);
        return 1;
    }
    fclose(fp);
    return 0;
}

运行结果:

fopen failed: No such file or directory
errno = 2

我个人习惯,在嵌入式开发中,perror 用得特别多。因为嵌入式设备往往没有图形界面,调试全靠串口打印。你打印一行清晰的错误信息,比看一堆十六进制错误码强多了。

strerror 函数

除了 perror,还有一个 strerror 函数,它把错误码转换成字符串:

#include <string.h>
char *strerror(int errnum);

用法:

printf("Error: %s\n", strerror(errno));

perror 的区别?perror 直接打印到 stderr,strerror 返回字符串,你可以自己决定怎么处理。比如写到日志文件里:

fprintf(log_file, "Error: %s\n", strerror(errno));

常见陷阱

我见过不少新手犯的错,这里列几个:

  1. 忘记包含头文件errno 需要 <errno.h>perror 需要 <stdio.h>。少一个,编译就报错。
  2. 在成功调用后检查 errno:errno 不会自动清零。你上一个函数失败了,errno 被设为 EACCES。下一个函数成功了,errno 还是 EACCES。你检查 errno,以为又出错了。其实没有。
  3. 多线程环境:标准 C 里,errno 是全局变量。多线程下,一个线程的 errno 可能被另一个线程覆盖。好在现代系统(Linux、Windows)都支持线程局部存储,每个线程有自己的 errno。但如果你用老旧的嵌入式 RTOS,就得小心了。
我的建议:在嵌入式开发中,如果 RTOS 不支持线程局部 errno,就自己封装一个错误码变量。每个任务(线程)维护自己的错误码,互不干扰。

知识体系图

下面这张图,帮你理清 errno 和 perror 的关系:

C语言错误处理:errno 与 perror 系统调用/库函数 调用失败 设置 errno 为错误码 perror("提示") 打印到 stderr strerror(errno) 返回字符串 控制台输出错误信息 写入日志 / 自定义处理

实际项目中的用法

我在做嵌入式 Linux 项目时,经常写这样的错误处理宏:

#include <stdio.h>
#include <errno.h>
#include <string.h>

#define CHECK(cond, msg) \
    do { \
        if (!(cond)) { \
            fprintf(stderr, "%s:%d: ", __FILE__, __LINE__); \
            perror(msg); \
            exit(EXIT_FAILURE); \
        } \
    } while(0)

int main() {
    FILE *fp = fopen("/dev/ttyS0", "r");
    CHECK(fp != NULL, "open serial port");

    // 正常使用...
    fclose(fp);
    return 0;
}

这个宏的好处是:出错时,不仅打印错误信息,还打印文件名和行号。调试时一眼就能定位问题。

你想想看,如果项目里有几百个文件,每个文件都有几十处系统调用。没有这种统一的错误处理,出错了你都不知道从哪查起。

避坑指南

我曾经在一个项目中,遇到一个特别隐蔽的 bug。程序跑着跑着,突然打不开文件了。我检查了文件路径、权限,都没问题。后来用 perror 一打,发现错误是 EMFILE(24)——打开的文件太多。

原来,程序里有个循环,每次迭代都 fopen 但不 fclose。文件描述符泄漏了。如果没有 perror,我可能还在傻乎乎地检查文件路径呢。

所以,我的建议是:每个系统调用后面,都跟一个错误检查。哪怕你觉得“这不可能出错”,也要检查。因为现实往往比你想的更复杂。

核心要点:
  • errno 记录最后一次系统调用的错误码
  • perror 把 errno 转成可读字符串并打印到 stderr
  • strerror 把错误码转成字符串,灵活处理
  • 永远用函数返回值判断是否出错,不要依赖 errno
  • 多线程下注意 errno 的线程安全性

好了,关于 errno 和 perror 就讲这么多。记住一句话:错误处理不是可有可无的装饰,而是程序的护城河。你多写一行检查,可能就少熬一个通宵的调试。


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