一、前言
我們在擼程式碼的時候,經常需要對程式碼的安全性進行檢查,例如:
- 指標是否為空?
- 被除數是否為 0?
- 函式呼叫的返回結果是否有效?
- 開啟一個檔案是否成功?
對這一類的邊界條件進行檢查的手段,一般都是使用 if 或者 assert 斷言,無論使用哪一個,都可以達到檢查的目的。那麼是否就意味著:這兩者可以隨便使用,想起來哪個就用哪個?
這篇小短文我們就來掰扯掰扯:在不同的場景下,到底是應該用 if,還是應該使用 assert 斷言?
寫這篇文章的時候,我想起了孔乙己老先生的那個問題:茴香豆的“茴”字有幾種寫法?
似乎我們沒有必要來糾結應該怎麼選擇,因為都能夠實現想要的功能。以前我也是這麼想的,但是,現在我不這麼認為。
成為技術大牛、拿到更好的offer,也許就在這些細微之間就分出了勝負。
二、assert 斷言
剛才,我問了下旁邊的一位工作 5 年多的嵌入式開發者:if 和 assert 如何選擇?他說:assert 是幹什麼的?!
看來,有必要先簡單說一下 assert 斷言。
assert() 的原型是:
void assert(int expression);
- 如果巨集的引數求值結果為非零值,則不做任何操作(no action);
- 如果巨集的引數是零值,就列印診斷訊息,然後呼叫abort()。
例如下面的程式碼:
#include <assert.h>
int my_div(int a, int b)
{
assert(0 != b);
return a / b;
}
- 當 b 不為 0 時,assert 斷言什麼都不做,程式往下執行;
- 當 b 為 0 時,assert 斷言就列印錯誤資訊,然後終止程式;
從功能上來說,assert(0 != b);
與下面的程式碼等價:
if (0 == b)
{
fprintf(stderr, "b is zero...");
abort();
}
assert 是一個巨集,不是一個函式
在 assert.h 標頭檔案中,有如下定義:
#ifdef NDEBUG
#define assert(condition) ((void)0)
#else
#define assert(condition) /*implementation defined*/
#endif
既然是巨集定義,說明是在預處理的時候進行巨集替換。(關於巨集的更多內容,可以看一下這篇文章:提高程式碼逼格的利器:巨集定義-從入門到放棄)。
從上面的定義中可以看到:
- 如果定義了巨集 NDEBUG,那麼 assert() 巨集將不做什麼動作,也就是相當於一條空語句:
(void)0;
,當在 release 階段編譯程式碼的時候,都會在編譯選項中(Makefile)定義這個巨集。- 如果沒有定義巨集 NDEBUG,那麼 assert() 巨集將會把一些檢查程式碼進行替換,我們在開發階段執行 debug 模式編譯時,一般都會遮蔽掉這 NDEBUG 這個巨集。
三、if VS assert
還是以一個程式碼片段來描述問題,以場景化來討論比較容易理解。
// brief: 把兩個短字串拼接成一個字串
char *my_concat(char *str1, char *str2)
{
int len1 = strlen(str1);
int len2 = strlen(str2);
int len3 = len1 + len2;
char *new_str = (char *)malloc(len3 + 1);
memset(new_str, 0 len3 + 1);
sprintf(new_str, "%s%s", str1, str2);
return new_str;
}
如果一個開發人員寫出上面的程式碼,一定會被領導約談的!它存在下面這些問題:
- 沒有對輸入引數進行有效性檢查;
- 沒有對 malloc 的結果進行檢查;
- sprintf 的效率很低;
- ...
1. 使用 if 語句來檢查
char *my_concat(char *str1, char *str2)
{
if (!str1 || !str2) // 引數錯誤
return NULL;
int len1 = strlen(str1);
int len2 = strlen(str2);
int len3 = len1 + len2;
char *new_str = (char *)malloc(len3 + 1);
if (!new_str) // 申請堆空間失敗
return NULL;
memset(new_str, 0 len3 + 1);
sprintf(new_str, "%s%s", str1, str2);
return new_str;
}
2. 使用 assert 斷言來檢查
char *my_concat(char *str1, char *str2)
{
// 確保引數正確
assert(NULL != str1);
assert(NULL != str2);
int len1 = strlen(str1);
int len2 = strlen(str2);
int len3 = len1 + len2;
char *new_str = (char *)malloc(len3 + 1);
// 確保申請堆空間成功
assert(NULL != new_str);
memset(new_str, 0 len3 + 1);
sprintf(new_str, "%s%s", str1, str2);
return new_str;
}
3. 你喜歡哪一個?
首先宣告一點:以上這 2 種檢查方式,在實際的程式碼中都很常見,從功能上來說似乎也沒有什麼影響。因此,沒有嚴格的錯與對之分,很多都是依賴於每個人的偏好習慣不同而已。
(1) assert 支持者
我作為 my_concat()
函式的實現者,目的是拼接字串,那麼傳入的引數必須是合法有效的,呼叫者需要負責這件事。如果傳入的引數無效,我會表示十分的驚訝!怎麼辦:崩潰給你看!
(2)if 支持者
我寫的 my_concat()
函式十分的健壯,我就預料到呼叫者會亂搞,故意的傳入一些無效引數,來測試我的編碼水平。沒事,來吧,我可以處理任何情況!
這兩個派別的理由似乎都很充足!那究竟該如何選擇?難道真的的跟著感覺走嗎?
假設我們嚴格按照常規的流程去開發一個專案:
- 在開發階段,編譯選項中不定義 NDEBUG 這個巨集,那麼 assert 就發揮作用;
- 專案釋出時,編譯選項中定義了 NDEBUG 換個巨集,那麼 assert 就相當於空語句;
也就是說,只有在 debug 開發階段,用 assert 斷言才能夠正確的檢查到引數無效。而到了 release 階段,assert 不起作用,如果呼叫者傳遞了無效引數,那麼程式只有崩潰的命運了。
這說明什麼問題?是程式碼中存在 bug?還是程式碼寫的不夠健壯?
從我個人的理解上看,這壓根就是單元測試沒有寫好,沒有測出來引數無效的這個 case!
4. assert 的本質
assert 就是為了驗證有效性,它最大作用就是:在開發階段,讓我們的程式儘可能地 crash。每一次的 crash,都意味著程式碼中存在著 bug,需要我們去修正。
當我們寫下一個 assert 斷言的時候,就說明:斷言失敗的這種情況是不可以的,是不被允許的。必須保證斷言成功,程式才能繼續往下執行。
5. if-else 的本質
if-else 語句用於邏輯處理,它是為了處理各種可能出現的情況。就是說:每一個分支都是合理的,是允許出現的,我們都要對這些分支進行處理。
6. 我喜歡的版本
char *my_concat(char *str1, char *str2)
{
// 引數必須有效
assert(NULL != str1);
assert(NULL != str2);
int len1 = strlen(str1);
int len2 = strlen(str2);
int len3 = len1 + len2;
char *new_str = (char *)malloc(len3 + 1);
// 申請堆空間失敗的情況,是可能的,是允許出現的情況。
if (!new_str)
return NULL;
memset(new_str, 0 len3 + 1);
sprintf(new_str, "%s%s", str1, str2);
return new_str;
}
對於引數而言:我認為傳入的引數必須是有效的,如果出現了無效引數,說明程式碼中存在 bug,不允許出現這樣的情況,必須解決掉。
對於資源分配結果(malloc 函式)而言:我認為資源分配失敗是合理的,是有可能的,是允許出現的,而且我也對這個情況進行了處理。
當然了,並不是說對引數檢查就要使用 assert,主要是根據不同的場景、語義來判斷。例如下面的這個例子:
int g_state;
void get_error_str(bool flag)
{
if (TRUE == flag)
{
g_state = 1;
assert(1 == g_state); // 確保賦值成功
}
else
{
g_state = 0;
assert(0 == g_state); // 確保賦值成功
}
}
flag 引數代表不同的分支情況,而賦值給 g_state 之後,必須保證賦值結果的正確性,因此使用 assert 斷言。
五、總結
這篇文章分析了 C 語言中比較晦澀、模糊的一個概念,似乎有點虛無縹緲,但是的確又需要我們停下來仔細考慮一下。
如果有些場景,實在拿捏不好,我就會問自己一個問題:
這種情況是否被允許出現?
不允許:就用 assert 斷言,在開發階段就儘量找出所有的錯誤情況;
允許:就用 if-else,說明這是一個合理的邏輯,需要進行下一步處理。
不吹噓,不炒作,不浮誇,認真寫好每一篇文章!
歡迎轉發、分享給身邊的技術朋友,道哥在此表示衷心的感謝! 轉發的推薦語已經幫您想好了:
道哥總結的這篇總結文章,寫得很用心,對我的技術提升很有幫助。好東西,要分享!
推薦閱讀
C語言指標-從底層原理到花式技巧,用圖文和程式碼幫你講解透徹
一步步分析-如何用C實現物件導向程式設計
我最喜歡的程式之間通訊方式-訊息匯流排
物聯網閘道器開發:基於MQTT訊息匯流排的設計過程(上)
提高程式碼逼格的利器:巨集定義-從入門到放棄
原來gdb的底層除錯原理這麼簡單
利用C語言中的setjmp和longjmp,來實現異常捕獲和協程
關於加密、證書的那些事
深入LUA指令碼語言,讓你徹底明白除錯原理