把複雜的任務拆分成簡單的子模塊
在C語言程序開發中,程式設計師常常會把一個複雜的任務拆分成若干個較為簡單的子模塊,這些子模塊可以看做是複雜任務的各個組成部分。因此,程式設計師將子模塊逐個完成後,就可以將其像「積木」一樣搭建起來,進而解決複雜任務。
之所以要這麼做,是因為若直接編寫C語言代碼一次性解決複雜任務,往往會讓整個代碼「揉作一團」,不僅開發時容易引入 bug,而且後期維護起來也比較痛苦。想像一下,若將任務拆分成若干個子模塊,以後發現問題時,可能只需要修改某個子模塊就可以了。但是若沒有這麼做,查找和解決問題就必須對全部代碼下手了。哪一種開發風格維護起來方便,相信讀者自然明白。
將任務拆分成子模塊後,每個子模塊常常被封裝成一個C語言函數,所以,最後的「堆積木」其實就是調用各個C語言函數。不過,每一個子模塊都有可能得到正常結果,也有可能得到異常結果,這通常用C語言函數的返回值區分。在「堆積木」階段調用各個函數時,應該根據被調用函數的返回值做不同的處理。
例如,某個子模塊負責計算用戶輸入數字的 log(對數) 值時,如果用戶輸入的是正數,則該子模塊能夠得到正常的結果。但如果用戶輸入的是負數,子模塊顯然就無法得到正常結果了。
下面是一個例子
這裡的例子代碼儘可能的簡單,是為了將重心放在討論主題上:
int cond(){staticint cnt = 0; srandom(time(NULL)+ cnt++); if(random()%10 < 5)return -1;return0;}int fun1(){/** 其他邏輯 */int ret = cond();if(ret == -1)return -1;/** 其他邏輯 */return0;}int fun2(){/** 其他邏輯 */int ret = cond();if(ret == -1)return -1;/** 其他邏輯 */return0;}int main(){if(!fun1() && !fun2())printf("cond is true\n");elseprintf("cond is false\n");return0;}
cond() 函數產出 0~10 的隨機數,如果隨機數小於 5 就返回 -1(模擬異常結果),否則返回 0(模擬正常結果)。fun1() 函數和 fun2() 函數都會根據 cond() 函數的返回值做一些進一步的工作(上面的C語言代碼略過了「進一步工作」)。在 main() 函數中「堆積木」調用 fun1() 和 fun2() 函數時,使用了 if 語句判斷它們的返回值,並且根據返回值做了不同的處理。
現在編譯這段C語言程序並執行,得到如下結果:
# gcc t.c -g# ./a.out cond is true# ./a.out cond is false從輸出結果可以發現,C語言程序輸出了「cond is false」(模擬異常)。我們往往不希望程序輸出異常結果,所以看到異常結果後,就需要知道為什麼會出現這個結果。
適當的輸出信息有利於定位異常
查看C語言原始碼,發現程序輸出異常結果是因為 main() 函數裡的 if(!fun1() && !fun2()) 為假,但是無論 fun1() 還是 fun2() 返回 -1,都會導致 if 條件表達式為假,這麼看來, main() 函數調用 fun1() 和 fun2() 函數的方式就不太合適了,因為到這裡我們已經無法繼續追蹤異常原因了。似乎 main() 函數這麼寫更合適,相關C語言代碼如下,請看:
int main(){int ret1 = fun1();int ret2 = fun2();if(!ret1 && !ret2)printf("cond is true\n");elseif(ret1 && ret2)printf("cond is false because of fun1 and fun2\n");elseif(ret1)printf("cond is false because of fun1\n");elseprintf("cond is false because of fun2\n");return0;}
編譯修改後的C語言代碼並執行,得到如下結果:
# gcc t.c# ./a.out cond is false because of fun1 and fun2# ./a.out cond is false because of fun1# ./a.out cond is false because of fun2# ./a.out cond is false because of fun2# ./a.out cond is true這次我們就知道異常輸出是哪個函數導致的了,不過僅調用兩個函數就寫了這麼多行極有可能用不到的錯誤提示代碼,太麻煩了,如果其他地方也需要用到類似的調用,就更麻煩了,有沒有更方便的方法呢?我們嘗試將錯誤提示信息塞入 fun1() 和 fun2() 函數試試,如下修改 fun1() 和 fun2() 函數的代碼:
int fun1(){/** 其他邏輯 */int ret = cond();if(ret == -1){printf("fun1 get unexpected cond\n");return -1; }/** 其他邏輯 */return0;}int fun2(){/** 其他邏輯 */int ret = cond();if(ret == -1){printf("fun2 get unexpected cond\n");return -1; }/** 其他邏輯 */return0;}
現在使用修改之前的 main() 函數如下:
編譯並執行這段C語言代碼並執行,得到如下輸出:
# gcc t.c # ./a.out fun2 get unexpected condcond is false# ./a.out fun1 get unexpected condcond is false# ./a.out cond is true這樣一來,我們既能根據輸出推斷異常是由哪個函數導致的,也能儘可能的保持C語言代碼的簡潔性。不過代碼還是有一點點囉嗦:
fun1 get unexpected condfun2 get unexpected cond這兩句輸出僅有 fun1 和 fun2 是不同的,但是我們卻需要完整的寫兩遍幾乎一樣的語句,而且以後萬一需要修改,還需要兩處都修改,一來麻煩,二來容易出錯。能不能避免這種情況呢?
使用__FUNCTION__,__LINE__,__FILE__等關鍵字
在C語言程序的編譯階段,編譯器會將__FUNCTION__,__LINE__,__FILE__這幾個關鍵字解釋為「所在函數名」,「所在行號」,「所在文件名」。所以有了這幾個關鍵字,我們就沒有必要再手動輸入函數名了,針對本節提到的例子,完全可以使用上一節介紹的 define 宏定義:
#define error_info() printf("%s get unexpected cond (%s line:%d)\n",\ __FUNCTION__, __FILE__, __LINE__)...if(ret == -1){ error_info();return -1; }...
編譯並執行這段C語言代碼,得到如下結果:
# gcc t.c -g# ./a.out cond is true# ./a.out fun1 get unexpected cond (t.c line:26)cond is false# ./a.out fun2 get unexpected cond (t.c line:41)cond is false可以看出,程序不僅把異常的函數名輸出了,還把該函數所在的文件名(t.c) 以及行號(line:26, line:41)輸出了,這樣的調試信息看起來非常舒服,在大型項目開發中,實用性很強。
類似的調試宏還有TIME,DATE等,就不一一演示了。
小結
本節討論了在C語言程序開發中,複雜任務常被拆分成多個子模塊並一一封裝為函數,這些函數可能有正常處理結果,也有可能有異常處理結果,所以本節討論了輸出基本調試信息對定位問題的重要性,並在最後介紹了幾種C語言程序開發常用的調試宏,這些宏在大型項目開發中實用性很強。