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、复杂项目可考虑封装错误处理逻辑(如返回结构体包含状态和数据)。

相关推荐
Scott9999HH1 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
码智社2 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海2 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖3 小时前
JDK 26 新特性详解
java·开发语言
马优晨4 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区6 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大6 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai6 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
এ慕ོ冬℘゜6 小时前
前端基础:什么是时间戳?JS获取时间戳三种方法与实战用途
开发语言·前端·javascript
执明wa7 小时前
LayoutInflater详解: XML是如何变成View的?
android·xml·开发语言·android studio