C语言错误处理完全指南

工程化错误处理

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程序的基石,应在设计阶段就规划好错误传播路径,而非事后补救。