<assert.h>
<assert.h> 提供了 assert 宏,用于在调试阶段检查程序的不变量 (Invariant) 和前置条件 (Precondition)。它的定位是“尽快失败并暴露 bug”,而不是“把错误优雅地处理掉”。
1. 基本用法
c
#include <assert.h>
int div_int(int a, int b) {
assert(b != 0);
return a / b;
}1
2
3
4
5
6
2
3
4
5
6
当断言表达式为假(0)时,程序会打印诊断信息并终止。
2. 断言失败时会发生什么
当 assert(expr) 失败时,它会:
- 向标准错误输出(
stderr)输出一条诊断信息(其具体格式由实现决定)。 - 终止程序(通常通过调用
abort)。
因此,断言失败通常意味着:程序已经进入了“不应该到达”的状态,继续运行只会产生更多未知后果。
如何理解断言的角色
把 assert 视为“写给作者自己的合同”。只要合同被撕毁,就应该立刻停机检查,而不是硬着头皮继续跑下去。
3. NDEBUG:断言可能被移除
如果在包含 <assert.h> 之前定义了宏 NDEBUG,那么 assert 会被禁用,通常等价于:
c
assert(any_expression);
/* 展开后近似为 */
((void)0);1
2
3
2
3
这带来两个直接后果:
- 断言表达式可能不会被求值。
- 不要在断言表达式里编写依赖副作用的代码。
示例:
c
#include <assert.h>
#include <stdio.h>
int main(void) {
int i = 0;
assert(i++ == 0); // 若断言被禁用,i++ 不会执行。
printf("%d\n", i);
return 0;
}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
在启用断言的情况下,可能输出:
bash
1
在禁用断言的情况下,可能输出:
bash
0
4. 常见使用场景
- 检查内部前置条件:例如函数要求“参数指针一定非空”。
- 维护不变量:例如数据结构的大小关系、索引边界等。
- 标记不可达分支:例如
switch的default分支在逻辑上不可能发生。
示例(不可达分支):
c
#include <assert.h>
enum color {
COLOR_RED,
COLOR_GREEN,
COLOR_BLUE,
};
const char* color_name(enum color c) {
switch (c) {
case COLOR_RED: {
return "red";
}
case COLOR_GREEN: {
return "green";
}
case COLOR_BLUE: {
return "blue";
}
default: {
assert(!"unreachable");
return "unknown";
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
断言不是输入校验
“用户输入不合法”“文件打不开”这类失败属于外部世界的不确定性,应该用 if 分支、返回值与错误码进行处理,而不是用 assert 把程序直接杀死。
5. 习题
- 阅读下面代码,分别说明在“启用断言”和“禁用断言(定义
NDEBUG)”时程序的输出与行为差异:
c
#include <assert.h>
#include <stdio.h>
int main(void) {
int x = 0;
assert(++x == 1);
printf("%d\n", x);
return 0;
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
- 在你自己的项目中实现一个宏
MDR_ASSERT(cond):- 默认行为与
assert类似。 - 当定义
NDEBUG时被禁用。 - 要求:不得对
cond进行多次求值。
- 默认行为与
- 写一个函数
int pop(stack* s, int* out);:- 哪些条件适合用
assert,哪些条件必须通过返回值向调用者报告错误? - 给出你选择的错误处理策略,并写出函数签名与返回值含义。
- 哪些条件适合用