#ifdef NDEBUG
#define assert(expr) (static_cast<void> (0))
#else
......
#endif
assert宏是在标准库中提供的。它在库文件中声明,它可以在程序中测试逻辑表达式,如果指定的逻辑表达式是false,assert()就会终止程序,并显示诊断消息。关闭断言使用#define NDEBUG,该语句会忽略转换单元中的所有断言语句。而且这个指令仅放在#include 之前才有效。示例如下:
#include
#define NDEBUG //关闭所有断言,必须放在#include 之前
#include
using namespace std;
int main()
{
int a = 10, b = 2;
//使用断言,若assert()中为false,则程序终止退出
assert(a < b);
cout << a << b << endl;
return 0;
}
在断言的使用中,应该遵循这样的一个规定:对来自系统内部的可靠数据使用断言,对于外部不可靠数据不能使用断言,而应该使用错误处理代码。
换句话而言,断言是用来处理不应该发生的非法情况,而对于可能发生的应该使用错误处理代码。
对于用户输入,与外部系统进行协议交互时的情况,也不能使用断言进行参数的判断,这种情况属于正常的错误检查。
下面的例子说明了断言的使用场景
char * Strdup(const char * src){
assert(src!=NULL);
char * result = NULL;
size_t len = strlen(src) +1;
result = (char *)malloc(len);
assert(result != NULL);
return result;
}
复制
例子中第一个断言assert(src!=NULL)用于判断传入的参数的正确性,保证参数不为NULL 第二个断言assert(result != NULL)检查函数返回值是否为NULL。
例子中的两个断言,第一个是合法的,而第二个不合法,第一个合法是因为传入的参数必须不为NULL,断言如果成功,则说明调用代码存在问题,这属于非法的情况,此处属于断言的正确使用情况。
第二个断言则不同,malloc对于返回NULL的情况属于调用正常情况,这应该使用正常的错误处理逻辑,不应该使用断言。
在assert宏只有在Debug版本中情况下,应该避免断言表达式中使用改变环境的语句。
如下例子因为断言语句的缘故,将导致不同的编译版本产生不同的结果。
int test(int i)
{
assert(i++);
return i;
}
复制
因此应该避免在断言表达式中使用改变上下文环境的语句,也就是确保断言仅仅作为一个检查而存在,不应该参与正常语句的处理。
error 是用于表达不同错误值的一个全局变量。如果一个系统调用或库函数调用失败,可以通过errno的值来确定问题所在。
因errno是一个全局变量,在调用不同系统调用或者库函数失败时都有可能修改它的值,因为在使用errno时,应先将其清0
errno = 0;
FILE *fp = fopen("test.txt", "r");
if (fp == NULL) {
if (errno!=0) {
printf("error : %d \n",errno);
printf("错误信息 : %s \n",strerror(errno));
}
}
复制
但errno并不是所有的库函数都适合使用,就error而言库函数一般分为如下几种。
1.函数返回值无法判断错误,需进一步从errno中获取错误信息
函数 | 返回值 | errno值 |
---|---|---|
fgetwc、fputwc | WEOF | EILSEQ |
strtol、wcstol | LONG_MIN或LONG_MAX | ERANGE |
strtoll、wcstoll | LLONG_MIN或LLONG_MAX | ERANGE |
strtoul、wcstoul | ULONG_MAX | ERANGE |
strtoull、wcstoull | ULLONG_MAX | ERANGE |
strtoumax、wcstoumax | UINTLLONG_MAX | ERANGE |
strtod、wcstod | 0或者±HUGE_VAL | ERANGE |
strtof、wcstof | 0或者±HUGE_VALF | ERANGE |
strtold、wcstold | 0或者±HUGE_VALL | ERANGE |
strtoimax、wcstoimax | IMAX_MIN或INTMAX_MAX | ERANGE |
以字符串转成长整型函数strtol为例, 在64位机器下,long长度为8字节,最大值LONG_MAX 为 0x7fffffffffffffff,当变量longStr 取超出长整型最大值的字符串”0xffffffffffffffff”和刚好等于最大值的字符串”0x7fffffffffffffff”时,函数的返回值都为相同的LONG_MAX。此时金聪返回值是无法判断函数的执行的成功与否。这个时要判断errno的值。如下例中,会打印出错误的信息。
errno =0;
//LONG_MAX的最大值为0x7fffffffffffffff
const char * longStr = "0xffffffffffffffff";
long ret = strtol(longStr,NULL,16);
if (ret == LONG_MAX) {
if (errno!=0) {
printf("error : %d \n",errno);
printf("错误信息 : %s \n",strerror(errno));
}else{
printf("等于long的最大值\n");
}
}
复制
2.函数返回值可知错误,errno可知更详细的错误
函数 | 返回值 | errno值 | |
---|---|---|---|
ftell() | -1L | positive | |
fgetpos()、fsetpos() | nonzero | positive | |
mbrtowc()、mbsrtowcs() | (size_t)(-1) | EILSEQ | |
signal() | SIG_ERR | positive | |
wcrtomb()、wcsrtombs | (size_t)(-1) | EILSEQ | |
mbrtoc16()、mbrtoc32() | (size_t)(-1) | EILSEQ | |
c16rtomb()、cr21rtomb | (size_t)(-1) | EILSEQ |
3.有不同标准文档的库函数
有些函数在不同的标准下对errno有不同的定义,例如fopen中便是一个例子。C99并没有对使用fopen是对errno做要求,但POSIX.1却声明了错误时返回NULL,并将错误码写入errno。
goto语句有很多优点,例如goto语句可以非常方便的在局部作用域中跳出多层循环,执行如无条件的跳转。 但正因为goto语句可以灵活的跳转,如果不加以限制它会破坏程序的结构化风格,使得代码难以理解与测试,同时不加限制的使用goto语句可能跳过变量的初始化、重要的计算等语句。
以下例子在a小于0或者a小于等于100时会使用goto跳转到标记为Error的语句中。
注意goto只能在局部作用域中跳转。
void testGoto(int a)
{
if (a>0) {
if (a>100) {
printf(" a = %d \n",a);
}else{
goto Error;
}
}else{
goto Error;
}
Error:
printf("Test Error a = %d \n",a);
}
复制
相比与goto语句只能在局部作用域中跳转,setjump与longjmp可以进行跨作用域跳转,也就是跨函数跳转。
我们知道函数调用都以函数栈的形式进行调用与退出,既然要做到跨函数跳转,那便需要对当前的函数栈进行保存与还原,而setjmp的作用便是保存当前函数栈至类型jmp_buf结构体变量中,而longjmp的作用便是从此结构体中恢复,还原函数栈。
而相对于goto仅在作用域内跳转,setjmp和longjmp则使代码更加的难以维护以及可读。
在C++中,标准在 或vassert.h>头文件中为程序员提供了 assert宏,用于在运行时进行断言。前面错误示例改成assert宏实现就正确了。与静态断言使用的 static_assert 不同,assert 并不支持自定义错误消息。
// #define NDEBUG
#include
#include
template <typename T>
T *array_alloc(unsigned int size)
{
assert(size > 0);
return new T[size];
}
int main()
{
std::cout << "main start" << std::endl;
int *ah = array_alloc<int>(0);
// 编译通过但,运行时候报错
std::cout << "main end" << std::endl;
// 输出结果
// main start
// Assertion failed: size > 0, file e:\CodeFile\CPP_SINGLE\ASSERT\runningTime_assert\1-runtime_assert.cpp, line 6
}
// 需要注意的是,C 程序中的运行时断言是否可用,也会受到宏常量 NDEBUG 的影响。
// 当该宏常量的定义先于 #include 语句出现时,编译器会忽略对 assert 宏函数调用代码的编译。
// 反之,它便会在程序运行时进行正常的断言检查。通过这种方式,我们可以相对灵活地控制运行时断言的启用与关闭。
// 事实上,assert宏在中的实现方式类似:
#ifdef NDEBUG
#define assert(expr) (static_cast<void>(0))
#else
//其他
#endif
类似
#include
#include
#include
#include
using namespace std;
template <typename T, typename U>
void bit_copy(T &a, U &b)
{
static_assert(sizeof(a) == sizeof(b), "a and b must be equal");
memcpy(&a, &b, sizeof(a));
}
int main()
{
int a = 10;
int b = 30;
bit_copy(a, b);
cout << "ok" << endl;
}
处理的表达式必须在编译期间就能知道结果。
另外必须注意的是,static_assert的断言表达式的结果必须是在编译时期可以计算的表达式,即必须是常量表达式。如果读者使用了变量,则会导致错误,如:
int bit_deal (int a) (
static_assert( a>=1,"the parameters of a should >=1.");
);
上面使用了参数变量a ,因而static_assert无法通过编 译。如果需要对变量进行检查,就要是实现运行时的检查,使用assert宏了。
该关键字的用法类似C++11标准之前开源库 Boost内置的BOOST_STATIC_ASSERT断言机制类似,利用1/(e)这个表达式来判定:
#include
#include
#include
#include
using namespace std;
#define cp_assert(e) \
do \
{ \
enum \
{ \
assert_static = 1 / (e) \
}; \
\
\
} while (0)
template <typename T, typename U>
void bit_copy(T &a, U &b)
{
cp_assert(sizeof(a) == sizeof(b));
memcpy(&a, &b, sizeof(a));
}
int main()
{
int a = 10;
int b = 30;
bit_copy(a, b);
cout << "ok" << endl;
}
在通常情况下,static_assert可以用于任何名字空间,在预处理阶段,static_assert 宏会被展开成名为 _Static_assert 的 C 关键字。该关键字以类似“函数调用”的形式在 C 代码中使用,它的第一个参数接收一个常量表达式。程序在被编译时,编译器会对该表达式进行求值,并将所得结果与数字 0 进行比较。若两者相等,则程序终止编译,并会将通过第二个参数指定的错误信息,与断言失败信息合并输出。若两者不相等,程序会被正常编译,且该关键字对应的 C 代码不会生成任何对应的机器指令。
在 c/c++代码中,我们常能看到名为 errno 的预处理器宏:
1)#error 是一种预编译器指示字,用于生成一个编译错误消息 。
2)#error [message] //message为用户自定义的错误提示信息,可缺省。
3)#error 编译指示字用于自定义程序员特有的编译错误消息。
4)#error 可用于提示编译条件是否满足。编译过程中的任何错误意味着无法生成最终的可执行程序。
它常用的用法就是,通过预处理指令#if和 #error的配合,可以让程序员在预处理阶段进行断言。例如我们可以在程序中判断某个自定义宏是否存储而进行断言提示:
#ifndef MY_OPT
#error “MY_OPT not define in code, include
#endif
如果程序中没有包含需要的头文件
类似的,还有#warning 这种用于生成编译警告消息预处理宏。
main.cpp
#include "primary.h"
int main()
{
say_hi();
return 0;
}
primary.h
#ifndef _PRIMARY_H
#define _PRIMARY_H
#endif
#include "secondary.h" // 模拟 primary 依赖secondary 头文件的内容
// ......... 包含其他处理的代码
//. .........
secondary.h
#ifndef _PRIMARY_H
#error Never use secondary.h directly.Include primary.h instead.
#endif
#include
void say_hi()
{
printf("say hi\n");
}
如果再main 中只包含 secondary.h 而没有 包含 primary.h
#include "secondary.h"
int main()
{
say_hi();
return 0;
}
In file included from e:\CodeFile\CPP_SINGLE\ASSERT\3PreCompilie_assert\mian.cpp:1:
e:\CodeFile\CPP_SINGLE\ASSERT\3PreCompilie_assert\secondary.h:3:2: error: #error Never use secondary.h directly.Include primary.h instead.
3 | #error Never use secondary.h directly.Include primary.h instead.
| ^~~~~
```
# 总结
- \1. 断言(assertion)分类:运行时、预编译期、编译期,即断言的判断触发时机
- \2. 运行时断言使用assert宏来实现
- \3. 一旦定义了NDEBUG宏,那么assert宏将被展开成一条空语句不起作用,(可能会)被编译器优化掉,原意是在非Debug编译下不把运行时assert编进来
- \4. 预编译时期的断言可以使用预处理命令#ifdef或#ifundef配合#error实现,可以在预编译时期对一些宏进行检查以确定编译参数、头文件引用状态等
- \5. 编译期断言在C++11之前可以使用除0操作来实现,传入编译期可判定的表达式,则在编译期可计算出是true还是false(即0),然后除0可在编译期报告错误
- \6. 使用上一条中的方式拿到的是除0错误,语义不明朗,在C++11中编译期断言可直接用static_assert关键字实现,传入待判定的编译期可解的表达式以及断言失败时上报的错误信息