在C語言程序開發中,程式設計師寫代碼時應該考慮的「面面俱到」,這樣才能寫出功能穩定的程序。例如,在實現 open() 函數時,先完成它的功能固然是重要的,但是程式設計師還需要考慮各種「意外」,比如下面這種情況。
假設不存在 /dev/sth 這個文件,仍然調用 open() 函數打開它:
int fd = open("/dev/sth", O_RDONLY);此時 open() 函數不應該感到迷惑,而是具備處理這種「意外」的能力。標準庫的 open() 函數在遇到這種情況時,會返回一個錯誤碼,對應著「文件不存在」的錯誤信息。
所以我們在開發C語言程序的過程中,寫出的代碼也應具備這種處理「意外」的能力。處理「意外」最常用的方式之一就是返回一個錯誤碼,輸出一段錯誤提示信息,這一點其實之前的文章討論過。
使用 assert
在C語言程序開發階段,為了方便,我們可以在可能出現不預期的「意外」處使用 assert()。assert() 的C語言原型如下:
#include <assert.h>void assert(scalar expression);
使用它需要包含 assert.h,assert() 接收一個參數 expression,可以是一個表達式,如果 expression 為真,則什麼都不會發生。如果 expression 為假,則 assert() 會終止C語言程序,並且輸出 assert 失敗的代碼位置。
例如下面這段C語言代碼:
int fd = open("/dev/sth", O_RDONLY);assert(fd > 0);printf("fd = %d\n", fd);
編譯並執行,得到如下結果:
# gcc t.c# ./a.out a.out: t.c:11: main: Assertion `fd > 0' failed.Aborted可以看出,第 12 行的 printf() 函數並沒有被執行。這是因為程序運行環境裡並沒有 「/dev/sth」 這個文件,所以 open() 函數執行失敗,傳遞給 assert() 的參數為假,C語言程序被終止,並且輸出 t.c 源文件第 11 行代碼 assert 失敗。
assert() 可以輸出出錯的代碼位置,這個特性在較為大型的C語言程序開發中是非常好用的,因為無需程式設計師再去手工調試代碼,排查出錯代碼的位置了。
不過,assert() 在遇到假參數時,直接將C語言程序終止太過於死板。比如某個C語言程序有兩套邏輯,第一套邏輯在 open() 函數成功打開文件時運行,第二套邏輯則在 open() 函數打開文件失敗時運行。要是使用 assert() 判斷 open() 函數是否成功打開文件,則第二套邏輯永遠沒有機會運行。
所以,assert() 一般僅用於開發階段幫助程式設計師定位錯誤,不能依賴 assert() 處理「意外」。事實上,為了便於使用,在定義了 NDEBUG 宏之後,assert() 就不再生成代碼了,此時 assert() 相當於一個空格。請看下面這段C語言代碼:
#include <stdio.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#define NDEBUG#include <assert.h>int main(){int fd = open("/dev/sth", O_RDONLY); assert(fd > 0);printf("fd = %d\n", fd);return0; }
編譯上述C語言代碼並執行,得到如下輸出:
# gcc t.c# ./a.out fd = -1編譯時 assert
可以看出,assert() 用於處理C語言程序可能出現諸多預期之外的「意外」時很有用,它能夠自己輸出究竟哪一個「意外」發生。但是 assert() 也是死板的,它在遇到假條件時直接把程序終止,剩餘的代碼邏輯不再有機會執行。
另外還有一點要說明,assert() 本身也會影響C語言程序的運行效率,這也是它常常只被使用在開發階段的另一個原因。
其實仔細想想,使用 assert() 的目的其實只是希望它能夠在C語言程序遇到不預期的「意外」時提醒程式設計師,我們並不關心 assert() 是否參與程序運行。如果使用 assert() 判斷的是常量表達式,那我們可以自己定義一個 static_assert() 宏,並且讓它在編譯時就判斷條件表達式是否成立,這樣的宏可能在某些場合更加好用。
那該如何實現編譯時 assert 這個功能呢?
其實很簡單,首先應該明白數組的長度不可能是負數,基於這一點,static_assert() 宏就容易實現了,請看下面的C語言代碼:
#define static_assert(expr) \do{ char tmp[(expr)?1:-1]; }while(0)如果條件表達式為真,則 static_assert() 宏會定義一個長度為 1 的數組,否則就會嘗試定一個長度為 -1 的數組,此時必定無法編譯通過。這裡值得一提的一個小技巧是使用 {} 符號將定義的 tmp 數組的作用域限定在本次調用的 static_assert 宏裡,避免多次調用 static_assert 時出現重複定義。
寫出如下C語言代碼測試之:
int main() { static_assert(2>1);printf("assert 2>1\n");static_assert(2<1);printf("assert 2<1\n");return0; }
編譯這段C語言代碼,得到如下輸出:
顯然,static_assert() 宏在編譯階段就將假條件表達式找出來了。可能有些讀者會覺得如果 assert 成功,就會定義一個 tmp 數組,雖然它的長度很短,但是仍然浪費了棧空間。其實這裡可以把長度為零的數組,即:
#define static_assert(expr) \do{ char tmp[(expr)?0:-1]; }while(0)在 assert 成功時會執行 char tmp[0];,它的長度為 0,感興趣的讀者可以使用 sizeof() 測試一下。到這裡,我們就較為粗略的定義好了 static_assert 宏,它在編譯階段就能發現假條件。
小結
本節主要介紹了 assert() 的使用,應該能夠發現,在開發階段,它能夠幫助程式設計師快速的定位「意外」,也討論了 assert() 的不足之處,並在此基礎上自己定義了「編譯時」的static_assert 宏。按照這樣的思路,其實還有很多定義 static_assert() 宏的其他方法,具體哪些方法留給讀者自己思考了。