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模拟是资源管理优化。三者结合,就能在C语言里写出接近C++异常安全级别的代码。
最后提醒一句:setjmp/longjmp 不是银弹。它破坏了正常的函数调用栈,会让调试变得困难。我个人的原则是:能用返回值解决的,绝不用longjmp。只有在深度嵌套的错误处理、或者需要「原子性」资源获取的场景下,才考虑用它。记住,简单才是王道。
公众号:蓝海资料掘金营,微信deep3321