21、宏与错误处理:错误码宏,异常模拟(setjmp/longjmp 包装),资源清理宏(RAII 模拟)

错误处理这事儿,在C语言里一直是个老大难。不像C++有try-catch,Java有异常机制,咱们C程序员只能靠返回值、错误码、还有一堆if-else硬扛。但说实话,用宏配合setjmp/longjmp,咱们也能整出一套像模像样的异常处理框架来。我在几个嵌入式项目里都这么干过,效果还不错。

21.1 错误码宏:让错误处理更规范

先说说错误码。很多新手喜欢直接返回-1或者0,但这样可读性太差了。你想想看,三个月后回头看代码,你知道-1代表什么?我建议用宏定义一套错误码体系。

// 错误码定义
#define ERR_SUCCESS     0
#define ERR_INVALID_PARAM  -1
#define ERR_TIMEOUT        -2
#define ERR_BUSY           -3
#define ERR_NO_MEMORY      -4
#define ERR_IO_FAILURE     -5
#define ERR_UNKNOWN        -99

// 错误检查宏
#define IS_ERROR(ret)      ((ret) < 0)
#define IS_SUCCESS(ret)    ((ret) == 0)

// 错误返回宏
#define RETURN_IF_ERROR(expr) \
    do { \
        int _ret = (expr); \
        if (_ret < 0) { \
            return _ret; \
        } \
    } while(0)

// 错误跳转宏
#define GOTO_IF_ERROR(expr, label) \
    do { \
        int _ret = (expr); \
        if (_ret < 0) { \
            goto label; \
        } \
    } while(0)
我的习惯:错误码用负数,成功用0或正数。这样用 if (ret < 0) 就能判断是否出错,简洁明了。

我在一个通信协议栈项目里,就吃过错误码不统一的亏。有人返回-1,有人返回0xFF,还有人直接返回NULL指针。后来我强制要求所有人用这套宏,代码质量明显上来了。

21.2 setjmp/longjmp 包装:模拟异常处理

setjmp和longjmp是C标准库提供的非局部跳转机制。说白了,就是能让你从一个函数直接跳回到调用链上某个保存点。这玩意儿用好了,就能模拟出try-catch的效果。

#include <setjmp.h>

// 异常上下文
typedef struct {
    jmp_buf env;
    int      error_code;
    const char *error_msg;
} ExceptionContext;

// 线程局部存储(或全局变量)
static __thread ExceptionContext *g_ctx = NULL;

// TRY 宏:设置异常捕获点
#define TRY(ctx) \
    do { \
        g_ctx = (ctx); \
        g_ctx->error_code = 0; \
        g_ctx->error_msg = NULL; \
        int _except_val = setjmp(g_ctx->env); \
        if (_except_val == 0) {

// CATCH 宏:捕获异常
#define CATCH(code) \
            g_ctx = NULL; \
        } \
    } while(0); \
    if (g_ctx && g_ctx->error_code == (code)) {

// THROW 宏:抛出异常
#define THROW(code, msg) \
    do { \
        g_ctx->error_code = (code); \
        g_ctx->error_msg = (msg); \
        longjmp(g_ctx->env, 1); \
    } while(0)

// END_TRY 宏
#define END_TRY \
    } \
    g_ctx = NULL;
注意:setjmp/longjmp 会跳过栈上局部变量的析构(如果有的话)。在嵌入式系统中,还要注意中断状态、锁状态等资源的恢复。我曾经在一个RTOS项目里用longjmp跳出了临界区,结果导致死锁——血的教训啊。

使用示例:

int read_sensor_data(int sensor_id, int *value) {
    // 如果传感器忙,抛出异常
    if (is_sensor_busy(sensor_id)) {
        THROW(ERR_BUSY, "sensor is busy");
    }
    
    int ret = sensor_read(sensor_id, value);
    if (ret < 0) {
        THROW(ERR_IO_FAILURE, "sensor read failed");
    }
    
    return ERR_SUCCESS;
}

void process_sensor(void) {
    ExceptionContext ctx;
    
    TRY(&ctx) {
        int value;
        read_sensor_data(1, &value);
        // 处理数据...
    }
    CATCH(ERR_BUSY) {
        printf("Sensor busy, retry later: %s\n", ctx.error_msg);
    }
    CATCH(ERR_IO_FAILURE) {
        printf("IO error: %s\n", ctx.error_msg);
    }
    END_TRY;
}

21.3 资源清理宏:RAII 模拟

C语言没有析构函数,资源管理全靠手动。但我们可以用宏来模拟RAII(Resource Acquisition Is Initialization)的效果。核心思路是:在作用域退出时自动执行清理代码。

// 自动资源清理宏
#define AUTO_CLEANUP(cleanup_func) \
    __attribute__((cleanup(cleanup_func)))

// 示例:自动关闭文件
static inline void file_close(FILE **fp) {
    if (*fp) {
        fclose(*fp);
        *fp = NULL;
    }
}

#define AUTO_FILE  AUTO_CLEANUP(file_close)

// 示例:自动释放内存
static inline void mem_free(void **p) {
    if (*p) {
        free(*p);
        *p = NULL;
    }
}

#define AUTO_MEM   AUTO_CLEANUP(mem_free)

// 示例:自动释放互斥锁
static inline void mutex_unlock(pthread_mutex_t **lock) {
    if (*lock) {
        pthread_mutex_unlock(*lock);
        *lock = NULL;
    }
}

#define AUTO_LOCK  AUTO_CLEANUP(mutex_unlock)
核心思想:利用GCC的 __attribute__((cleanup)) 扩展,在变量作用域结束时自动调用清理函数。这样就能实现类似C++ RAII的效果——资源获取即初始化,作用域退出自动释放。

使用示例:

void read_config_file(const char *path) {
    AUTO_FILE FILE *fp = fopen(path, "r");
    if (!fp) {
        return;  // 自动关闭文件
    }
    
    AUTO_MEM char *buffer = malloc(1024);
    if (!buffer) {
        return;  // 自动释放buffer,自动关闭文件
    }
    
    // 读取配置...
    fread(buffer, 1, 1024, fp);
    
    // 函数返回时,自动释放buffer,自动关闭文件
}

void critical_section_example(void) {
    static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    
    AUTO_LOCK pthread_mutex_t *lock = &mutex;
    pthread_mutex_lock(lock);
    
    // 临界区代码...
    // 即使中间有return,也会自动解锁
    
    // 函数返回时,自动解锁
}

21.4 综合实战:一个完整的错误处理框架

把上面这些技术组合起来,就能构建一个比较完善的错误处理框架。我在一个工业控制项目里就是这么干的,代码的健壮性提升了不少。

// error_framework.h
#ifndef ERROR_FRAMEWORK_H
#define ERROR_FRAMEWORK_H

#include <setjmp.h>
#include <stdio.h>

// 错误码
#define ERR_SUCCESS        0
#define ERR_INIT_FAILURE   -10
#define ERR_OPEN_FAILURE   -11
#define ERR_READ_FAILURE   -12
#define ERR_WRITE_FAILURE  -13
#define ERR_CLOSE_FAILURE  -14

// 异常上下文
typedef struct {
    jmp_buf env;
    int     code;
    char    msg[128];
} ExceptionFrame;

// TRY-CATCH-THROW 宏
#define TRY(frame) \
    do { \
        ExceptionFrame *_f = &(frame); \
        _f->code = 0; \
        int _ex = setjmp(_f->env); \
        if (_ex == 0) {

#define CATCH(code) \
            _f->code = 0; \
        } \
    } while(0); \
    if (_f->code == (code)) {

#define THROW(frame, code, fmt, ...) \
    do { \
        ExceptionFrame *_f = &(frame); \
        _f->code = (code); \
        snprintf(_f->msg, sizeof(_f->msg), (fmt), ##__VA_ARGS__); \
        longjmp(_f->env, 1); \
    } while(0)

#define END_TRY }

// RAII 资源清理宏
#define AUTO_CLOSE(fd) \
    __attribute__((cleanup(close_fd))) int fd

static inline void close_fd(int *fd) {
    if (*fd >= 0) {
        close(*fd);
        *fd = -1;
    }
}

#endif // ERROR_FRAMEWORK_H

使用示例:

void process_device_data(const char *dev_path) {
    ExceptionFrame frame;
    
    TRY(frame) {
        AUTO_CLOSE(int fd = open(dev_path, O_RDONLY));
        if (fd < 0) {
            THROW(frame, ERR_OPEN_FAILURE, 
                 "Cannot open device: %s", dev_path);
        }
        
        char buffer[256];
        int ret = read(fd, buffer, sizeof(buffer));
        if (ret < 0) {
            THROW(frame, ERR_READ_FAILURE,
                 "Read failed from %s", dev_path);
        }
        
        // 处理数据...
        printf("Read %d bytes from %s\n", ret, dev_path);
        
    } // 自动关闭fd
    
    CATCH(ERR_OPEN_FAILURE) {
        fprintf(stderr, "Open error: %s\n", frame.msg);
    }
    CATCH(ERR_READ_FAILURE) {
        fprintf(stderr, "Read error: %s\n", frame.msg);
    }
    END_TRY;
}
我的建议:不要滥用setjmp/longjmp。它跳过了正常的函数调用栈,会让控制流变得难以追踪。我一般只在资源获取、初始化这类「要么全做,要么全不做」的场景下使用。日常的错误处理,还是老老实实用返回值+错误码更靠谱。

21.5 知识体系图

宏与错误处理知识体系 错误码宏 异常模拟 RAII模拟 ERR_SUCCESS ERR_INVALID_PARAM IS_ERROR/IS_SUCCESS RETURN_IF_ERROR setjmp/longjmp TRY/CATCH/THROW ExceptionContext 非局部跳转 __attribute__((cleanup)) AUTO_FILE/AUTO_MEM 作用域自动释放 资源获取即初始化 综合应用:构建健壮的嵌入式错误处理框架 统一错误码规范 异常捕获与恢复 自动资源管理 日志与调试支持

这张图把本章的三个核心知识点串起来了。错误码宏是基础,异常模拟是控制流增强,RAII模拟是资源管理优化。三者结合,就能在C语言里写出接近C++异常安全级别的代码。

最后提醒一句:setjmp/longjmp 不是银弹。它破坏了正常的函数调用栈,会让调试变得困难。我个人的原则是:能用返回值解决的,绝不用longjmp。只有在深度嵌套的错误处理、或者需要「原子性」资源获取的场景下,才考虑用它。记住,简单才是王道。

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