C++ assert 断言

在 C++ 中,assert(断言)是一个专门用于 “开发与测试阶段” 的内部自检工具。所谓的 “断言”,意思就是断定程序运行到这里时,某一个条件一定成立。

比如一个内部计算矩形面积的函数,正常情况下收到的宽度和高度都应该大于 0。一旦前面某一步算错了传进负数,使用 assert 就能立即报错并终止运行,帮我们在源头找出问题。

C++ assert 基本用法

在 C++ 中,如果想要使用 assert,我们需要先引入 <cassert> 头文件。

语法:

assert(条件表达式);

说明:

assert 是通过宏定义实现的,平时我们直接像调用普通函数那样使用它就可以了。当程序执行到 assert 语句时,会根据括号里条件的真假做出以下 2 种反应:

  • 条件为真(true):说明当前程序状态符合预期,assert 什么也不会做,程序继续正常往下执行。
  • 条件为假(false):说明程序内部出现了违背预期的逻辑错误。此时 assert 会在终端打印出报错所在的源文件名、行号以及未通过的条件表达式,随后立即终止程序。

下面我们先来看一个断言条件成立、程序正常运行的例子。假设我们要编写一个计算矩形面积的函数 calcArea(),矩形的宽度 width 和高度 height 都必须大于 0。

示例 1:断言条件成立

#include <cassert>
#include <iostream>

// 计算矩形面积
int calcArea(int width, int height) {
    assert(width > 0);
    assert(height > 0);
    return width * height;
}

int main() {
    std::cout << "矩形面积:" << calcArea(5, 4) << '\n';
    return 0;
}

运行结果如下。

矩形面积:20

分析:

在这个例子中,我们调用 calcArea(5, 4) 传入的宽度为 5、高度为 4。进入函数后,width > 0 和 height > 0 两个条件都为 true,程序顺利通过检查并算出面积 20。

当我们需要检查多个独立条件时,建议像上面这样拆成多行独立的 assert 语句,不要使用 && 合并进同一个 assert 里。如果把多个条件挤在同一行,一旦断言失败,你就很难直接看出到底是哪一个条件没通过。

如果传入的高度变成了负数 -2,程序会有什么反应呢?我们来看下面这个例子。

示例 2:断言失败终止程序

#include <cassert>
#include <iostream>

// 计算矩形面积
int calcArea(int width, int height) {
    assert(width > 0);
    assert(height > 0);
    return width * height;
}

int main() {
    std::cout << "矩形面积:" << calcArea(5, -2) << '\n';
    return 0;
}

运行结果如下。

(报错)main.cpp:7: int calcArea(int, int): Assertion `height > 0' failed.

分析:

调用 calcArea(5, -2) 时,第 6 行的 width > 0 检查通过,但第 7 行的 height > 0 计算结果为 false。程序随即触发断言失败报错(Assertion failed),意思是 height > 0 这条断言检查没有通过。

此时程序不会继续往下计算面积,而是在终端标出报错发生在 main.cpp 的第 7 行以及具体的失败条件,随后立即终止运行。这样一来,小伙伴们根据终端提示就能快速定位到出问题的代码行。

注意 assert 断言失败时是直接调用标准库的 std::abort() 函数终止程序,并不会抛出异常。因此,不要尝试在外层使用 try...catch 去捕获 assert 的失败。

使用 NDEBUG 关闭断言

如果在代码里写了很多 assert,等程序正式发布后每次运行都要检查这些条件,会不会影响运行效率呢?

这一点小伙伴们不用担心。等程序测试完毕、准备正式发布时,我们只需要定义一个名为 NDEBUG 的宏,就能把项目里所有的 assert 检查全部关闭。

其中,NDEBUG 是 “No Debug(非调试模式)” 的缩写。关闭 assert 主要有 2 种方式:① 在源文件中定义 NDEBUG;② 在编译命令中添加 -DNDEBUG 参数。

1. 在源文件中定义 NDEBUG

在平时单文件练习时,最直接的做法是在源文件的最顶部写上一行 #define NDEBUG。需要注意,这行代码必须放在 #include <cassert> 之前才行。

示例 3:定义 NDEBUG 关闭断言

#define NDEBUG    // 关闭当前文件中的 assert
#include <cassert>
#include <iostream>

int calcArea(int width, int height) {
    assert(width > 0);
    assert(height > 0);
    return width * height;
}

int main() {
    std::cout << "矩形面积:" << calcArea(5, -2) << '\n';
    return 0;
}

运行结果如下。

矩形面积:-10

分析:

当 #define NDEBUG 写在 #include <cassert> 上方时,<cassert> 头文件在引入时检测到已经定义了 NDEBUG,就会把 assert 替换为空操作。也就是说,代码里的两条 assert 语句在预处理阶段直接被忽略了。

对比示例 2 可以看到,哪怕传入的依然是错误的 calcArea(5, -2),程序也没有报错终止,而是直接算出了 -10。

下面这种写法就是错误的。如果把 #define NDEBUG 写在了 #include <cassert> 的后面,由于引入头文件时还没有定义该宏,代码里的 assert 依然会照常生效报错。

#include <cassert>
#define NDEBUG

2. 在编译命令中添加 -DNDEBUG

在实际项目开发中,如果有几十个 .cpp 源文件,我们不可能在发布前挨个打开文件去手写 #define NDEBUG。更省事的做法是保持源代码不动,在使用 g++ 编译程序时直接加上 -DNDEBUG 参数。其中,-D 表示在编译阶段定义一个宏,具体命令如下:

# 开发调试阶段:正常开启 assert
g++ -std=c++20 main.cpp -o app

# 正式发布阶段:关闭所有 assert
g++ -std=c++20 -DNDEBUG main.cpp -o app

C++ assert 的注意事项

虽然 assert 使用起来很方便,但如果没弄清它的适用边界,反而容易埋下隐患。在实际开发中,我们主要注意以下 2 个方面。

1. 不要在 assert 中修改数据

既然定义了 NDEBUG 之后,整条 assert 语句都会在预处理阶段被移除,括号里面的表达式就只应该做 “纯粹的条件判断”,不能包含修改变量值或调用业务函数的操作。比如把自增运算直接写进 assert 里就会引发问题。

// 错误方式
assert(++count > 0);

// 正确方式
count++;
assert(count > 0);

在上面的错误方式中,平时调试没有定义 NDEBUG 时,++count 会正常执行。一旦到了正式发布阶段加上 NDEBUG,整行 assert 都会被预处理器删掉。这样一来,count 自增这一步也跟着消失了,很容易引发 “调试时正常,一发布就出错” 的隐蔽问题。

2. 区分 assert 与运行期错误处理

既然 assert 能检查条件,那它能不能代替 if...else 语句或异常处理呢?实际上这两者的职责完全不同,我们可以按错误的来源把它们区分清楚:

  • 内部逻辑自检:比如内部函数约定传入的指针不能为空,或者内部算出来的下标必然合法。只要代码逻辑写对了,这类错误原本就不应该发生,适合在开发阶段使用 assert 及时暴露代码自身的 Bug。
  • 外部输入与环境错误:比如用户在键盘上输入了负数年龄、启动时找不到配置文件或者网络连接中断。这类情况在正式发布后依然随时可能发生,必须使用 if...else 或异常处理在运行期妥善应对,不能依赖发布后就会失效的 assert。

用一句话来区分就是:“查内部 Bug” 靠 assert,“防外部意外” 靠 if...else 或异常。

常见问题

1. assert 和 static_assert 有什么区别?

除了 assert 之外,平时阅读 C++ 代码时大家有时还会见到一个名字很像的 static_assert(静态断言)。它们的主要区别在于检查发生的时机不同:

assert(运行期断言):需要引入 <cassert> 头文件,在程序运行起来之后才检查条件,因此可以检查运行期动态变化的普通变量。如果条件不成立,程序会在运行期报错终止。

static_assert(编译期断言):是 C++11 引入的关键字,无需引入头文件,在编译阶段就直接检查常量表达式。比如配合 sizeof 运算符编写 static_assert(sizeof(int) == 4, "需要 32 位 int"),一旦条件不成立,编译阶段就会直接报错。

上一篇:

下一篇:

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号