26、断言 assert:什么时候用?怎么用?

说实话,很多C语言开发者对 assert 的态度挺两极分化的。

有人觉得它可有可无,调试完就删掉。有人把它当宝贝,每个函数入口都塞几个。

我个人习惯是:该用的时候一个不能少,不该用的时候一个都不加

今天咱们就把 assert 这件事聊透。

assert 到底是什么?

assert 是标准库 <assert.h> 提供的一个宏。它的作用很简单:在运行时检查一个条件是否为真。如果条件为假,程序会立即终止,并打印出失败的文件名、行号和表达式内容。

#include <assert.h>

int divide(int a, int b) {
    assert(b != 0);  // 除数不能为0
    return a / b;
}

你看,这就是最典型的用法。如果 b 是0,程序直接崩掉,告诉你「第5行出事了」。

核心要点:assert 只在调试模式下生效。如果你定义了 NDEBUG 宏,所有 assert 都会被预处理器移除,不产生任何代码。

什么时候该用 assert?

这个问题我当年也困惑过。后来在项目中踩了几次坑,慢慢总结出几条原则。

1. 检查「绝不应该发生」的情况

assert 不是用来处理错误的。它是用来检查逻辑上的不可能

举个例子:你写了一个链表查找函数,理论上如果链表是空的,调用者就不该来找。但万一调用者犯傻呢?

Node* find_node(List* list, int key) {
    assert(list != NULL);       // 链表指针不能为空
    assert(list->head != NULL); // 链表不能为空链表
    // ... 查找逻辑
}

这里 assert 是在说:「调用者,你承诺过链表不为空,现在我来验证一下。」

2. 检查函数的前置条件

我在项目中遇到过这样一个 bug:一个排序函数,传入的数组指针是 NULL,结果跑了半天才崩,定位问题花了两小时。

从那以后,我所有对外接口的函数,入口处都会加 assert:

void sort_array(int* arr, size_t len) {
    assert(arr != NULL);    // 数组指针不能为空
    assert(len > 0);        // 长度必须大于0
    // ... 排序逻辑
}

你想想看,如果调用者传了非法参数,程序在函数入口就崩掉,是不是比跑到一半再崩好得多?

3. 检查后置条件

有些函数执行完后,某些条件必须成立。比如内存分配:

void* my_malloc(size_t size) {
    void* ptr = malloc(size);
    assert(ptr != NULL);  // 分配失败就崩
    return ptr;
}

嗯,这里要注意:生产代码中不要用 assert 检查 malloc。因为 release 版本下 assert 会被移除,等于没检查。这里只是举例说明后置条件的用法。

4. 检查 switch-case 的默认分支

这个技巧我特别喜欢。当你写了一个枚举类型的 switch,理论上所有分支都覆盖了,但万一以后枚举增加了新值呢?

typedef enum { RED, GREEN, BLUE } Color;

void print_color(Color c) {
    switch (c) {
        case RED:   printf("红色"); break;
        case GREEN: printf("绿色"); break;
        case BLUE:  printf("蓝色"); break;
        default:
            assert(0 && "未知颜色");
    }
}

这样一旦有人给 Color 加了新值但忘了更新 switch,调试时立刻就能发现。

什么时候不该用 assert?

说白了,assert 不是万能的。有些场景用了反而坏事。

场景 为什么不能用 assert 应该用什么
用户输入校验 用户输入不可控,release 版本下 assert 被移除,等于没校验 if-else + 错误处理
文件打开失败 文件可能不存在,这是正常情况,不是逻辑错误 检查返回值 + 错误码
网络连接超时 这是运行时环境问题,不是程序逻辑错误 超时重试 + 错误处理
内存分配失败 release 版本下 assert 消失,等于没检查 if (ptr == NULL) 处理

我曾经犯过的错:在一个嵌入式项目中,用 assert 检查了串口接收缓冲区的溢出。结果发布版本下 assert 被移除,缓冲区溢出导致系统死机,现场排查了三天才发现是这个问题。从那以后,所有「可能发生」的错误我都用 if 处理,只有「绝不可能发生」的情况才用 assert。

assert 的隐藏用法:静态断言

C11 标准引入了 static_assert,它是在编译期检查的,不依赖运行时。

这个在嵌入式开发中特别有用。比如检查结构体大小是否符合预期:

#include <assert.h>

typedef struct {
    uint8_t  id;
    uint32_t value;
    uint16_t flag;
} __attribute__((packed)) Message;

static_assert(sizeof(Message) == 7, "Message 结构体大小异常");

如果结构体因为对齐或其他原因变成了8字节,编译直接报错,不会等到运行时才发现。

SVG:assert 使用决策流程图

assert 使用决策流程 遇到一个检查点 这种情况 可能发生吗? 用 if 处理 返回错误码/错误处理 继续判断 编译期 可检查? 用 static_assert 编译期检查 用 assert 运行时检查 核心原则:assert 只用于「绝不可能发生」的逻辑错误,不用于「可能发生」的运行时错误

避坑指南

最后分享几个我踩过的坑,希望能帮你少走弯路。

  • 不要在 assert 里写有副作用的表达式。比如 assert(++x > 0),release 版本下 ++x 不会执行,逻辑就错了。
  • 不要用 assert 代替参数校验。对外接口的参数校验应该用 if,assert 只用于内部逻辑的「自检」。
  • 嵌入式开发中慎用 assert。有些嵌入式平台没有标准输出,assert 崩了连错误信息都看不到。我一般会自己实现一个类似的宏,把错误信息写到日志缓冲区。
  • 发布版本记得定义 NDEBUG。否则 assert 代码会留在最终二进制里,既占空间又影响性能。

我的个人习惯:在项目里维护一个 debug_assert.h,里面封装了 assert 的变体。调试阶段用标准 assert,发布阶段替换成自己的日志记录函数。这样既保留了调试信息,又不会让程序直接崩溃。

好了,关于 assert 的用法就聊到这里。记住一句话:assert 是你的 debug 伙伴,不是你的错误处理机制。用对了地方,它能帮你快速定位 bug;用错了地方,它会在发布版本里给你挖坑。


公众号:蓝海资料掘金营,微信deep3321