C++断言 static_assert, complie_assert, preComplie_assert

发布时间:2026/7/16 18:25:52

C++断言 static_assert, complie_assert, preComplie_assert C三类断言文章目录C三类断言前置为什么要用断言如何使用断言注意避免使用断言去检查程序错误避免在断言表达式中使用改变上下文的语句异常处理获取错误代码errno避免使用goto语句避免使用setjmp与longjmp小结三类断言运行期间断言编译期间断言预编译期间断言样例总结前置为什么要用断言首先要搞清楚为什么要用断言不能看别人代码中有就追赶时髦地用一用从效果上来说assert断言能用if语句替换那么为什么不用if语句把断言替换呢一般而言if语句是处理逻辑上的可能会发生的错误断言则用来处理不应该发生的状况。什么是不应该发的的状况呢这要区分数据的来源、数据来源于系统内部子程序、子模块间的调用、数据来源于系统外部外部设备如键盘的输入、串口数据的读取、网络数据的读取。对内部来源的数据我们没法去通过常规的测试手段去验证此时断言就用上了。当然你如果硬是要用if语句也没人说你不对但大量的if语句出现在源码中时会造成代码臃肿降低了可读性另外会产生不紧凑代码影响效率。程序开发初期码农们忽视的是程序间调用参数的合法性对这些参数可使用断言来防止意外随着程序进入release版时可以定义NDEBUG来让断言失效。以下是NDEGBU对assert的处理代码。#ifdefNDEBUG#defineassert(expr)(static_castvoid(0))#else......#endif如何使用断言assert宏是在标准库中提供的。它在库文件中声明它可以在程序中测试逻辑表达式如果指定的逻辑表达式是falseassert()就会终止程序并显示诊断消息。关闭断言使用#define NDEBUG,该语句会忽略转换单元中的所有断言语句。而且这个指令仅放在#include 之前才有效。示例如下#includeiostream#defineNDEBUG//关闭所有断言必须放在#include cassert之前#includecassertusingnamespacestd;intmain(){inta10,b2;//使用断言若assert中为false则程序终止退出assert(ab);coutabendl;return0;}注意避免使用断言去检查程序错误在断言的使用中应该遵循这样的一个规定对来自系统内部的可靠数据使用断言对于外部不可靠数据不能使用断言而应该使用错误处理代码。换句话而言断言是用来处理不应该发生的非法情况而对于可能发生的应该使用错误处理代码。对于用户输入与外部系统进行协议交互时的情况也不能使用断言进行参数的判断这种情况属于正常的错误检查。下面的例子说明了断言的使用场景char*Strdup(constchar*src){assert(src!NULL);char*resultNULL;size_t lenstrlen(src)1;result(char*)malloc(len);assert(result!NULL);returnresult;}复制例子中第一个断言assert(src!NULL)用于判断传入的参数的正确性保证参数不为NULL 第二个断言assert(result ! NULL)检查函数返回值是否为NULL。例子中的两个断言第一个是合法的而第二个不合法第一个合法是因为传入的参数必须不为NULL断言如果成功则说明调用代码存在问题这属于非法的情况此处属于断言的正确使用情况。第二个断言则不同malloc对于返回NULL的情况属于调用正常情况这应该使用正常的错误处理逻辑不应该使用断言。避免在断言表达式中使用改变上下文的语句在assert宏只有在Debug版本中情况下应该避免断言表达式中使用改变环境的语句。如下例子因为断言语句的缘故将导致不同的编译版本产生不同的结果。inttest(int i){assert(i);returni;}复制因此应该避免在断言表达式中使用改变上下文环境的语句也就是确保断言仅仅作为一个检查而存在不应该参与正常语句的处理。异常处理获取错误代码errnoerror 是用于表达不同错误值的一个全局变量。如果一个系统调用或库函数调用失败可以通过errno的值来确定问题所在。因errno是一个全局变量在调用不同系统调用或者库函数失败时都有可能修改它的值因为在使用errno时应先将其清0errno0;FILE*fpfopen(test.txt,r);if(fpNULL){if(errno!0){printf(error : %d \n,errno);printf(错误信息 : %s \n,strerror(errno));}}复制但errno并不是所有的库函数都适合使用就error而言库函数一般分为如下几种。1.函数返回值无法判断错误需进一步从errno中获取错误信息函数返回值errno值fgetwc、fputwcWEOFEILSEQstrtol、wcstolLONG_MIN或LONG_MAXERANGEstrtoll、wcstollLLONG_MIN或LLONG_MAXERANGEstrtoul、wcstoulULONG_MAXERANGEstrtoull、wcstoullULLONG_MAXERANGEstrtoumax、wcstoumaxUINTLLONG_MAXERANGEstrtod、wcstod0或者±HUGE_VALERANGEstrtof、wcstof0或者±HUGE_VALFERANGEstrtold、wcstold0或者±HUGE_VALLERANGEstrtoimax、wcstoimaxIMAX_MIN或INTMAX_MAXERANGE以字符串转成长整型函数strtol为例 在64位机器下long长度为8字节最大值LONG_MAX 为 0x7fffffffffffffff当变量longStr 取超出长整型最大值的字符串”0xffffffffffffffff”和刚好等于最大值的字符串”0x7fffffffffffffff”时函数的返回值都为相同的LONG_MAX。此时金聪返回值是无法判断函数的执行的成功与否。这个时要判断errno的值。如下例中会打印出错误的信息。errno0;//LONG_MAX的最大值为0x7fffffffffffffffconstchar*longStr0xffffffffffffffff;long retstrtol(longStr,NULL,16);if(retLONG_MAX){if(errno!0){printf(error : %d \n,errno);printf(错误信息 : %s \n,strerror(errno));}else{printf(等于long的最大值\n);}}复制2.函数返回值可知错误errno可知更详细的错误函数返回值errno值ftell()-1Lpositivefgetpos()、fsetpos()nonzeropositivembrtowc()、mbsrtowcs()(size_t)(-1)EILSEQsignal()SIG_ERRpositivewcrtomb()、wcsrtombs(size_t)(-1)EILSEQmbrtoc16()、mbrtoc32()(size_t)(-1)EILSEQc16rtomb()、cr21rtomb(size_t)(-1)EILSEQ3.有不同标准文档的库函数有些函数在不同的标准下对errno有不同的定义例如fopen中便是一个例子。C99并没有对使用fopen是对errno做要求但POSIX.1却声明了错误时返回NULL并将错误码写入errno。避免使用goto语句goto语句有很多优点例如goto语句可以非常方便的在局部作用域中跳出多层循环执行如无条件的跳转。 但正因为goto语句可以灵活的跳转如果不加以限制它会破坏程序的结构化风格使得代码难以理解与测试同时不加限制的使用goto语句可能跳过变量的初始化、重要的计算等语句。以下例子在a小于0或者a小于等于100时会使用goto跳转到标记为Error的语句中。注意goto只能在局部作用域中跳转。voidtestGoto(int a){if(a0){if(a100){printf( a %d \n,a);}else{goto Error;}}else{goto Error;}Error:printf(Test Error a %d \n,a);}复制避免使用setjmp与longjmp相比与goto语句只能在局部作用域中跳转setjump与longjmp可以进行跨作用域跳转也就是跨函数跳转。我们知道函数调用都以函数栈的形式进行调用与退出既然要做到跨函数跳转那便需要对当前的函数栈进行保存与还原而setjmp的作用便是保存当前函数栈至类型jmp_buf结构体变量中而longjmp的作用便是从此结构体中恢复还原函数栈。而相对于goto仅在作用域内跳转setjmp和longjmp则使代码更加的难以维护以及可读。小结C语言中使用函数的返回值来标志函数是否执行成功默认成功返回1失败返回0当使用接口时必须对函数进行正确性的验证检查它的返回值并且对每个错误的返回值进行相应的处理以及提示。同样的道理如果作为接口的开发方需要对函数的各种情况反映到返回值中。编写代码是无论使用什么样的错误处理方式发现程序中错误最好的方法便是执行程序让数据在函数中流动在判断逻辑中查找到函数出错的地方。三类断言运行期间断言在C中标准在 或vassert.h头文件中为程序员提供了 assert宏用于在运行时进行断言。前面错误示例改成assert宏实现就正确了。与静态断言使用的 static_assert 不同assert 并不支持自定义错误消息。// #define NDEBUG#includecassert#includeiostreamtemplatetypenameTT*array_alloc(unsignedintsize){assert(size0);returnnewT[size];}intmain(){std::coutmain startstd::endl;int*aharray_allocint(0);// 编译通过但运行时候报错std::coutmain endstd::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.h 语句出现时编译器会忽略对 assert 宏函数调用代码的编译。// 反之它便会在程序运行时进行正常的断言检查。通过这种方式我们可以相对灵活地控制运行时断言的启用与关闭。// 事实上assert宏在cassert中的实现方式类似需要注意的是C 程序中的运行时断言是否可用也会受到宏常量 NDEBUG 的影响。当该宏常量的定义先于 #include assert.h 语句出现时编译器会忽略对 assert 宏函数调用代码的编译。反之它便会在程序运行时进行正常的断言检查。通过这种方式我们可以相对灵活地控制运行时断言的启用与关闭。事实上assert宏在中的实现方式类似#ifdefNDEBUG#defineassert(expr)(static_castvoid(0))#else//其他#endif类似编译期间断言在C11标准中引入了 static_assert断言来解决这个问题。static_assert使用起来非常简单它接收两个参数一个是断言表达式这个表达式通常需要返回一个bool值一个则是警告信息它通常也就是一段字符串。#includecassert#includestring#includecstdio#includeiostreamusingnamespacestd;templatetypenameT,typenameUvoidbit_copy(Ta,Ub){static_assert(sizeof(a)sizeof(b),a and b must be equal);memcpy(a,b,sizeof(a));}intmain(){inta10;intb30;bit_copy(a,b);coutokendl;}处理的表达式必须在编译期间就能知道结果。另外必须注意的是static_assert的断言表达式的结果必须是在编译时期可以计算的表达式即必须是常量表达式。如果读者使用了变量则会导致错误如intbit_deal(inta)(static_assert(a1,the parameters of a should 1.););​ 上面使用了参数变量a 因而static_assert无法通过编 译。如果需要对变量进行检查就要是实现运行时的检查使用assert宏了。该关键字的用法类似C11标准之前开源库 Boost内置的BOOST_STATIC_ASSERT断言机制类似利用1/(e)这个表达式来判定#includecassert#includestring#includecstdio#includeiostreamusingnamespacestd;#definecp_assert(e)\do\{\enum\{\assert_static1/(e)\};\\\}while(0)templatetypenameT,typenameUvoidbit_copy(Ta,Ub){cp_assert(sizeof(a)sizeof(b));memcpy(a,b,sizeof(a));}intmain(){inta10;intb30;bit_copy(a,b);coutokendl;}​ 在通常情况下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的配合可以让程序员在预处理阶段进行断言。例如我们可以在程序中判断某个自定义宏是否存储而进行断言提示#ifndefMY_OPT#error “MY_OPTnot define in code, include mydef.h instead.”#endif如果程序中没有包含需要的头文件mydef.h并进行编译该头文件内的宏_MY_OPT_就无法匹配到而引发错误。#error指令会将后面的语句输出从而提醒用户要使用这个头文件这样一来通过预处理时的断言发布者就可以避免一些头文件的引用问题。类似的还有#warning 这种用于生成编译警告消息预处理宏。样例main.cpp#includeprimary.hintmain(){say_hi();return0;}primary.h#ifndef_PRIMARY_H#define_PRIMARY_H#endif#includesecondary.h// 模拟 primary 依赖secondary 头文件的内容// ......... 包含其他处理的代码//. .........secondary.h#ifndef_PRIMARY_H#errorNever use secondary.h directly.Include primary.h instead.#endif#includecstdiovoidsay_hi(){printf(say hi\n);}如果再main 中只包含 secondary.h 而没有 包含 primary.h#includesecondary.hintmain(){say_hi();return0;}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. 编译期断言在C11之前可以使用除0操作来实现传入编译期可判定的表达式则在编译期可计算出是true还是false即0然后除0可在编译期报告错误\6. 使用上一条中的方式拿到的是除0错误语义不明朗在C11中编译期断言可直接用static_assert关键字实现传入待判定的编译期可解的表达式以及断言失败时上报的错误信息 # 总结 - \1. 断言assertion分类运行时、预编译期、编译期即断言的判断触发时机 - \2. 运行时断言使用assert宏来实现 - \3. 一旦定义了NDEBUG宏那么assert宏将被展开成一条空语句不起作用可能会被编译器优化掉原意是在非Debug编译下不把运行时assert编进来 - \4. 预编译时期的断言可以使用预处理命令#ifdef或#ifundef配合#error实现可以在预编译时期对一些宏进行检查以确定编译参数、头文件引用状态等 - \5. 编译期断言在C11之前可以使用除0操作来实现传入编译期可判定的表达式则在编译期可计算出是true还是false即0然后除0可在编译期报告错误 - \6. 使用上一条中的方式拿到的是除0错误语义不明朗在C11中编译期断言可直接用static_assert关键字实现传入待判定的编译期可解的表达式以及断言失败时上报的错误信息

相关新闻