每日一个C语言知识:C 错误处理

C 语言错误处理

C语言中的错误处理主要依赖于返回值、全局变量和标准库提供的错误处理机制。


1. 基本的错误处理机制

1.1 返回值检查

这是最常用的错误处理方式,函数通过返回值来指示成功或失败。

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 示例:文件操作错误处理
void file_operation_example() {
    FILE *file = fopen("nonexistent.txt", "r");
    
    if (file == NULL) {
        printf("错误:无法打开文件\n");
        return;
    }
    
    // 文件操作...
    fclose(file);
}

// 示例:内存分配错误处理
void memory_allocation_example() {
    int *array = (int*)malloc(1000000000 * sizeof(int)); // 可能失败
    
    if (array == NULL) {
        printf("错误:内存分配失败\n");
        return;
    }
    
    // 使用数组...
    free(array);
}

// 示例:字符串操作错误处理
void string_operation_example() {
    char source[] = "Hello, World!";
    char destination[10];
    
    // 安全的字符串复制
    if (strncpy(destination, source, sizeof(destination) - 1) == NULL) {
        printf("错误:字符串复制失败\n");
        return;
    }
    destination[sizeof(destination) - 1] = '\0'; // 确保终止
    
    printf("复制结果: %s\n", destination);
}

2. 标准错误处理机制

2.1 errno 全局变量

C标准库提供了 errno 全局变量来记录错误代码。

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>   // 包含errno的定义
#include <string.h>  // 包含strerror()

int main() {
    FILE *file;
    
    // 尝试打开不存在的文件
    file = fopen("nonexistent_file.txt", "r");
    if (file == NULL) {
        printf("打开文件失败!\n");
        printf("错误代码: %d\n", errno);
        printf("错误描述: %s\n", strerror(errno));
    }
    
    // 尝试除以零
    errno = 0; // 重置errno
    int result = 1 / 0;
    if (errno != 0) {
        printf("数学运算错误!\n");
        printf("错误代码: %d\n", errno);
        printf("错误描述: %s\n", strerror(errno));
    }
    
    return 0;
}

2.2 perror() 函数

perror() 自动将 errno 转换为可读的错误信息。

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

int main() {
    FILE *file;
    
    // 使用perror输出错误信息
    file = fopen("/invalid/path/file.txt", "r");
    if (file == NULL) {
        perror("打开文件失败");
        // 输出: 打开文件失败: No such file or directory
    }
    
    // 内存分配错误
    int *ptr = malloc(1000000000000ULL);
    if (ptr == NULL) {
        perror("内存分配失败");
        // 输出: 内存分配失败: Cannot allocate memory
    } else {
        free(ptr);
    }
    
    return 0;
}

3. 常见的错误代码

c 复制代码
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <math.h>

void demonstrate_common_errors() {
    printf("常见错误代码及其含义:\n");
    printf("======================\n");
    
    // 模拟各种错误
    errno = EPERM;      printf("EPERM (%d): %s\n", EPERM, strerror(EPERM));
    errno = ENOENT;     printf("ENOENT (%d): %s\n", ENOENT, strerror(ENOENT));
    errno = EINTR;      printf("EINTR (%d): %s\n", EINTR, strerror(EINTR));
    errno = EIO;        printf("EIO (%d): %s\n", EIO, strerror(EIO));
    errno = EBADF;      printf("EBADF (%d): %s\n", EBADF, strerror(EBADF));
    errno = EAGAIN;     printf("EAGAIN (%d): %s\n", EAGAIN, strerror(EAGAIN));
    errno = ENOMEM;     printf("ENOMEM (%d): %s\n", ENOMEM, strerror(ENOMEM));
    errno = EACCES;     printf("EACCES (%d): %s\n", EACCES, strerror(EACCES));
    errno = EFAULT;     printf("EFAULT (%d): %s\n", EFAULT, strerror(EFAULT));
    errno = EINVAL;     printf("EINVAL (%d): %s\n", EINVAL, strerror(EINVAL));
}

int main() {
    demonstrate_common_errors();
    return 0;
}

4. 自定义错误处理系统

4.1 定义错误码和错误处理函数

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 自定义错误码
typedef enum {
    SUCCESS = 0,
    ERROR_NULL_POINTER,
    ERROR_INVALID_INPUT,
    ERROR_OUT_OF_MEMORY,
    ERROR_FILE_IO,
    ERROR_NETWORK,
    ERROR_CALCULATION
} ErrorCode;

// 错误信息映射
const char* error_messages[] = {
    "操作成功",
    "空指针错误",
    "无效输入",
    "内存不足",
    "文件I/O错误",
    "网络错误",
    "计算错误"
};

// 错误处理函数
void handle_error(ErrorCode code, const char* context) {
    if (code != SUCCESS) {
        fprintf(stderr, "错误 [%d]: %s - %s\n", 
                code, error_messages[code], context);
        
        // 根据错误严重程度决定是否退出
        if (code == ERROR_OUT_OF_MEMORY || code == ERROR_NULL_POINTER) {
            fprintf(stderr, "严重错误,程序退出\n");
            exit(EXIT_FAILURE);
        }
    }
}

// 示例函数:安全的除法
ErrorCode safe_divide(double a, double b, double *result) {
    if (result == NULL) {
        return ERROR_NULL_POINTER;
    }
    
    if (b == 0.0) {
        return ERROR_CALCULATION;
    }
    
    *result = a / b;
    return SUCCESS;
}

// 示例函数:安全的字符串复制
ErrorCode safe_string_copy(char *dest, const char *src, size_t dest_size) {
    if (dest == NULL || src == NULL) {
        return ERROR_NULL_POINTER;
    }
    
    if (dest_size == 0) {
        return ERROR_INVALID_INPUT;
    }
    
    strncpy(dest, src, dest_size - 1);
    dest[dest_size - 1] = '\0'; // 确保字符串终止
    
    return SUCCESS;
}

int main() {
    double result;
    ErrorCode err;
    
    // 测试安全除法
    err = safe_divide(10.0, 2.0, &result);
    handle_error(err, "除法运算");
    if (err == SUCCESS) {
        printf("10.0 / 2.0 = %.2f\n", result);
    }
    
    err = safe_divide(10.0, 0.0, &result);
    handle_error(err, "除以零");
    
    // 测试安全字符串复制
    char buffer[10];
    err = safe_string_copy(buffer, "Hello, World!", sizeof(buffer));
    handle_error(err, "字符串复制");
    if (err == SUCCESS) {
        printf("复制结果: %s\n", buffer);
    }
    
    return 0;
}

4.2 带错误上下文的结构体

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

// 错误信息结构体
typedef struct {
    int code;
    char message[256];
    char file[128];
    int line;
    time_t timestamp;
} ErrorInfo;

// 全局错误信息
ErrorInfo last_error = {0};

// 设置错误信息
void set_error(int code, const char* message, const char* file, int line) {
    last_error.code = code;
    strncpy(last_error.message, message, sizeof(last_error.message) - 1);
    strncpy(last_error.file, file, sizeof(last_error.file) - 1);
    last_error.line = line;
    last_error.timestamp = time(NULL);
}

// 获取错误信息
void print_last_error() {
    if (last_error.code != 0) {
        char time_str[64];
        strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", 
                 localtime(&last_error.timestamp));
        
        fprintf(stderr, "[%s] 错误 %d: %s\n", time_str, 
                last_error.code, last_error.message);
        fprintf(stderr, "位置: %s:%d\n", last_error.file, last_error.line);
    }
}

// 清除错误信息
void clear_error() {
    last_error.code = 0;
    last_error.message[0] = '\0';
    last_error.file[0] = '\0';
    last_error.line = 0;
}

// 带错误检查的内存分配
void* checked_malloc(size_t size, const char* file, int line) {
    void *ptr = malloc(size);
    if (ptr == NULL) {
        set_error(1, "内存分配失败", file, line);
    }
    return ptr;
}

// 带错误检查的文件打开
FILE* checked_fopen(const char* filename, const char* mode, 
                    const char* file, int line) {
    FILE *f = fopen(filename, mode);
    if (f == NULL) {
        char message[256];
        snprintf(message, sizeof(message), "无法打开文件: %s", filename);
        set_error(2, message, file, line);
    }
    return f;
}

// 宏简化错误检查调用
#define CHECKED_MALLOC(size) checked_malloc(size, __FILE__, __LINE__)
#define CHECKED_FOPEN(filename, mode) checked_fopen(filename, mode, __FILE__, __LINE__)

int main() {
    clear_error(); // 清除之前的错误
    
    // 使用带错误检查的函数
    int *data = (int*)CHECKED_MALLOC(1000000000000ULL * sizeof(int));
    if (data == NULL) {
        print_last_error();
    } else {
        free(data);
    }
    
    FILE *file = CHECKED_FOPEN("nonexistent.txt", "r");
    if (file == NULL) {
        print_last_error();
    } else {
        fclose(file);
    }
    
    return 0;
}

5. 资源管理和错误处理

5.1 使用 goto 进行错误清理

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
    char *name;
    int *scores;
    FILE *log_file;
} Student;

int initialize_student(Student *student, const char *name, int score_count) {
    int status = -1;
    
    // 初始化指针为NULL,便于清理
    student->name = NULL;
    student->scores = NULL;
    student->log_file = NULL;
    
    // 分配姓名
    student->name = malloc(strlen(name) + 1);
    if (student->name == NULL) {
        perror("分配姓名内存失败");
        goto cleanup;
    }
    strcpy(student->name, name);
    
    // 分配分数数组
    student->scores = malloc(score_count * sizeof(int));
    if (student->scores == NULL) {
        perror("分配分数数组失败");
        goto cleanup;
    }
    
    // 打开日志文件
    student->log_file = fopen("student.log", "w");
    if (student->log_file == NULL) {
        perror("打开日志文件失败");
        goto cleanup;
    }
    
    // 初始化成功
    status = 0;
    printf("学生 %s 初始化成功\n", name);
    
cleanup:
    if (status != 0) {
        // 清理已分配的资源
        free(student->name);
        free(student->scores);
        if (student->log_file != NULL) {
            fclose(student->log_file);
        }
        printf("学生初始化失败\n");
    }
    
    return status;
}

void cleanup_student(Student *student) {
    free(student->name);
    free(student->scores);
    if (student->log_file != NULL) {
        fclose(student->log_file);
    }
    printf("学生资源已清理\n");
}

int main() {
    Student student;
    
    if (initialize_student(&student, "张三", 5) == 0) {
        // 使用学生对象...
        printf("正在使用学生: %s\n", student.name);
        
        // 清理资源
        cleanup_student(&student);
    }
    
    return 0;
}

5.2 使用 setjmplongjmp 进行错误恢复

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>
#include <string.h>

jmp_buf error_recovery;

typedef enum {
    ERR_NONE,
    ERR_MEMORY,
    ERR_FILE,
    ERR_NETWORK
} ErrorType;

// 模拟可能失败的函数
int risky_operation(int scenario) {
    switch (scenario) {
        case 0:
            return 0; // 成功
        case 1:
            printf("内存分配失败\n");
            longjmp(error_recovery, ERR_MEMORY);
        case 2:
            printf("文件操作失败\n");
            longjmp(error_recovery, ERR_FILE);
        case 3:
            printf("网络连接失败\n");
            longjmp(error_recovery, ERR_NETWORK);
        default:
            return 0;
    }
}

void handle_error(ErrorType error) {
    switch (error) {
        case ERR_MEMORY:
            printf("处理内存错误: 尝试减少内存使用\n");
            break;
        case ERR_FILE:
            printf("处理文件错误: 检查文件权限和路径\n");
            break;
        case ERR_NETWORK:
            printf("处理网络错误: 检查网络连接\n");
            break;
        default:
            printf("未知错误\n");
    }
}

int main() {
    ErrorType error;
    
    // 设置错误恢复点
    if ((error = (ErrorType)setjmp(error_recovery)) == ERR_NONE) {
        printf("开始执行风险操作...\n");
        
        // 模拟不同的场景
        for (int i = 0; i < 4; i++) {
            printf("\n场景 %d:\n", i);
            risky_operation(i);
            printf("场景 %d 执行成功\n", i);
        }
        
        printf("\n所有操作完成!\n");
    } else {
        printf("\n捕获到错误,进行错误恢复...\n");
        handle_error(error);
        printf("错误恢复完成,继续执行...\n");
    }
    
    return 0;
}

6. 信号处理

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <setjmp.h>
#include <unistd.h>

jmp_buf segmentation_fault_recovery;

// 段错误信号处理函数
void segmentation_fault_handler(int sig) {
    printf("\n捕获到段错误信号 (%d)\n", sig);
    printf("进行错误恢复...\n");
    
    // 跳转到恢复点
    longjmp(segmentation_fault_recovery, 1);
}

// 浮点错误处理函数
void floating_point_handler(int sig) {
    printf("\n捕获到浮点错误信号 (%d)\n", sig);
    printf("处理浮点异常...\n");
    
    // 恢复默认处理并重新发送信号
    signal(sig, SIG_DFL);
    raise(sig);
}

int main() {
    // 设置信号处理函数
    signal(SIGSEGV, segmentation_fault_handler); // 段错误
    signal(SIGFPE, floating_point_handler);      // 浮点异常
    
    printf("信号处理示例\n");
    printf("============\n");
    
    // 段错误恢复示例
    if (setjmp(segmentation_fault_recovery) == 0) {
        printf("1. 段错误恢复测试:\n");
        
        int *bad_pointer = NULL;
        // 这会导致段错误,但会被我们的处理函数捕获
        // *bad_pointer = 42; // 取消注释来测试
        
        printf("段错误测试通过\n");
    } else {
        printf("从段错误中恢复成功\n");
    }
    
    // 浮点错误示例
    printf("\n2. 浮点错误测试:\n");
    
    // 这会导致浮点异常
    // int result = 1 / 0; // 取消注释来测试
    
    printf("浮点错误测试通过\n");
    
    printf("\n程序正常结束\n");
    return 0;
}

7. 断言(Assertions)

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <math.h>

// 使用断言进行调试
double calculate_discriminant(double a, double b, double c) {
    // 前置条件检查
    assert(!isnan(a) && !isnan(b) && !isnan(c)); // 确保不是NaN
    assert(isfinite(a) && isfinite(b) && isfinite(c)); // 确保是有限数
    
    double discriminant = b * b - 4 * a * c;
    
    // 后置条件检查
    assert(!isnan(discriminant)); // 结果不应为NaN
    assert(isfinite(discriminant)); // 结果应为有限数
    
    return discriminant;
}

// 自定义断言宏
#define CUSTOM_ASSERT(condition, message) \
    do { \
        if (!(condition)) { \
            fprintf(stderr, "断言失败: %s\n", message); \
            fprintf(stderr, "文件: %s, 行号: %d\n", __FILE__, __LINE__); \
            abort(); \
        } \
    } while(0)

void test_custom_assert() {
    int *ptr = malloc(sizeof(int));
    
    CUSTOM_ASSERT(ptr != NULL, "内存分配失败");
    CUSTOM_ASSERT(*ptr == 0, "新分配的内存应该被初始化为0"); // 这不一定成立!
    
    free(ptr);
}

int main() {
    printf("断言示例\n");
    printf("========\n");
    
    // 标准断言测试
    printf("1. 标准断言测试:\n");
    double disc = calculate_discriminant(1.0, -3.0, 2.0);
    printf("判别式: %.2f\n", disc);
    
    // 这会触发断言失败
    // double bad_disc = calculate_discriminant(0.0, 0.0, 0.0); // 取消注释测试
    
    // 自定义断言测试
    printf("\n2. 自定义断言测试:\n");
    test_custom_assert();
    
    printf("\n所有测试通过!\n");
    
    return 0;
}

8. 完整的错误处理框架示例

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <time.h>

// 错误级别
typedef enum {
    LOG_DEBUG,
    LOG_INFO,
    LOG_WARNING,
    LOG_ERROR,
    LOG_CRITICAL
} LogLevel;

// 错误处理配置
typedef struct {
    LogLevel min_level;
    FILE *log_file;
    int exit_on_critical;
} ErrorConfig;

// 全局错误处理配置
ErrorConfig error_config = {
    .min_level = LOG_WARNING,
    .log_file = NULL,
    .exit_on_critical = 1
};

// 初始化错误处理系统
int error_system_init(const char *log_filename, LogLevel min_level) {
    if (log_filename != NULL) {
        error_config.log_file = fopen(log_filename, "a");
        if (error_config.log_file == NULL) {
            perror("无法打开日志文件");
            return -1;
        }
    } else {
        error_config.log_file = stderr;
    }
    
    error_config.min_level = min_level;
    return 0;
}

// 清理错误处理系统
void error_system_cleanup() {
    if (error_config.log_file != NULL && error_config.log_file != stderr) {
        fclose(error_config.log_file);
        error_config.log_file = NULL;
    }
}

// 记录错误
void log_message(LogLevel level, const char *file, int line, const char *format, ...) {
    if (level < error_config.min_level) {
        return;
    }
    
    // 获取当前时间
    time_t now = time(NULL);
    char time_str[64];
    strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", localtime(&now));
    
    // 级别名称
    const char *level_names[] = {
        "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"
    };
    
    // 格式化消息
    va_list args;
    va_start(args, format);
    
    fprintf(error_config.log_file, "[%s] %s: %s:%d: ", 
            time_str, level_names[level], file, line);
    vfprintf(error_config.log_file, format, args);
    fprintf(error_config.log_file, "\n");
    fflush(error_config.log_file);
    
    va_end(args);
    
    // 关键错误处理
    if (level == LOG_CRITICAL && error_config.exit_on_critical) {
        fprintf(error_config.log_file, "关键错误,程序退出\n");
        error_system_cleanup();
        exit(EXIT_FAILURE);
    }
}

// 日志宏
#define LOG_DEBUG(...)    log_message(LOG_DEBUG, __FILE__, __LINE__, __VA_ARGS__)
#define LOG_INFO(...)     log_message(LOG_INFO, __FILE__, __LINE__, __VA_ARGS__)
#define LOG_WARNING(...)  log_message(LOG_WARNING, __FILE__, __LINE__, __VA_ARGS__)
#define LOG_ERROR(...)    log_message(LOG_ERROR, __FILE__, __LINE__, __VA_ARGS__)
#define LOG_CRITICAL(...) log_message(LOG_CRITICAL, __FILE__, __LINE__, __VA_ARGS__)

// 示例应用程序
int main() {
    // 初始化错误处理系统
    if (error_system_init("app.log", LOG_DEBUG) != 0) {
        fprintf(stderr, "错误处理系统初始化失败\n");
        return EXIT_FAILURE;
    }
    
    LOG_INFO("应用程序启动");
    
    // 模拟各种级别的日志
    LOG_DEBUG("这是调试信息");
    LOG_INFO("应用程序初始化完成");
    LOG_WARNING("内存使用率较高");
    LOG_ERROR("文件读取失败");
    
    // 模拟关键错误(会退出程序)
    // LOG_CRITICAL("数据库连接失败"); // 取消注释测试
    
    LOG_INFO("应用程序正常退出");
    
    // 清理
    error_system_cleanup();
    return EXIT_SUCCESS;
}

总结

C语言错误处理的主要方法:

方法 适用场景 优点 缺点
返回值检查 函数调用、资源分配 简单直观 错误信息有限
errno全局变量 系统调用、库函数 标准化的错误代码 线程不安全
perror() 快速错误报告 自动格式化错误信息 灵活性差
自定义错误码 应用程序特定错误 可读性好,类型安全 需要额外定义
setjmp/longjmp 错误恢复、嵌套调用 非局部跳转 复杂,可能泄漏资源
信号处理 系统级错误、异常 处理异步事件 平台依赖性强
断言 调试、开发阶段 自动检查假设 发布版本中通常被禁用
相关推荐
国服第二切图仔8 小时前
Rust开发之使用panic!处理不可恢复错误
开发语言·后端·rust
郝学胜-神的一滴8 小时前
Qt删除布局与布局切换技术详解
开发语言·数据库·c++·qt·程序人生·系统架构
奔跑吧邓邓子8 小时前
【C语言实战(66)】筑牢防线:C语言安全编码之输入与错误处理
c语言·安全·开发实战·错误处理·输入验证
闲人编程8 小时前
现代Python开发环境搭建(VSCode + Dev Containers)
开发语言·vscode·python·容器·dev·codecapsule
雨落在了我的手上8 小时前
C语言入门(十三):操作符详解(1)
c语言
代码搬运媛9 小时前
【工具上新】快速了解一站式开发工具 bun
开发语言·bun
fantasy5_59 小时前
手撕vector:从零实现一个C++动态数组
java·开发语言·c++
任风雨9 小时前
3.1.1.Java基础知识
java·开发语言
froginwe119 小时前
CSS3 框大小:深入解析与优化技巧
开发语言