防禦式程式設計的重點就是需要防禦一些程式未曾預料的錯誤,這是一種提高軟體質量的輔助性方法,斷言assert就用於防禦式程式設計,編寫程式碼時,我們總是會做出一些假設,斷言就是用於在程式碼中捕捉這些假設。使用斷言是為了驗證預期的結果——當程式執行到斷言的位置時,對應的斷言應該為真;若斷言不為真時,程式會終止執行,並給出錯誤資訊。可以在任何時候啟用和禁用斷言驗證,因此可以在程式偵錯時啟用斷言而在程式釋出時禁用斷言。同樣,程式投入執行後,終端使用者在遇到問題時可以重新啟用斷言。
在大部分編譯器下,assert() 是一個宏;在少數的編譯器下,assert() 就是一個函數。我們不需要關心這些差異,可以只把 assert()當作函數使用即可。即:
1 void assert(int expression);
在程式執行時它會計算括號內的表示式,如果 expression為非0說明其值為真,assert()不執行任何動作,程式繼續執行後面的語句;如果 expression為0說明其值為假,assert()將會報告錯誤,並終止程式的執行,值得了解的是,程式終止是呼叫abort()函數,這個函數功能就是終止程式執行,直接從呼叫的地方跳出,abort()函數也是標準庫函數,在<stdlib.h>中定義。因此assert()用來判斷程式中是否出現了明顯非法的邏輯,如果出現了就終止程式以免導致嚴重後果,同時也便於查詢錯誤。
assert() 在c標準庫中的<assert.h>中被定義。下面就看下在assert.h中的定義:
1 #ifdef NDEBUG
2 #define assert(e) ((void)0)
3 #else
4 #define assert(e) ((void) ((e) ? ((void)0) : __assert (#e, __FILE__, __LINE__)))
5 #endif
可以看到在定義了NDEBUG時,assert()無效,只有在未定義NDEBUG時,assert()才實現具體的函數功能。NDEBUG是「No Debug」的意思,也即「非偵錯」。程式一般分為Debug版本和Release版本,Debug版本是程式設計師在測試程式碼期間使用的編譯版本,Release版本是將程式提供給使用者時使用的釋出版本,一般來說斷言assert()是僅在Debug版本起作用的宏。在釋出版本時,我們不應該再依賴assert()宏,因為程式一旦出錯,assert()會丟擲一段使用者看不懂的提示資訊,並毫無預警地終止程式執行,這樣會嚴重影響軟體的使用者體驗,所以在釋出模式下應該讓assert()失效,另外在程式中頻繁的呼叫assert()會影響程式的效能,增加額外的開銷。因此可以在<assert.h>中定義NDEBUG宏,將assert()功能關閉。
1 #define NDEBUG //定義NDEBUG
2 #ifdef NDEBUG
3 #define assert(e) ((void)0)
4 #else
5 #define assert(e) ((void) ((e) ? ((void)0) : __assert (#e, __FILE__, __LINE__)))
6 #endif
當定義了NDEBUG之後,assert()執行的具體函數就變成了 ((void)0),這表示啥也不幹了,宏裡面這樣用的目的是防止該宏被用作右值,因為void型別不能用作右值。所以當在標頭檔案中定義了NDEBUG之後,assert()的檢測功能就自動失效了。
可以看到assert()執行實際上是通過三目運運算元來判斷表示式e的真假,執行相應的處理。當表示式e為真時,執行(void)0,即什麼也不執行,程式繼續執行;當表示式e為假時,那麼它會列印出來assert的內容、當前的檔名、當前行號,接著終止程式執行。
在未定義NDEBUG時,assert()功能生效的情況下,來看一個簡單的assert()使用的例子:
1 #include <stdio.h>
2 #include <assert.h>
3 void main()
4 {
5 int i = 8;
6 assert(i > 0);
7 printf("i = %d\n", i);
8 i = -8;
9 assert(i > 0);
10 printf("i = %d\n", i);
11 }
可以看出在程式中使用assert(i > 0)來判斷;當 i > 0 時,assert的判斷表示式為真,assert不生效;當 i < 0 時,assert的判斷表示式為假,assert生效。
在程式第5行 i = 8,執行完assert後,程式將執行後續的printf列印出 i 的值;而在第8行 i = -8,執行完assert後,程式終止,不會執行後續的printf。
使用assert的核心原則是:用於處理絕不應該發生的情況,這就是為什麼應該在程式Debug版本中使用,這是為了將主觀上不應該發生的錯誤在程式Debug版本中就應該解決掉,從而在程式Release版本時不會產生這種不應該發生的型別的錯誤。
assert用函數來判斷是否滿足表示式條件後終止程式,在Debug版本中用assert來判斷程式的合法性,定位不允許發生的錯誤,那麼什麼是不應該發生的錯誤,例如像下面這種除0操作,主觀上就不應該發生,就是就要在Debug版本中檢查排除掉這種錯誤,以免影響後續程式的執行。
1 #include <stdio.h>
2 #include <assert.h>
3 void fun(int a, int b)
4 {
5 assert(b != 0);
6 int i = a / b;
7 }
if是一個關鍵字,一般用於根據條件來判斷邏輯的正確性,即是否根據條件對應執行,Debug和Release版本中都可以使用,例如下面用if的時候,就允許這些判斷條件是正常發生的,是合理的,需要根據發生的條件執行對應的邏輯,程式可以往下執行。
1 #include <stdio.h>
2 #include <assert.h>
3 void fun(int a, int b)
4 {
5 if(a > 0)
6 ...
7 else if(a < 0)
8 ...
9 else
10 ...
11 }
因此在使用前,可以先判斷下,如果邏輯不允許發生,那麼就使用assert在Debug階段將問題解決掉;如果邏輯允許的,那麼就使用if,當然也可以用if判斷後進行條件的return操作,來杜絕不允許邏輯,本質是防止錯誤的邏輯影響後續程式的執行。例如上述的用來判斷除0操作的例子也可以用if:
1 #include <stdio.h>
2 #include <assert.h>
3 void fun(int a, int b)
4 {
5 if(0 == b)
6 return;
7 int i = a / b;
8 }
一般assert可以用於判斷函數入參的合法性,比如入參值是否符合,指標是否為空:
1 #include <stdio.h>
2 #include <assert.h>
3 void fun1(int a)
4 {
5 assert(a > 0);
6 ...
7 }
8 void fun2(int *p)
9 {
10 assert(p != NULL);
11 ...
12 }
assert的判斷條件語句一定是確定的,在Debug版本中使用的排除掉錯誤的條件邏輯,不要影響到Release版本時的正常邏輯。例如下面的例子,在Debug版本時,i++到>=100時,assert生效,程式終止;但是到了Release版本,由於要增加NDEBUG宏,assert()無效。assert(i++ < 100)就變成了空操作(void)0;由於沒有i++語句執行,那麼while成了死迴圈。
1 #include <stdio.h>
2 #include <assert.h>
3 void main()
4 {
5 int i = 0;
6 while(i <= 110)
7 {
8 assert(i++ < 100);
9 printf("i = %d\n",i);
10 }
11 }
一般一個assert只用一個判斷語句來實現,如果在一個assert中使用多條判斷語句,當錯誤發生時,會不知道是哪個條件語句出現錯誤,錯誤表現的就不直觀。
1 #include <stdio.h>
2 #include <assert.h>
3 void fun1(int a, int b) //錯誤使用
4 {
5 assert(a > 0 && b > 5);
6 ...
7 }
8 void fun2(int a, int b) //正確使用
9 {
10 assert(a > 0);
11 assert(b > 5);
12 ...
13 }
更多技術內容和書籍資料獲取敬請關注微信公眾號「明解嵌入式」
本文來自部落格園,作者:Sharemaker,轉載請註明原文連結:https://www.cnblogs.com/Sharemaker/p/16954224.html