Skip to content

C 标准库 - <assert.h>

概述

<assert.h> 头文件定义了 assert() 宏,用于在程序运行时验证假设条件是否成立。如果条件为假(即表达式求值为 0),程序会打印诊断信息并终止执行。

assert 宏

函数原型

c
void assert(scalar expression);

功能

assert() 宏在调试阶段非常有用,它可以:

  1. 验证程序中的假设条件
  2. 在条件不满足时提供详细的错误信息
  3. 帮助快速定位和修复程序中的错误

工作原理

assert(expression) 被调用时:

  1. 如果 expression 的值为真(非零),程序继续执行
  2. 如果 expression 的值为假(零),程序会:
    • 打印诊断信息到标准错误流(stderr)
    • 调用 abort() 函数终止程序

诊断信息通常包括:

  • 源文件名
  • 行号
  • 失败的表达式

基本用法

简单示例

c
#include <stdio.h>
#include <assert.h>

int main() {
    int x = 10;
    
    assert(x > 0);
    printf("x = %d\n", x);
    
    x = -5;
    assert(x > 0);  // 这会触发断言失败
    
    printf("这行不会执行\n");
    return 0;
}

输出(当断言失败时):

x = 10
Assertion failed: x > 0, file test.c, line 10

数组边界检查

c
#include <stdio.h>
#include <assert.h>

#define ARRAY_SIZE 10

int get_element(int arr[], int index) {
    assert(index >= 0 && index < ARRAY_SIZE);
    return arr[index];
}

int main() {
    int numbers[ARRAY_SIZE] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    
    printf("numbers[5] = %d\n", get_element(numbers, 5));
    printf("numbers[15] = %d\n", get_element(numbers, 15));  // 断言失败
    
    return 0;
}

指针检查

c
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

void process_data(int *data) {
    assert(data != NULL);
    printf("处理数据: %d\n", *data);
}

int main() {
    int value = 42;
    process_data(&value);
    
    process_data(NULL);  // 断言失败
    
    return 0;
}

禁用断言

使用 NDEBUG 宏

在发布版本中,通常需要禁用断言以提高性能。可以通过定义 NDEBUG 宏来实现:

c
#define NDEBUG
#include <assert.h>

int main() {
    int x = -5;
    assert(x > 0);  // 这个断言不会被执行
    printf("程序继续执行\n");
    return 0;
}

编译时定义 NDEBUG

使用编译器选项定义 NDEBUG

bash
gcc -DNDEBUG program.c -o program

条件编译

c
#ifdef DEBUG
    #include <assert.h>
    #define ASSERT(expr) assert(expr)
#else
    #define ASSERT(expr) ((void)0)
#endif

int main() {
    int x = -5;
    ASSERT(x > 0);  // 在 DEBUG 模式下会检查
    printf("程序继续执行\n");
    return 0;
}

实际应用场景

1. 函数参数验证

c
#include <assert.h>
#include <string.h>

char* safe_strcpy(char* dest, const char* src, size_t dest_size) {
    assert(dest != NULL);
    assert(src != NULL);
    assert(dest_size > 0);
    
    strncpy(dest, src, dest_size - 1);
    dest[dest_size - 1] = '\0';
    
    return dest;
}

2. 不变量检查

c
#include <assert.h>

typedef struct {
    int* data;
    size_t size;
    size_t capacity;
} Vector;

void vector_push_back(Vector* vec, int value) {
    assert(vec != NULL);
    assert(vec->size <= vec->capacity);
    
    if (vec->size == vec->capacity) {
        vec->capacity *= 2;
        vec->data = realloc(vec->data, vec->capacity * sizeof(int));
        assert(vec->data != NULL);
    }
    
    vec->data[vec->size++] = value;
    
    assert(vec->size <= vec->capacity);
}

3. 状态验证

c
#include <assert.h>

typedef enum {
    STATE_IDLE,
    STATE_RUNNING,
    STATE_PAUSED,
    STATE_STOPPED
} State;

typedef struct {
    State current_state;
    int counter;
} Machine;

void machine_start(Machine* m) {
    assert(m != NULL);
    assert(m->current_state == STATE_IDLE);
    
    m->current_state = STATE_RUNNING;
    m->counter = 0;
}

void machine_pause(Machine* m) {
    assert(m != NULL);
    assert(m->current_state == STATE_RUNNING);
    
    m->current_state = STATE_PAUSED;
}

自定义断言处理

自定义断言宏

c
#include <stdio.h>
#include <stdlib.h>

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

int main() {
    int x = -5;
    CUSTOM_ASSERT(x > 0, "x 必须大于 0");
    return 0;
}

带日志的断言

c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define LOG_ASSERT(expr) \
    do { \
        if (!(expr)) { \
            time_t now = time(NULL); \
            fprintf(stderr, "[%s] 断言失败: %s\n", \
                    ctime(&now), #expr); \
            fprintf(stderr, "文件: %s, 行: %d\n", __FILE__, __LINE__); \
            abort(); \
        } \
    } while (0)

int main() {
    int x = -5;
    LOG_ASSERT(x > 0);
    return 0;
}

断言的最佳实践

1. 只用于调试

断言主要用于开发和调试阶段,不应该用于处理运行时错误:

c
// 好的做法
void* allocate_memory(size_t size) {
    void* ptr = malloc(size);
    assert(ptr != NULL);  // 调试时检查
    return ptr;
}

// 更好的做法(生产环境)
void* safe_allocate_memory(size_t size) {
    void* ptr = malloc(size);
    if (ptr == NULL) {
        fprintf(stderr, "内存分配失败\n");
        exit(EXIT_FAILURE);
    }
    return ptr;
}

2. 避免副作用

断言表达式不应该有副作用:

c
// 不好的做法
int i = 0;
assert(i++ > 0);  // 副作用:i 会增加

// 好的做法
int i = 0;
assert(i > 0);    // 无副作用
i++;

3. 提供有意义的条件

断言条件应该清晰表达程序的假设:

c
// 不好的做法
assert(flag);

// 好的做法
assert(is_valid_state(state));
assert(ptr != NULL && *ptr > 0);

4. 不要用于用户输入验证

断言不应该用于验证用户输入:

c
// 不好的做法
void process_user_input(int value) {
    assert(value >= 0 && value <= 100);  // 用户输入不应该用断言
}

// 好的做法
void process_user_input(int value) {
    if (value < 0 || value > 100) {
        fprintf(stderr, "输入值必须在 0 到 100 之间\n");
        return;
    }
}

常见错误

1. 在发布版本中依赖断言

c
// 危险的做法
int divide(int a, int b) {
    assert(b != 0);  // 发布版本中这个检查会被禁用
    return a / b;
}

// 安全的做法
int divide(int a, int b) {
    if (b == 0) {
        fprintf(stderr, "除零错误\n");
        return 0;
    }
    return a / b;
}

2. 使用复杂的表达式

c
// 不好的做法
assert((ptr != NULL) && (*ptr > 0) && (ptr[1] < 100));

// 好的做法
assert(ptr != NULL);
assert(*ptr > 0);
assert(ptr[1] < 100);

总结

<assert.h> 提供的 assert() 宏是一个强大的调试工具,它可以帮助开发者:

  1. 快速发现和定位程序中的错误
  2. 文档化程序的假设和不变量
  3. 提高代码的可靠性

正确使用断言可以显著提高代码质量,但需要注意:

  • 断言主要用于调试,不应该用于处理运行时错误
  • 发布版本中应该禁用断言
  • 避免在断言表达式中使用有副作用的代码
  • 为断言提供清晰、有意义的条件表达式