写在前面:
本文章旨在总结备份、方便以后查询,由于是个人总结,如有不对,欢迎指正;另外,内容大部分来自网络、书籍、和各类手册,如若侵权请告知,马上删帖致歉。
对于断言,相信大家都不陌生,大多数编程语言也都有断言这一特性,应用场景也很广泛,包括我们在 STM8、STM32中都能发现;那我们就来聊一下 assert断言
一、C 标准库的 assert.h头文件
在 C语言中,C 标准库的 assert.h头文件提供了一个名为 assert 的宏,它可用于验证程序做出的假设,并在假设为假时输出诊断消息;一般情况下,assert 宏只用在 Debug 版本(内部调试版本)中,而在 Release 版本(发行版本)中应该被忽略
如果引用了 <assert.h>头文件,则宏 NDEBUG可以把所使用的 assert断言判定取消掉;实际上就是把宏 assert修改了,代码如下:
#ifdef NDEBUG
#define assert(_Expression) ((void)0)
#else /* !defined (NDEBUG) */
/* 当未 define NDEBUG时所要执行的代码 */
......
#endif /* !defined (NDEBUG) */
二、一些用法上的注意事项
1、尽可能的每个 assert只检验一个条件,因为若是同时检验多个条件的话,那么如果断言失败,将无法直观的判断是哪个条件失败
2、在函数中使用断言来检查参数的合法性是断言最主要的应用场景之一,它主要体现在如下 3 个方面:
- 在代码执行之前或者在函数的入口处,使用断言来检查参数的合法性,这称为前置条件断言。
- 在代码执行之后或者在函数的出口处,使用断言来检查参数是否被正确地执行,这称为后置条件断言。
- 在代码执行前后或者在函数的入出口处,使用断言来检查参数是否发生了变化,这称为前后不变断言。
3、避免在断言表达式中使用改变环境的语句
eg:
/* 错误的 */
int test(char i)
{
assert(i++);
return i;
}
/* 正确的 */
int test(char i)
{
assert(i);
i++;
return i;
}
因为如果是在 Debug 版本中,假设我们传入的值为 1,那么在执行“assert(i++)”语句的时候虽然通过条件检查,进而继续执行 “i++”,最后输出的结果值为 2;但当我们在 Release 版本中,函数中的断言语句 “assert(i++)”将被忽略掉,这样表达式 “i++” 将得不到执行,从而导致输出的结果值还是 1
4、避免利用断言来作检查程序错误
在对断言的使用中,一定要遵循这样一条规定:对来自系统内部的可靠的数据使用断言,对于外部不可靠数据不能够使用断言,而应该使用错误处理代码。换句话说,断言是用来处理不应该发生的非法情况,而对于可能会发生且必须处理的情况应该使用错误处理代码,而不是断言。总之就是:断言是用来检查非法情况的,而不是测试和处理错误的;因此,不要混淆非法情况与错误情况之间的区别,后者是必然存在且一定要处理的
三、ST库中的 assert_param语句
在 ST库的应用中,亦或者说是自定义断言,我们都可以学着怎么去定义一种适合自己的断言输出,ST库是个很好的选择,他的框架很简单,下面以 stm32为例:
/* Uncomment the line below to expanse the "assert_param" macro in the
Standard Peripheral Library drivers code */
/* #define USE_FULL_ASSERT 1 */
/* Exported macro ------------------------------------------------------------*/
#ifdef USE_FULL_ASSERT
/**
* @brief The assert_param macro is used for function's parameters check.
* @param expr: If expr is false, it calls assert_failed function which reports
* the name of the source file and the source line number of the call
* that failed. If expr is true, it returns no value.
* @retval None
*/
#define assert_param(expr) ((expr) ? (void)0 : assert_failed((uint8_t *)__FILE__, __LINE__))
/* Exported functions ------------------------------------------------------- */
void assert_failed(uint8_t* file, uint32_t line);
#else
#define assert_param(expr) ((void)0)
#endif /* USE_FULL_ASSERT */
它的断言宏 assert_param是定义在 stm32fxxx_conf.h中,原理很简单,看上面代码;在默认情况下是使用 Release版本的,若是用 Debug版本则通过宏 assert_param(相当于 C 标准库的宏 assert)来进行对传入的信息进行判定(前提是定义了宏 USE_FULL_ASSERT),其实看注释就可以知道怎么开启宏 USE_FULL_ASSERT了。先说一下 Release版本,其实跟 C 标准库的一样,用 (void) 0来去除断言;而 Debug版本则是利用了 “?”运算符(三元运算符)来进行处理,然后当假设为假时调用 assert_failed函数,这个函数官方也有提供,代码如下:
#ifdef USE_FULL_ASSERT
/**
* @brief Reports the name of the source file and the source line number
* where the assert_param error has occurred.
* @param file: pointer to the source file name
* @param line: assert_param error line source number
* @retval None
*/
void assert_failed(uint8_t* file, uint32_t line)
{
/* User can add his own implementation to report the file name and line number,
ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
/* Infinite loop */
while (1)
{
}
}
#endif
看注释,我们可以利用 printf("Wrong parameters value: file %s on line %d\r\n", file, line);来输出信息,以达到直观的判断是哪个条件失败
最后说一下 __FILE__和 __LINE__这两个 ANSIC特殊标准定义:
- __LINE__ :正在编译文件的行号
- __FILE__ :正在编译文件的文件名
拓展:
- __DATE__:编译时刻的日期字符串
- __TIME__:编译时刻的时间字符串
- __STDC__:判断该文件是不是标准 C程序