一、什麼是assert()?
編寫代碼時,我們總是會做出一些假設,斷言(assert)就是用于在代碼中捕捉這些假設,可以将斷言看作是異常處理的一種進階形式。
斷言表示為一些布爾表達式,程式員相信在程式中的某個特定點該表達式值為真。可以在任何時候啟用和禁用斷言驗證,是以可以在測試時啟用斷言,而在部署時禁用斷言。同樣,程式投入運作後,最終使用者在遇到問題時可以重新啟用斷言。
注意assert()是一個宏,而不是函數。
二、assert怎麼用?
1、assert所在的頭檔案及原型
在MinGW工具中,assert()宏在存在于頭檔案assert.h中,其關鍵内容如下:
#ifdef NDEBUG#define assert(x)((void)0)#else /* debugging enabled */_CRTIMP void __cdecl __MINGW_NOTHROW _assert (const char*, const char*, int) __MINGW_ATTRIB_NORETURN;#define assert(e) ((e) ? (void)0 : _assert(#e, __FILE__, __LINE__))#endif/* NDEBUG */
assert()宏接受一個整形表達式參數。如果表達式的值為假,assert()宏就會調用_assert函數在标準錯誤流中列印一條錯誤資訊,并調用abort()(abort()函數的原型在stdlib.h頭檔案中)函數終止程式。
當我們認為已經排除了程式的bug時,就可以把宏定義#define NDEBUG寫在包含assert.h位置前面。
小知識:
- __cdecl是C Declaration的縮寫(declaration,聲明),表示C語言預設的函數調用方法:所有參數從右到左依次入棧。
- _CRTIMP是C run time implement的簡寫,C運作庫的實作的意思。作為使用者代碼,不應該使用這個東西。提示是使用dll的動态 C 運作時庫還是靜态連接配接的 C 運作庫的一個宏。
#ifndef _CRTIMP#ifdef _DLL#define _CRTIMP __declspec(dllimport)#else /* ndef _DLL */#define _CRTIMP#endif /* _DLL */#endif /* _CRTIMP */
- __MINGW_NOTHROW與__MINGW_ATTRIB_NORETURN是異常處理相關辨別
這幾個辨別符在C語言标準庫檔案中都有用得到,但是我們不需要關心,在我們使用者的角度來看,以上函數原型我們看成:void _assert(const char*, const char*, int);即可。
2、assert應用
assert主要用于類型檢查及單元測試中。
單元測試(unit testing),是指對軟體中的最小可測試單元進行檢查和驗證。對于單元測試中單元的含義,一般來說,要根據實際情況去判定其具體含義,如C語言中單元指一個函數。
(1)例子一:除法運算
/*編譯工具:mingw32 gcc6.3.0*/#include #include int main(void){int a, b, c;printf("請輸入b, c的值:");scanf("%d %d", &b, &c);a = b / c;printf("a = %d", a);return 0;}
此處,變量c作為分母是不能等于0,如果我們輸入2 0,結果是什麼呢?結果是程式會蹦:

這個例子中隻有幾行代碼,我們很快就可以找到程式蹦的原因就是變量c的值為0。但是,如果代碼量很大,我們還能這麼快的找到問題點嗎?
這時候,assert()就派上用場了,以上代碼中,我們可以在a = b / c;這句代碼之前加上assert(c);這句代碼用來判斷變量c的有效性。此時,再編譯運作,得到的結果為:
可見,程式蹦的同時還會在标準錯誤流中列印一條錯誤資訊:
Assertion failed:c, file hello.c, line 12
這條資訊包含了一些對我們查找bug很有幫助的資訊:問題出在變量c,在hello.c檔案的第12行。這麼一來,我們就可以迅速的定位到問題點了。
這時候細心的朋友會發現,上邊我們對assert()的介紹中,有這麼一句說明:如果表達式的值為假,assert()宏就會調用_assert函數在标準錯誤流中列印一條錯誤資訊,并調用abort()(abort()函數的原型在stdlib.h頭檔案中)函數終止程式。
是以,針對我們這個例子,我們的assert()宏我們也可以用以下代碼來代替:
if (0 == c){puts("c的值不能為0,請重新輸入!");abort();}
這樣,也可以給我們起到提示的作用:
但是,使用assert()至少有幾個好處:
1)能自動辨別檔案和出問題的行号。
2)無需要更改代碼就能開啟或關閉assert機制(開不開啟關系到程式大小的問題)。如果認為已經排除了程式的bug,就可以把下面的宏定義寫在包含assert.h的位置的前面:
#define NDEBUG
并重新編譯程式,這樣編輯器就會禁用工程檔案中所有的assert()語句。如果程式又出現問題,可以移除這條#define指令(或把它注釋掉),然後重新編譯程式,這樣就可以重新啟用了assert()語句。
(2)例子二:STM32庫函數
我們來看我們比較熟悉的GPIO初始化函數:
可見,該函數的實作中,有三條assert_param()這樣的語句,其作用就是對一些函數入口參數進行一些有效性檢查。其實assert_param()這就類似與我們C标準庫中的assert()。針對stm32f10x系列來說,其被定義在檔案stm32f10x_conf.h中:
這是一個例子,除了GPIO初始化函數之外,STM32固件庫函數中的其他函數都是會做這樣的參數檢查。
三、assert與if的比較?
assert()斷言功能好像用if也能實作,仔細一看這兩者還是有差別。下面看一下它們的差別:
先看一個例子,我們使用malloc函數定義一個存着堆空間中的變量,我們該怎麼定義及該怎麼做一些防禦處理呢?
首先,我們要知道,malloc函數如果配置設定成功記憶體則傳回指向被配置設定記憶體的指針(此存儲區中的初始值不确定),否則傳回空指針NULL。看如下代碼:
int* p = (int*)malloc(sizeof(int));assert(p);/* 錯誤示例 */
這麼寫會有問題嗎?
看似沒問題,但是問題很大!我們的assert()會在我們調試完畢之後禁用掉,這麼一來以上代碼就相當于隻有下面這一句了:
int* p = (int*)malloc(sizeof(int));
此時,當我們的程式在跑的時候malloc申請不到記憶體空間了,也沒有做一些解決措施,可能就會産生緻命錯誤。
我們應該把以上代碼改寫為:
int* p = (int*)malloc(sizeof(int));if (NULL == p) /*請使用if來判斷,這是有必要的*/{ /* 做一些處理 */}
下面看一下assert與if做防錯處理的幾點用法差別:
1、assert語句用在debug版本的調試中;if(NULL!=p)是在release版本中檢驗指針的有效性;
2、assert一般用與檢查函數參數的合法性(有效性)而不是正确性,但是合法的程式并不見得是程式邏輯正确的程式,該用if做判斷處理的地方還是得做處理。
也就是assert在調試期間用來檢查一些不允許出現的情況是否有發生,一旦發生就表明我們的程式很可能有BUG,而if判斷的就是我們理所應當處理的各種情況,且這些情況如果發生并不代表程式發生BUG。
四、_Static_assert(C11标準)
assert()是在運作時進行檢查的,如果一份工程很大,編譯起來需要很長時間,一些情況在運作時檢查,效率就比較低了。
這時候_Static_assert()就派上用場了,這是C11标準中的一個特性,_Static_assert()在編譯時進行檢查,如果編譯時檢測到代碼裡的一些異常情況,就會導緻程式無法通過編譯。下面來看一個例子:
/*編譯環境:mingw32 gcc6.3.0編譯指令:gcc -std=c11 hello.c -o hello.exe*/#include #include /*CHAR_BIT是limits.h中的一個宏*/_Static_assert(CHAR_BIT == 16, "16-bit char falsely assumed");int main(void){printf("歡迎關注嵌入式大雜燴!檢視更多筆記");return 0;}
_Static_assert接受兩個參數,第一個參數是整型常量表達式,第二個參數是一個字元串。如果第一個表達式為0,編譯時就會輸出第二個參數的字元串,而且編譯不通過。
該程式編譯結果如下:
可見,編譯報錯了,并且列印提示了我們的問題所在點,列印出了我們_Static_assert第二個參數的字元串,這樣我們就可以很快地定位到導緻編譯錯誤的問題了。
以上就是關于assert()斷言宏的一些總結筆記,如有錯誤歡迎指出!