每日一个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 错误恢复、嵌套调用 非局部跳转 复杂,可能泄漏资源
信号处理 系统级错误、异常 处理异步事件 平台依赖性强
断言 调试、开发阶段 自动检查假设 发布版本中通常被禁用
相关推荐
8Qi85 小时前
回文子串(Palindromic Substrings)—— 题解
算法·leetcode·职场和发展·动态规划
xskukuku6 小时前
使用VSCode配置C语言运行环境
c语言·ide·vscode
想吃火锅10058 小时前
【leetcode】405.数字转换为十六进制数js
开发语言·javascript·ecmascript
专注VB编程开发20年8 小时前
AI 生成C# WinForm 窗体 = 目前就是垃圾
开发语言·人工智能·c#
cfm_29148 小时前
JVM GC垃圾回收初步了解
java·开发语言·jvm
~小先生~9 小时前
Python从入门到放弃(一)
开发语言·python
许彰午9 小时前
17_synchronized关键字深度解析
java·开发语言
z落落9 小时前
C# 泛型接口和泛型类+泛型约束
开发语言·c#
阿正的梦工坊9 小时前
【Rust】02-变量、不可变性与基础类型
开发语言·后端·rust