C語言的藝術之——函數

好記性不如爛筆頭o(^▽^)o

系列的文章:
《C語言的藝術之——頭文件》
《C語言的藝術之——函數》
《C語言的藝術之——標識符命令與定義》
《C語言的藝術之——變量》
C語言的藝術之——註釋
C語言的藝術之——排版與格式
C語言的藝術之——安全性

C語言的藝術之函數

1、一個函數僅完成一件功能

一個函數實現多個功能給開發、使用、維護都帶來很大的困難。
將沒有關聯或者關聯很弱的語句放到同一函數中,會導致函數職責不明確,難以理解,難以測試和改動。

案例:realloc
  在標準C語言中,realloc是一個典型的不良設計。這個函數基本功能是重新分配內存,但它承擔了太多的其他任務:如果傳入的指針參數爲NULL就分配內存,如果傳入的大小參數爲0就釋放內存,如果可行則就地重新分配,如果不行則移到其他地方分配。如果沒有足夠可用的內存用來完成重新分配(擴大原來的內存塊或者分配新的內存塊),則返回NULL,而原來的內存塊保持不變。這個函數不易擴展,容易導致問題。例如下面代碼容易導致內存泄漏:

char *buffer = (char *)malloc(XXX_SIZE);
.....
buffer = (char *)realloc(buffer, NEW_SIZE);

  如果沒有足夠可用的內存用來完成重新分配,函數返回爲NULL,導致buffer原來指向的內存被丟失。

2、重複代碼應該儘可能提煉成函數

重複代碼提煉成函數可以帶來維護成本的降低。

  項目組應當使用代碼重複度檢查工具,在持續集成環境中持續檢查代碼重複度指標變化趨勢,並對新增重複代碼及時重構。當一段代碼重複兩次時,即應考慮消除重複,當代碼重複超過三次時,應當立刻着手消除重複。
  一般情況下,可以通過提煉函數的形式消除重複代碼。

3、避免函數過長,新增函數儘量不超過50行(非空非註釋行)

僅對新增函數做要求,對已有函數修改時,建議不增加代碼行。

過長的函數往往意味着函數功能不單一,過於複雜。
函數的有效代碼行數,即NBNC(非空非註釋行)應當在[1,50]區間。
業界普遍認爲一個函數的代碼行不要超過一個屏幕,避免來回翻頁影響閱讀。
例外:某些實現算法的函數,由於算法的聚合性與功能的全面性,可能會超過50行。

4、避免函數的代碼塊嵌套過深,新增函數的代碼塊嵌套不超過4層

僅對新增函數做要求,對已有的代碼建議不增加嵌套層次。

  函數的代碼塊嵌套深度指的是函數中的代碼控制塊(例如:if、for、while、switch等)之間互相包含的深度。每級嵌套都會增加閱讀代碼時的腦力消耗,因爲需要在腦子裏維護一個“棧”(比如,進入條件語句、進入循環……)。應該做進一步的功能分解,從而避免使代碼的閱讀者一次記住太多的上下文。優秀代碼參考值:[1, 4]。

5、可重入函數應避免使用共享變量;若需要使用,則應通過互斥手段(關中斷、信號量)對其加以保護

可重入函數是指可能被多個任務併發調用的函數。在多任務操作系統中,函數具有可重入性是多個任務可以共用此函數的必要條件。共享變量指的全局變量和static變量。

  編寫C語言的可重入函數時,不應使用static局部變量,否則必須經過特殊處理,才能使函數具有可重入性。
示例:函數square_exam返回g_exam平方值。那麼如下函數不具有可重入性。

int g_exam;
unsigned int example( int para )
{
    unsigned int temp;

    g_exam = para; // (**)
    temp = square_exam ( );

    return temp;
} 

  此函數若被多個線程調用的話,其結果可能是未知的,因爲當(**)語句剛執行完後,另外一個使用本函數的線程可能正好被激活,那麼當新激活的線程執行到此函數時,將使g_exam賦於另一個不同的para值,所以當控制重新回到“temp =square_exam ( )”後,計算出的temp很可能不是預想中的結果。此函數應如下改進。

int g_exam;
unsigned int example( int para )
{
    unsigned int temp;

    [申請信號量操作]    // 若申請不到“信號量”,說明另外的進程正處於
    g_exam = para;      //給g_exam賦值並計算其平方過程中(即正在使用此
    temp = square_exam( );  // 信號),本進程必須等待其釋放信號後,纔可繼
    [釋放信號量操作]    // 續執行。其它線程必須等待本線程釋放信號量後
    // 才能再使用本信號。
    return temp;
} 

6、對參數的合法性檢查,由調用者負責還是由接口函數負責,應在項目組/模塊內應統一規定。缺省由調用者負責

對於模塊間接口函數的參數的合法性檢查這一問題,往往有兩個極端現象,即:要麼是調用者和被調用者對參數均不作合法性檢查,結果就遺漏了合法性檢查這一必要的處理過程,造成問題隱患;要麼就是調用者和被調用者均對參數進行合法性檢查,這種情況雖不會造成問題,但產生了冗餘代碼,降低了效率。

7、對函數的錯誤返回碼要全面處理

一個函數(標準庫中的函數/第三方庫函數/用戶定義的函數)能夠提供一些指示錯誤發生的方法。這可以通過使用錯誤標記、特殊的返回數據或者其他手段,不管什麼時候函數提供了這樣的機制,調用程序應該在函數返回時立刻檢查錯誤指示。

8、設計高扇入,合理扇出(小於7)的函數

扇出是指一個函數直接調用(控制)其它函數的數目,而扇入是指有多少上級函數調用它。

  扇出過大,表明函數過分複雜,需要控制和協調過多的下級函數;而扇出過小,例如:總是1,表明函數的調用層次可能過多,這樣不利於程序閱讀和函數結構的分析,並且程序運行時會對系統資源如堆棧空間等造成壓力。通常函數比較合理的扇出(調度函數除外)通常是3~5。
  扇出太大,一般是由於缺乏中間層次,可適當增加中間層次的函數。扇出太小,可把下級函數進一步分解多個函數,或合併到上級函數中。當然分解或合併函數時,不能改變要實現的功能,也不能違背函數間的獨立性。
  扇入越大,表明使用此函數的上級函數越多,這樣的函數使用效率高,但不能違背函數間的獨立性而單純地追求高扇入。公共模塊中的函數及底層函數應該有較高的扇入。
  較良好的軟件結構通常是頂層函數的扇出較高,中層函數的扇出較少,而底層函數則扇入到公共模塊中。

9、廢棄代碼(沒有被調用的函數和變量)要及時清除

說明:程序中的廢棄代碼不僅佔用額外的空間,而且還常常影響程序的功能與性能,很可能給程序的測試、維護等造成不必要的麻煩。

10、函數不變參數使用const

不變的值更易於理解/跟蹤和分析,把const作爲默認選項,在編譯時會對其進行檢查,使代碼更牢固/更安全。

11、函數應避免使用全局變量、靜態局部變量和I/O操作,不可避免的地方應集中使用

帶有內部“存儲器”的函數的功能可能是不可預測的,因爲它的輸出可能取決於內部存儲器(如某標記)的狀態。這樣的函數既不易於理解又不利於測試和維護。在C語言中,函數的static局部變量是函數的內部存儲器,有可能使函數的功能不可預測。

12、檢查函數所有非參數輸入的有效性,如數據文件、公共變量等

函數的輸入主要有兩種:一種是參數輸入;另一種是全局變量、數據文件的輸入,即非參數輸入。函數在使用輸入參數之前,應進行有效性檢查。

13、函數的參數個數不超過5個

函數的參數過多,會使得該函數易於受外部(其他部分的代碼)變化的影響,從而影響維護工作。函數的參數過多同時也會增大測試的工作量。
函數的參數個數不要超過5個,如果超過了建議拆分爲不同函數。

14、除打印類函數外,不要使用可變長參函數

可變長參函數的處理過程比較複雜容易引入錯誤,而且性能也比較低,使用過多的可變長參函數將導致函數的維護難度大大增加。

15、在源文件範圍內聲明和定義的所有函數,除非外部可見,否則應該增加static關鍵字

如果一個函數只是在同一文件中的其他地方調用,那麼就用static聲明。使用static確保只是在聲明它的文件中是可見的,並且避免了和其他文件或庫中的相同標識符發生混淆的可能性。

  建議定義一個STATIC宏,在調試階段,將STATIC定義爲static,版本發佈時,改爲空,以便於後續的打熱補丁等操作。

#ifdef _DEBUG
#define STATIC static
#else
#define STATIC
#endif
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章