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 里写有副作用的表达式。比如
assert(++x > 0),release 版本下++x不会执行,逻辑就错了。 - 不要用 assert 代替参数校验。对外接口的参数校验应该用 if,assert 只用于内部逻辑的「自检」。
- 嵌入式开发中慎用 assert。有些嵌入式平台没有标准输出,assert 崩了连错误信息都看不到。我一般会自己实现一个类似的宏,把错误信息写到日志缓冲区。
- 发布版本记得定义 NDEBUG。否则 assert 代码会留在最终二进制里,既占空间又影响性能。
我的个人习惯:在项目里维护一个 debug_assert.h,里面封装了 assert 的变体。调试阶段用标准 assert,发布阶段替换成自己的日志记录函数。这样既保留了调试信息,又不会让程序直接崩溃。
好了,关于 assert 的用法就聊到这里。记住一句话:assert 是你的 debug 伙伴,不是你的错误处理机制。用对了地方,它能帮你快速定位 bug;用错了地方,它会在发布版本里给你挖坑。
公众号:蓝海资料掘金营,微信deep3321