在 C++ 中,断言是一种用于陈述或断定表达式必须为true
的语句。
它用于检查除非存在 bug 否则不可能发生的情况。因此,它被用作调试工具,因为它会在断言变为false
时终止程序。
例如,如果我们想计算一个非空的 vector nums 的平均值,那么断言
nums.size() > 0
必须为真,除非程序中存在 bug。
创建 C++ 断言
在 C++ 中,我们可以使用 assert
预处理器宏来断言,该宏定义在 cassert
头文件中。
#include <cassert>
导入此文件后,我们可以使用以下语法创建断言
assert(expression);
在这里,如果expression
求值为
- 0(false)- assert 打印一条消息并终止程序
- 1(true)- 不做任何操作,继续程序的正常执行
示例 1:C++ assert
#include <iostream>
#include <cassert>
using namespace std;
int main() {
int even_num = 3;
// asserts the value of even_num must be even
assert((even_num % 2 == 0));
return 0;
}
输出
Assertion `(even_num % 2 == 0)' failed. Aborted
在上面的示例中,我们使用了 assert
宏来断言 even_num 的值应该是偶数。
assert(even_num % 2 == 0);
由于 even_num 的值为3,断言 even num % 2 == 0
失败。因此,程序终止并显示错误消息。
但是,如果我们把 even_num 改为2,程序就能无错误地执行。
注意: C++ assert
宏不提供自定义错误消息的参数。但是,我们可以使用逗号运算符添加错误描述。
assert(("The number should be even", even_num % 2 == 0));
这将产生错误消息
Assertion `("The number should be even", even_num % 2 == 0)' failed
禁用 C++ 中的 assert
断言用于检查除非存在 bug 否则不应发生的情况。因此,它们被用作调试工具。
因此,在应用程序发布之前,我们应该删除断言,因为已发布的构建应该能够正常运行,并且永远不应触发断言。
禁用断言的一种方法是在调试完成后在代码中搜索 assert 宏并手动删除它。
一个更好的方法是使用 NDEBUG
宏来禁用它。此宏会全局忽略所有断言。我们可以通过在包含 cassert
头文件之前使用 #define
指令来实现此宏。
因此,当我们完成调试并且不再需要断言时,可以添加以下代码。
#define NDEBUG
#include <cassert>
注意: 对于具有多个文件的更大项目,NDEBUG
可以通过编译器命令行选项或 IDE 设置来设置。
静态断言
assert
宏用于运行时断言。相比之下,static_assert
用于编译时断言。
static_assert
的语法是
static_assert(const_boolean_expression, message);
这里,
const_boolean_expression
- 编译时已知表达式message
- 如果断言失败时显示的消息
由于 static_assert
是一个关键字,我们不需要包含任何头文件。
示例 2:C++ Static Assert
#include <iostream>
using namespace std;
int main() {
static_assert(sizeof(int) >= 4, "Size of integer must be greater than or equal to 4 bytes");
return 0;
}
在上面的示例中,我们使用了静态断言,以便在代码运行的平台上整数的大小至少为4字节。
由于数据类型的大小可能因平台而异,因此我们可以使用静态断言在编译时进行断言。
与在运行时进行的 assert
不同,如果 static_assert
失败,程序将无法编译。
在这里,如果在旧系统(16 位)上运行程序,断言将失败,因为 sizeof(int)
是2 字节。
示例 3:C++ Static Assert - 泛型编程
#include <iostream>
using namespace std;
template <class T, int size>
class Container {
static_assert(size > 0, "The size of container cannot be less than 1");
T items[size];
};
int main() {
Container<int, 0> st;
return 0;
}
输出
error: static assertion failed: The size of container cannot be less than 1
在上面的示例中,我们使用静态断言来确保泛型类中数组的大小大于0。
在这里,我们创建了一个 Container
类的对象,使用
Container <int, 0> st;
由于 size 的值为0,编译会因为静态断言失败而中止。
何时使用断言
1. 不可达代码
这些是尝试运行程序时不会执行的代码。使用断言来确保不可达代码确实不可达。
让我们举个例子。
void unreachable_code_method() {
cout << "Reachable code";
return;
// Unreachable code
cout << "Unreachable code";
assert(false);
}
让我们以一个没有 default
情况的 switch
语句为例。
switch (day_of_week) {
case 1:
cout << "It's Sunday!";
break;
case 2:
cout << "It's Monday!";
break;
case 3:
cout << "It's Tuesday!";
break;
case 4:
cout << "It's Wednesday!";
break;
case 5:
cout << "It's Thursday!";
break;
case 6:
cout << "It's Friday!";
break;
case 7:
cout << "It's Saturday!";
break;
上面的 switch
语句表示一周的日期只能是1 到 7。没有 default
情况意味着程序员认为其中一种情况总是会被执行。
但是,可能还有一些尚未考虑到的情况,其中假设实际上是错误的。
应该使用断言来检查此假设,以确保不会达到 default
switch 情况。
default:
assert(("An Invalid Day", false));
如果 day_of_week 的值不是1 - 7 之间的任何值,则会发生断言错误。
2. 记录假设
为了记录其基本假设,许多程序员使用注释。让我们看一个例子。
if (i % 2 == 0) {
...
}
else { // We know (i % 2 == 1)
...
}
请改用断言。
随着程序的增长,注释可能会过时且不同步。但是,我们将被迫更新 assert
宏;否则,它们可能会因有效条件而失败。
if (i % 2 == 0) {
...
}
else {
assert(i % 2 == 1);
...
}
何时不使用断言
1. 公共函数中的参数检查
公共函数中的参数可能由用户提供。
因此,如果使用断言来检查这些参数,条件可能会失败并导致断言错误。
不要使用断言,而是让其导致适当的运行时异常并处理这些异常。
2. 评估影响程序运行的表达式
不要调用方法或评估会导致副作用的异常。例如,
int n = 5;
// assertion with side effects
// here assertion is decrementing 'n' and asserts if n is even
assert(--n && n % 2 == 0);
在上面的示例中,语句 --n && n % 2 == 0
具有副作用,即它修改了变量 n 的值。此外,仅当启用断言时,assert
宏才会运行。
但是,如果我们使用 NDEBUG
宏禁用了断言,则上述递减语句永远不会被评估,因此如果 n 在代码的后续部分中被引用,则会影响整个代码。
因此,不要在 assert
宏中评估具有副作用的表达式,而是可以这样写
int n = 5;
//decrement n
--n;
// assertion without side effects
// here assertion asserts if n is even
assert(n % 2 == 0);
这确保了 n 的值在断言被启用或禁用时是相同的。