22. 断言与错误处理.md 6.3 KB

22 断言与错误处理

22.1 完整概念讲解

assert宏

assert是调试期的条件检测宏,在条件为假时终止程序并打印错误信息。

特点:

  • 仅在调试模式(NDEBUG未定义)生效
  • Release编译通常禁用(定义NDEBUG
  • 用于检测程序员的逻辑错误,而非运行时错误

_Static_assert(C11)

编译期断言,在编译时检查条件,不满足则编译失败。

用途:

  • 检查结构体大小
  • 验证常量值
  • 确保类型满足特定条件

errno

全局错误变量,标准库函数在出错时设置。

常用函数:

  • perror():打印错误信息到stderr
  • strerror():将错误码转换为字符串

错误码设计模式

常见模式:

  • 返回0表示成功,负数表示错误
  • 返回指针,NULL表示错误
  • 输出参数+返回值(0成功,非0错误码)

嵌入式错误处理策略

  • 优先使用确定性错误处理,避免异常
  • 关键错误需要重启或看门狗
  • 资源受限时考虑简化的错误恢复

22.2 核心API/语法

#include <assert.h>

// 断言宏
assert(expression);  // 表达式为假时终止

// 编译期断言(C11)
_Static_assert(constant-expression, "message");

#include <errno.h>

// 错误变量
extern int errno;

// 错误信息函数
void perror(const char *s);
char *strerror(int errnum);

// 返回错误码示例
int func(void) {
    // 成功
    return 0;

    // 失败
    errno = EINVAL;
    return -1;
}

22.3 代码示例(完整可编译,附gcc命令)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <errno.h>

// 编译期断言(C11)
_Static_assert(sizeof(int) == 4, "int must be 4 bytes");
_Static_assert(sizeof(long) >= 4, "long must be at least 4 bytes");

// 错误码定义
#define ERR_SUCCESS     0
#define ERR_INVALID    -1
#define ERR_NO_MEMORY  -2
#define ERR_NOT_FOUND  -3

// 使用assert验证前置条件
int divide(int a, int b) {
    assert(b != 0);  // 调试期检查除数不为零
    return a / b;
}

// 错误码模式示例
int find_element(int arr[], int size, int target, int *index) {
    if (arr == NULL || index == NULL) {
        return ERR_INVALID;
    }

    for (int i = 0; i < size; i++) {
        if (arr[i] == target) {
            *index = i;
            return ERR_SUCCESS;
        }
    }

    return ERR_NOT_FOUND;
}

// 错误恢复示例
int safe_divide(int a, int b, int *result) {
    if (result == NULL) {
        errno = EINVAL;
        return ERR_INVALID;
    }

    if (b == 0) {
        errno = EDOM;
        return ERR_INVALID;
    }

    *result = a / b;
    return ERR_SUCCESS;
}

void demonstrate_assert(void) {
    printf("=== assert示例 ===\n");

    int x = 10;
    assert(x > 0);  // 通过
    printf("assert(x > 0) passed\n");

    // assert(x < 0);  // 会失败并终止程序

    printf("divide(10, 2) = %d\n", divide(10, 2));
}

void demonstrate_errno(void) {
    printf("\n=== errno示例 ===\n");

    FILE *fp = fopen("nonexistent.txt", "r");
    if (fp == NULL) {
        printf("fopen failed\n");
        printf("errno = %d\n", errno);
        perror("perror message");
        printf("strerror(errno) = %s\n", strerror(errno));
    }
}

void demonstrate_error_code(void) {
    printf("\n=== 错误码模式 ===\n");

    int arr[] = {10, 20, 30, 40, 50};
    int size = sizeof(arr) / sizeof(arr[0]);
    int index;
    int result;

    // 成功情况
    result = find_element(arr, size, 30, &index);
    if (result == ERR_SUCCESS) {
        printf("Found 30 at index %d\n", index);
    }

    // 未找到情况
    result = find_element(arr, size, 99, &index);
    if (result == ERR_NOT_FOUND) {
        printf("99 not found in array\n");
    }

    // 无效参数情况
    result = find_element(NULL, size, 30, &index);
    if (result == ERR_INVALID) {
        printf("Invalid parameter detected\n");
        perror("find_element");
    }
}

void demonstrate_error_recovery(void) {
    printf("\n=== 错误恢复 ===\n");

    int result;
    int ret;

    // 正常情况
    ret = safe_divide(10, 2, &result);
    if (ret == ERR_SUCCESS) {
        printf("10 / 2 = %d\n", result);
    }

    // 错误情况
    ret = safe_divide(10, 0, &result);
    if (ret != ERR_SUCCESS) {
        printf("Division by zero detected\n");
        printf("Error: %s\n", strerror(errno));
    }
}

int main() {
    demonstrate_assert();
    demonstrate_errno();
    demonstrate_error_code();
    demonstrate_error_recovery();

    return 0;
}

编译命令:

# 调试版本(assert生效)
gcc -Wall -Wextra -o assert_demo assert_demo.c
./assert_demo

# Release版本(assert禁用)
gcc -Wall -Wextra -DNDEBUG -o assert_demo_release assert_demo.c
./assert_demo_release

22.4 注意事项与易错点

  1. assert是宏不是函数:参数可能被执行多次,不要有副作用
  2. assert在Release中失效:不要用assert进行运行时错误检查
  3. errno不是线程安全的:某些实现中errno是宏,多线程需使用线程安全版本
  4. assert参数中的副作用assert(func++)在Release中func不会递增
  5. 检查返回值:标准库函数失败时设置errno,应立即检查

22.5 面试要点(3-5个Q&A)

Q1: assert和错误码有什么区别? A: assert检测程序员的逻辑错误,调试期使用,Release禁用。错误码检测运行时错误(如文件不存在、内存不足),生产环境必须处理。

Q2: 什么时候用assert,什么时候用错误码? A: assert用于验证不变量和前置条件(如参数非空、数组索引有效)。错误码用于可预期的运行时错误(如文件打开失败、网络超时)。

Q3: 为什么说assert参数不能有副作用? A: Release版本中assert被移除,参数表达式不执行。如assert(func++)在Release中func不会递增,导致程序行为不一致。

Q4: errno有什么局限性? A: errno只在函数失败时有效,成功时不会清零。多线程环境下某些实现errno是宏保证线程安全,但标准不保证。

Q5: 嵌入式系统中如何处理致命错误? A: 根据严重程度:1) 记录日志并继续 2) 重置模块 3) 触发看门狗重启 4) 进入安全状态。关键系统需要冗余设计和故障恢复机制。