C语言---错误处理

文章目录

  • 一、错误处理的核心方法
    • [1. 返回值检查](#1. 返回值检查)
    • [2. 错误码(Error Codes)](#2. 错误码(Error Codes))
    • [3. 全局变量 errno](#3. 全局变量 errno)
    • [4. 断言(Assertions)](#4. 断言(Assertions))
  • [5. 信号处理(Signals)](#5. 信号处理(Signals))
  • 二、错误处理的最佳实践
    • [1、 尽早检查错误](#1、 尽早检查错误)
    • [2、 统一错误码定义](#2、 统一错误码定义)
    • [3、 错误信息输出](#3、 错误信息输出)
    • [4、 资源清理](#4、 资源清理)
    • [5、 避免过度使用全局变量](#5、 避免过度使用全局变量)
    • [6、 设计可恢复的错误处理](#6、 设计可恢复的错误处理)
  • 三、高级错误处理技术
    • [1. 长跳转(setjmp/longjmp)](#1. 长跳转(setjmp/longjmp))
    • [2. 错误处理封装](#2. 错误处理封装)
  • 四、总结
  • 最佳实践建议

在C语言中,由于缺乏内置的异常处理机制(如C++的try/catch或Java的异常系统),错误处理主要依赖返回值检查、错误码、全局变量(如errno)和断言等机制。

一、错误处理的核心方法

1. 返回值检查

原理:函数通过返回值表示成功或失败,调用者需显式检查。

常见模式如下:

成功返回有效值(如malloc返回非NULL指针)。

失败返回特定值(如-1、NULL、EOF等)。

代码示例:

bash 复制代码
FILE* file = fopen("example.txt", "r");
if (file == NULL) {
    perror("Failed to open file"); // 输出错误信息
    exit(EXIT_FAILURE);           // 终止程序
}

2. 错误码(Error Codes)

原理:函数返回一个整数错误码(通常定义在<errno.h>或自定义枚举中)。

标准错误码:

1、ENOENT:文件或目录不存在。

2、EINVAL:无效参数。

3、ENOMEM:内存不足。

代码示例:

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

int main() {
    FILE* file = fopen("nonexistent.txt", "r");
    if (file == NULL) {
        printf("Error: %s\n", strerror(errno)); // 将errno转为可读字符串
        return 1;
    }
    fclose(file);
    return 0;
}

3. 全局变量 errno

原理:C标准库函数在失败时设置全局变量errno,表示具体错误原因。

使用场景:与perror()或strerror()配合输出错误信息。

注意:

1、errno可能被后续调用覆盖,需立即处理。

2、成功时errno的值未定义,不应依赖它判断成功。

代码示例:

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

int main() {
    int result = divide(10, 0); // 假设divide是自定义除法函数
    if (result == -1 && errno == EDOM) { // EDOM表示数学域错误
        printf("Division by zero!\n");
    }
    return 0;
}

4. 断言(Assertions)

原理:使用assert()宏在调试阶段捕获逻辑错误(如参数越界、空指针等)。

特点:

1、仅在NDEBUG未定义时生效(发布版本通常定义NDEBUG禁用断言)。

2、断言失败时终止程序并输出错误信息。

代码示例:

bash 复制代码
#include <stdio.h>
#include <assert.h>

int safe_divide(int a, int b) {
    assert(b != 0 && "Division by zero!"); // 调试阶段检查
    return a / b;
}

int main() {
    printf("%d\n", safe_divide(10, 0)); // 调试时触发断言
    return 0;
}

5. 信号处理(Signals)

原理:通过signal()或sigaction()捕获运行时信号(如SIGSEGV、SIGFPE)。

适用场景:处理严重错误(如段错误、浮点异常)。

示例:

bash 复制代码
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>

void handle_segfault(int sig) {
    printf("Segmentation fault occurred!\n");
    exit(EXIT_FAILURE);
}

int main() {
    signal(SIGSEGV, handle_segfault); // 注册信号处理函数
    int* ptr = NULL;
    *ptr = 42; // 触发段错误
    return 0;
}

6、程序退出时状态

通常情况下,程序成功执行完一个操作正常退出的时候会带有值 EXIT_SUCCESS。在这里,EXIT_SUCCESS 是宏,它被定义为 0。

如果程序中存在一种错误情况,当您退出程序时,会带有状态值 EXIT_FAILURE,被定义为 -1。所以,上面的程序可以写成:

bash 复制代码
#include <stdio.h>
#include <stdlib.h>
 
int main()
{
   int dividend = 20;
   int divisor = 5;
   int quotient;
 
   if( divisor == 0){
      fprintf(stderr, "除数为 0 退出运行...\n");
      exit(EXIT_FAILURE);
   }
   quotient = dividend / divisor;
   fprintf(stderr, "quotient 变量的值为: %d\n", quotient );
 
   exit(EXIT_SUCCESS);
}

二、错误处理的最佳实践

1、 尽早检查错误

不要忽略函数的返回值,尤其是涉及资源分配(如内存、文件、网络连接)的操作。

反例:

bash 复制代码
malloc(100); // 未检查返回值,可能导致后续空指针解引用

2、 统一错误码定义

使用枚举或宏定义错误码,避免硬编码(如-1、1等)。

示例:

bash 复制代码
typedef enum {
    ERR_SUCCESS = 0,
    ERR_FILE_OPEN,
    ERR_MEMORY_ALLOC,
    ERR_INVALID_INPUT
} ErrorCode;

3、 错误信息输出

使用perror()或strerror(errno)输出可读的错误信息,便于调试。

示例:

bash 复制代码
if (fopen("file.txt", "r") == NULL) {
    perror("fopen failed"); // 输出:fopen failed: No such file or directory
}

4、 资源清理

在错误发生时释放已分配的资源(如关闭文件、释放内存)。

示例:

bash 复制代码
FILE* file1 = fopen("a.txt", "r");
FILE* file2 = fopen("b.txt", "w");
if (file1 == NULL || file2 == NULL) {
    if (file1) fclose(file1);
    if (file2) fclose(file2);
    return ERR_FILE_OPEN;
}

5、 避免过度使用全局变量

errno是全局变量,多线程环境下需使用线程安全版本(如errno_t或strerror_s)。

6、 设计可恢复的错误处理

对于非致命错误(如用户输入错误),提供重试机制而非直接终止程序。

三、高级错误处理技术

1. 长跳转(setjmp/longjmp)

原理:通过setjmp保存程序状态,longjmp跳转回保存点,实现非局部跳转。

适用场景:深层嵌套函数中快速退出(如解析器错误恢复)。

示例:

bash 复制代码
#include <stdio.h>
#include <setjmp.h>

jmp_buf env;

void risky_operation() {
    if (error_condition) {
        longjmp(env, 1); // 跳转回setjmp处
    }
}

int main() {
    if (setjmp(env) == 0) {
        risky_operation();
    } else {
        printf("Error occurred, recovered!\n");
    }
    return 0;
}

2. 错误处理封装

将错误处理逻辑封装为函数或宏,减少重复代码。

示例:

bash 复制代码
#define CHECK_NULL(ptr, err_code) do { \
    if (ptr == NULL) { \
        perror("Null pointer error"); \
        return err_code; \
    } \
} while (0)

int foo() {
    int* ptr = malloc(100);
    CHECK_NULL(ptr, ERR_MEMORY_ALLOC);
    // ...
    free(ptr);
    return ERR_SUCCESS;
}

四、总结

方法 适用场景 优点 缺点
返回值检查 简单函数调用 明确、直接 需手动检查每个调用
errno + 错误码 标准库函数错误 与系统集成好 多线程不安全
断言 调试阶段逻辑错误 快速定位问题 发布版本需禁用
信号处理 严重运行时错误(如段错误) 捕获致命错误 复杂,可能掩盖问题
setjmp/longjmp 深层嵌套错误恢复 跨函数跳转 易导致资源泄漏,代码难维护

最佳实践建议

1、优先使用返回值检查和错误码。

2、调试阶段用断言捕获逻辑错误。

3、资源分配后立即检查并处理错误。

4、多线程程序避免依赖errno,改用线程安全方案。

5、复杂项目可考虑封装错误处理逻辑(如返回结构体包含状态和数据)。

相关推荐
枫叶丹42 小时前
【Qt开发】Qt系统(六)-> Qt 线程安全
c语言·开发语言·数据库·c++·qt·安全
信奥胡老师2 小时前
P14917 [GESP202512 五级] 数字移动
开发语言·数据结构·c++·学习·算法
天若有情6732 小时前
用 Python 爬取电商商品数据:从入门到反爬破解
开发语言·python
txinyu的博客2 小时前
结合STL,服务器项目解析vetcor map unordered_map
开发语言·c++
北京地铁1号线2 小时前
1.1 文档解析:PDF/Word/HTML的结构化提取
开发语言·知识图谱·文档解析
源代码•宸2 小时前
Golang原理剖析(程序初始化、数据结构string)
开发语言·数据结构·经验分享·后端·golang·string·init
忆锦紫2 小时前
图像增强算法:对比度增强算法以及MATLAB实现
开发语言·图像处理·matlab
m0_748250032 小时前
C++ Web 编程
开发语言·前端·c++