防禦式程式設計之斷言assert的使用

Sharemaker 發表於 2022-12-06

防禦式程式設計的重點就是需要防禦一些程式未曾預料的錯誤,這是一種提高軟體質量的輔助性方法,斷言assert就用於防禦式程式設計,編寫程式碼時,我們總是會做出一些假設,斷言就是用於在程式碼中捕捉這些假設。使用斷言是為了驗證預期的結果——當程式執行到斷言的位置時,對應的斷言應該為真;若斷言不為真時,程式會終止執行,並給出錯誤資訊。可以在任何時候啟用和禁用斷言驗證,因此可以在程式除錯時啟用斷言而在程式釋出時禁用斷言。同樣,程式投入執行後,終端使用者在遇到問題時可以重新啟用斷言。

1、原型函式

  在大部分編譯器下,assert() 是一個宏;在少數的編譯器下,assert() 就是一個函式。我們不需要關心這些差異,可以只把 assert()當作函式使用即可。即:

1 void assert(int expression);

   在程式執行時它會計算括號內的表示式,如果 expression為非0說明其值為真,assert()不執行任何動作,程式繼續執行後面的語句;如果 expression為0說明其值為假,assert()將會報告錯誤,並終止程式的執行,值得了解的是,程式終止是呼叫abort()函式,這個函式功能就是終止程式執行,直接從呼叫的地方跳出,abort()函式也是標準庫函式,在<stdlib.h>中定義。因此assert()用來判斷程式中是否出現了明顯非法的邏輯,如果出現了就終止程式以免導致嚴重後果,同時也便於查詢錯誤。

2、詳細釋義

  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時:

  當定義了NDEBUG之後,assert()執行的具體函式就變成了 ((void)0),這表示啥也不幹了,宏裡面這樣用的目的是防止該宏被用作右值,因為void型別不能用作右值。所以當在標頭檔案中定義了NDEBUG之後,assert()的檢測功能就自動失效了。

  • 未定義NDEBUG時:

  可以看到assert()執行實際上是透過三目運算子來判斷表示式e的真假,執行相應的處理。當表示式e為真時,執行(void)0,即什麼也不執行,程式繼續執行;當表示式e為假時,那麼它會列印出來assert的內容、當前的檔名、當前行號,接著終止程式執行。

3、用法舉例

  在未定義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。

4、使用注意事項

  使用assert的核心原則是:用於處理絕不應該發生的情況,這就是為什麼應該在程式Debug版本中使用,這是為了將主觀上不應該發生的錯誤在程式Debug版本中就應該解決掉,從而在程式Release版本時不會產生這種不應該發生的型別的錯誤。

  • 和if的區別

  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 }

 


更多技術內容和書籍資料獲取敬請關注微信公眾號“明解嵌入式”

防禦式程式設計之斷言assert的使用

相關文章