1. 程式人生 > >C語言的藝術之——函式

C語言的藝術之——函式

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

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