C语言没有内置的异常处理机制,错误处理完全依赖程序员的自觉与设计。本文系统介绍C语言中各种错误处理策略——从返回值检查、errno全局变量到setjmp/longjmp非局部跳转,帮助你构建健壮的错误处理体系。
返回值错误处理
错误码设计
/* 统一错误码定义 */
typedef enum {
ERR_OK = 0,
ERR_INVALID_ARG = -1,
ERR_OUT_OF_MEMORY= -2,
ERR_FILE_NOT_FOUND = -3,
ERR_PERMISSION_DENIED= -4,
ERR_IO_FAILURE = -5,
ERR_TIMEOUT = -6,
ERR_BUFFER_OVERFLOW = -7,
ERR_NOT_IMPLEMENTED = -8,
ERR_UNKNOWN = -99
} ErrorCode;
/* 错误码转字符串 */
const char *error_to_string(ErrorCode code) {
switch (code) {
case ERR_OK: return "成功";
case ERR_INVALID_ARG: return "无效参数";
case ERR_OUT_OF_MEMORY: return "内存不足";
case ERR_FILE_NOT_FOUND: return "文件未找到";
case ERR_PERMISSION_DENIED: return "权限不足";
case ERR_IO_FAILURE: return "I/O错误";
case ERR_TIMEOUT: return "操作超时";
case ERR_BUFFER_OVERFLOW: return "缓冲区溢出";
case ERR_NOT_IMPLEMENTED: return "未实现";
default: return "未知错误";
}
}
返回值检查模式
/* 函数返回错误码,结果通过指针参数返回 */
ErrorCode read_config(const char *path, Config *out) {
if (!path || !out) return ERR_INVALID_ARG;
FILE *fp = fopen(path, "r");
if (!fp) return ERR_FILE_NOT_FOUND;
char line[256];
while (fgets(line, sizeof(line), fp)) {
if (parse_line(line, out) != ERR_OK) {
fclose(fp);
return ERR_IO_FAILURE;
}
}
fclose(fp);
return ERR_OK;
}
/* 调用方检查返回值 */
int main(void) {
Config cfg;
ErrorCode err = read_config("app.conf", &cfg);
if (err != ERR_OK) {
fprintf(stderr, "配置读取失败: %s\n", error_to_string(err));
return 1;
}
/* 使用 cfg ... */
return 0;
}
errno 机制
errno 使用规范
/* errno 使用三步法:
* 1. 调用前不必清零(某些函数成功时不清零)
* 2. 调用后立即检查
* 3. 保存后再使用
*/
#include <errno.h>
#include <string.h>
void safe_file_copy(const char *src_path, const char *dst_path) {
FILE *src = fopen(src_path, "rb");
if (!src) {
int saved_errno = errno; // 立即保存
fprintf(stderr, "无法打开 %s: %s\n", src_path, strerror(saved_errno));
return;
}
FILE *dst = fopen(dst_path, "wb");
if (!dst) {
int saved_errno = errno;
fprintf(stderr, "无法创建 %s: %s\n", dst_path, strerror(saved_errno));
fclose(src);
return;
}
/* 执行拷贝 ... */
char buf[4096];
size_t n;
while ((n = fread(buf, 1, sizeof(buf), src)) > 0) {
if (fwrite(buf, 1, n, dst) != n) {
int saved_errno = errno;
fprintf(stderr, "写入失败: %s\n", strerror(saved_errno));
break;
}
}
fclose(dst);
fclose(src);
}
自定义错误信息
/* 线程安全的错误信息 */
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#define ERRMSG_SIZE 256
typedef struct {
ErrorCode code;
char message[ERRMSG_SIZE];
const char *file;
int line;
} ErrorInfo;
#define SET_ERROR(info, c, fmt, ...) do { \
(info)->code = (c); \
(info)->file = __FILE__; \
(info)->line = __LINE__; \
snprintf((info)->message, ERRMSG_SIZE, fmt, ##__VA_ARGS__); \
} while(0)
/* 使用示例 */
ErrorCode load_plugin(const char *name, ErrorInfo *err) {
if (!name) {
SET_ERROR(err, ERR_INVALID_ARG, "插件名不能为空");
return err->code;
}
void *handle = dlopen(name, RTLD_LAZY);
if (!handle) {
SET_ERROR(err, ERR_IO_FAILURE, "加载 %s 失败: %s", name, dlerror());
return err->code;
}
/* 初始化插件 ... */
return ERR_OK;
}
setjmp/longjmp 非局部跳转
基础用法
#include <setjmp.h>
#include <stdio.h>
#include <stdlib.h>
static jmp_buf jump_buffer;
void deep_function(int depth) {
if (depth <= 0) {
printf("到达底层,触发跳转\n");
longjmp(jump_buffer, 1); // 非局部跳转
}
printf("深度 %d,继续递归\n", depth);
deep_function(depth - 1);
printf("深度 %d,返回\n", depth); // longjmp后这行不会执行
}
int main(void) {
int ret = setjmp(jump_buffer);
if (ret == 0) {
printf("正常执行路径\n");
deep_function(5);
} else {
printf("从 longjmp 返回,值=%d\n", ret);
}
return 0;
}
资源清理框架
/* 利用 setjmp/longjmp 实现简易 try/catch */
#include <setjmp.h>
#define MAX_NESTING 16
static jmp_buf env_stack[MAX_NESTING];
static int env_depth = 0;
#define TRY \
do { \
if (env_depth >= MAX_NESTING) { \
fprintf(stderr, "嵌套过深\n"); \
exit(1); \
} \
int _exc = setjmp(env_stack[env_depth++]); \
if (_exc == 0) {
#define CATCH(e) \
} else { \
int e = _exc; \
env_depth--;
#define END_TRY \
} \
} while(0)
#define THROW(code) \
longjmp(env_stack[env_depth - 1], code)
/* 使用示例 */
void risky_operation(void) {
static int count = 0;
if (++count > 3) {
THROW(42);
}
printf("操作成功 (第%d次)\n", count);
}
int main(void) {
TRY
risky_operation();
risky_operation();
risky_operation();
risky_operation(); // 这里会抛出
CATCH(err)
printf("捕获异常: %d\n", err);
END_TRY
return 0;
}
错误传播链
链式错误包装
/* 错误链: 记录错误传播路径 */
#define ERR_CHAIN_SIZE 8
typedef struct ErrNode {
ErrorCode code;
char message[128];
const char *func;
const char *file;
int line;
} ErrNode;
typedef struct {
int depth;
ErrNode chain[ERR_CHAIN_SIZE];
} ErrorChain;
#define ERR_PUSH(chain, c, msg) do { \
if ((chain)->depth < ERR_CHAIN_SIZE) { \
ErrNode *_n = &(chain)->chain[(chain)->depth++]; \
_n->code = (c); \
_n->func = __func__; \
_n->file = __FILE__; \
_n->line = __LINE__; \
strncpy(_n->message, (msg), sizeof(_n->message) - 1); \
} \
} while(0)
void print_error_chain(const ErrorChain *ec) {
fprintf(stderr, "=== 错误链 (深度=%d) ===\n", ec->depth);
for (int i = ec->depth - 1; i >= 0; i--) {
const ErrNode *n = &ec->chain[i];
fprintf(stderr, " [%d] %s:%d %s(): %s (code=%d)\n",
i, n->file, n->line, n->func, n->message, n->code);
}
}
防御性编程
前置条件检查
/* 断言与前置条件 */
#include <assert.h>
/* 编译时静态断言 (C11) */
_Static_assert(sizeof(int) >= 4, "int必须至少4字节");
/* 运行时断言宏 */
#ifdef NDEBUG
#define REQUIRE(cond, msg) ((void)0)
#else
#define REQUIRE(cond, msg) \
do { \
if (!(cond)) { \
fprintf(stderr, "前置条件失败: %s\n 在 %s:%d %s()\n", \
msg, __FILE__, __LINE__, __func__); \
abort(); \
} \
} while(0)
#endif
/* 安全的数组操作 */
int safe_array_get(const int *arr, size_t len, size_t idx) {
REQUIRE(arr != NULL, "数组指针不能为空");
REQUIRE(idx < len, "数组下标越界");
return arr[idx];
}
/* 带容量检查的缓冲区写入 */
ErrorCode buf_write(Buffer *buf, const void *data, size_t len) {
if (!buf || !data) return ERR_INVALID_ARG;
if (buf->pos + len > buf->capacity) return ERR_BUFFER_OVERFLOW;
memcpy(buf->data + buf->pos, data, len);
buf->pos += len;
return ERR_OK;
}
资源获取即初始化(RAII模拟)
/* 用 goto 模拟 RAII 风格的资源清理 */
ErrorCode process_file(const char *path) {
FILE *fp = NULL;
char *buffer = NULL;
ErrorCode err = ERR_OK;
fp = fopen(path, "r");
if (!fp) { err = ERR_FILE_NOT_FOUND; goto cleanup; }
buffer = malloc(4096);
if (!buffer) { err = ERR_OUT_OF_MEMORY; goto cleanup; }
size_t n = fread(buffer, 1, 4096, fp);
if (n == 0 && !feof(fp)) { err = ERR_IO_FAILURE; goto cleanup; }
/* 处理数据 ... */
cleanup:
// 按逆序释放资源
if (buffer) free(buffer);
if (fp) fclose(fp);
return err;
}
总结
- 错误码 - 统一定义错误枚举,函数返回错误码,结果通过指针输出
- errno - 系统调用失败后立即保存errno,使用strerror获取描述
- setjmp/longjmp - 实现跨函数的错误跳转,需注意资源泄漏风险
- 错误链 - 记录错误传播路径,方便调试和定位
- 防御性编程 - 前置条件检查、边界验证、RAII式资源管理
- goto cleanup - C语言中最实用的资源清理模式
良好的错误处理是健壮C程序的基石,应在设计阶段就规划好错误传播路径,而非事后补救。