寫在前面:
本文章旨在總結備份、方便以後查詢,由於是個人總結,如有不對,歡迎指正;另外,內容大部分來自網絡、書籍、和各類手冊,如若侵權請告知,馬上刪帖致歉。
對於斷言,相信大家都不陌生,大多數編程語言也都有斷言這一特性,應用場景也很廣泛,包括我們在 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程序