1. 程式人生 > >C語言:程式設計規範

C語言:程式設計規範

程式碼總體原則

1、清晰第一

清晰性是易於維護、易於重構的程式必需具備的特徵。程式碼首先是給人讀的,一般情況下,程式碼的可閱讀性高於效能,只有確定性能是瓶頸時,才應該主動優化。

2、簡潔為美

簡潔就是易於理解並且易於實現。程式碼越長越難以看懂,也就越容易在修改時引入錯誤。寫的程式碼越多,意味著出錯的地方越多,也就意味著程式碼的可靠性越低。

​ 廢棄的程式碼(沒有被呼叫的函式和全域性變數)要及時清除,重複程式碼應該儘可能提煉成函式。

3、選擇合適的風格,與程式碼原有風格保持一致

​ 如果重構/修改其他風格的程式碼時,比較明智的做法是根據現有程式碼的現有風格繼續編寫程式碼。

術語定義

原則:程式設計時必須堅持的指導思想。
規則:程式設計時強制必須遵守的約定。
建議:程式設計時必須加以考慮的約定。
說明:對此原則/規則/建議進行必要的解釋。
示例:對此原則/規則/建議從正、反兩個方面給出例子。
延伸閱讀材料:建議進一步閱讀的參考材料。

1標頭檔案

原則1.1 標頭檔案中適合放置介面的宣告,不適合放置實現。

說明:標頭檔案是模組(Module)或單元(Unit)的對外介面。標頭檔案中應放置對外部的宣告,如對外提供的函式宣告、巨集定義、型別定義等。

內部使用的函式(相當於類的私有方法)宣告不應放在標頭檔案中。

內部使用的巨集、列舉、結構定義不應放入標頭檔案中。

變數定義不應放在標頭檔案中,應放在.c檔案中。

變數的宣告儘量不要放在標頭檔案中,亦即儘量不要使用全域性變數作為介面。變數是模組或單元的內部實現細節,不應通過在標頭檔案中宣告的方式直接暴露給外部,應通過函式介面的方式進行對外暴露。 即使必須使用全域性變數,也只應當在.c中定義全域性變數,在.h中僅宣告變數為全域性的。

原則1.2 標頭檔案應當職責單一。

說明:標頭檔案過於複雜,依賴過於複雜是導致編譯時間過長的主要原因。很多現有程式碼中標頭檔案過大,職責過多,再加上迴圈依賴的問題,可能導致為了在.c中使用一個巨集,而包含十幾個標頭檔案。

原則1.3 標頭檔案應向穩定的方向包含。

說明:標頭檔案的包含關係是一種依賴,一般來說,應當讓不穩定的模組依賴穩定的模組,從而當不穩定的模組發生變化時,不會影響(編譯)穩定的模組。

規則1.1 每一個.c檔案應有一個同名.h檔案,用於宣告需要對外公開的介面。

說明:如果一個.c檔案不需要對外公佈任何介面,則其就不應當存在,除非它是程式的入口,如main函式所在的檔案。

示例:對於如下場景,如在一個.c中存在函式呼叫關係:
void foo()
{
    bar();
}
void bar()
{
    Do something;
}
必須在foo之前宣告bar,否則會導致編譯錯誤。
這一類的函式宣告,應當在.c的頭部宣告,並宣告為static的,如下:
static void bar();
規則1.2 禁止標頭檔案迴圈依賴。

說明:標頭檔案迴圈依賴,指a.h包含b.h,b.h包含c.h,c.h包含a.h之類導致任何一個頭檔案修改,都導致所有包含了a.h/b.h/c.h的程式碼全部重新編譯一遍。而如果是單向依賴,如a.h包含b.h,b.h包含c.h,而c.h不包含任何標頭檔案,則修改a.h不會導致包含了b.h/c.h的原始碼重新編譯。

規則1.3 .c/.h檔案禁止包含用不到的標頭檔案。

說明:很多系統中標頭檔案包含關係複雜,開發人員為了省事起見,可能不會去一一鑽研,直接包含一切想到的標頭檔案,甚至有些產品乾脆釋出了一個god.h,其中包含了所有標頭檔案,然後釋出給各個專案組使用,這種只圖一時省事的做法,導致整個系統的編譯時間進一步惡化,並對後來人的維護造成了巨大的麻煩。

規則1.4 標頭檔案應當自包含。

說明:簡單的說,自包含就是任意一個頭檔案均可獨立編譯。如果一個檔案包含某個標頭檔案,還要包含另外一個頭檔案才能工作的話,就會增加交流障礙,給這個標頭檔案的使用者增添不必要的負擔。

示例:
如果a.h不是自包含的,需要包含b.h才能編譯,會帶來的危害:
每個使用a.h標頭檔案的.c檔案,為了讓引入的a.h的內容編譯通過,都要包含額外的標頭檔案b.h。
額外的標頭檔案b.h必須在a.h之前進行包含,這在包含順序上產生了依賴。
注意:該規則需要與“.c/.h檔案禁止包含用不到的標頭檔案”規則一起使用,不能為了讓a.h自包含,而在a.h中包含不必要的標頭檔案。a.h要剛剛可以自包含,不能在a.h中多包含任何滿足自包含之外的其他標頭檔案。
規則1.5 總是編寫內部#include保護符(#define 保護)。

說明:多次包含一個頭檔案可以通過認真的設計來避免。如果不能做到這一點,就需要採取阻止標頭檔案內容被包含多於一次的機制。

所有標頭檔案都應當使用#define 防止標頭檔案被多重包含,命名格式為FILENAME_H,為了保證唯一性,更好的命名是PROJECTNAME_PATH_FILENAME_H。

注:沒有在巨集最前面加上“_",即使用FILENAME_H代替_FILENAME_H_,是因為一般以"_"和”__"開頭的識別符號為系統保留或者標準庫使用,在有些靜態檢查工具中,若全域性可見的識別符號以"_"開頭會給出告警。

定義包含保護符時,應該遵守如下規則:
1)保護符使用唯一名稱;
2)不要在受保護部分的前後放置程式碼或者註釋。

例外情況:標頭檔案的版權宣告部分以及標頭檔案的整體註釋部分(如闡述此標頭檔案的開發背景、使用注意事項等)可以放在保護符(#ifndef XX_H)前面。

示例:假定VOS工程的timer模組的timer.h,其目錄VOS/include/timer/timer.h,應按如下方式保護:
# ifndef VOS_INCLUDE_TIMER_TIMER_H
#define VOS_INCLUDE_TIMER_TIMER_H
...
#endif

也可以使用如下簡單方式保護:
#ifndef TIMER_H
#define TIMER_H
..
#endif
規則1.6 禁止在標頭檔案中定義變數。

說明:在標頭檔案中定義變數,將會由於標頭檔案被其他.c檔案包含而導致變數重複定義。

規則1.7 只能通過包含標頭檔案的方式使用其他.c提供的介面,禁止在.c中通過extern的方式使用外部函式介面、變數。

說明:若A.c使用了B.c定義的foo()函式,則應當在B.h中宣告extern int foo(int input);並在A.c中通過#include

規則1.8 禁止在extern “C”中包含標頭檔案。
建議1.1 一個模組通常包含多個.c檔案,建議放在同一個目錄下,目錄名即為模組名。為方便外部使用者,建議每一個模組提供一個.h,檔名為目錄名。
建議1.2 如果一個模組包含多個子模組,則建議每一個子模組提供一個對外的.h,檔名為子模組名。
建議1.3 標頭檔案不要使用非習慣用法的副檔名,如.inc。
建議1.4 同一產品統一包含標頭檔案排列方式。

說明:常見的包含標頭檔案排列方式:功能塊排序、檔名升序、穩定度排序。

示例1:
以升序方式排列標頭檔案可以避免標頭檔案被重複包含,如:
#include <a.h>
#include <b.h>
#include <c/d.h>
#include <c/e.h>
#include <f.h>
示例2:
以穩定度排序,建議將不穩定的標頭檔案放在前面,如把產品的標頭檔案放在平臺的標頭檔案前面,如下:
#include <product.h>
#include <platform.h>
相對來說,product.h修改的較為頻繁,如果有錯誤,不必編譯platform.h就可以發現product.h的錯誤,可以部分減少編譯時間。

2函式

函式設計的精髓:編寫整潔函式,同時把程式碼有效組織起來。

原則2.1 一個函式僅完成一件功能。

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

案例:realloc。在標準C語言中,realloc是一個典型的不良設計。這個函式基本功能是重新分配記憶體,但它承擔了太多的其他任務:如果傳入的指標引數為NULL就分配記憶體,如果傳入的大小引數為0就釋放記憶體,如果可行則就地重新分配,如果不行則移到其他地方分配。如果沒有足夠可用的記憶體用來完成重新分配(擴大原來的記憶體塊或者分配新的記憶體塊),則返回NULL,而原來的記憶體塊保持不變。這個函式不易擴充套件,容易導致問題。例如下面程式碼容易導致記憶體洩漏:
char *buffer = (char *)malloc(XXX_SIZE);
.....
buffer =(char *)realloc(buffer, NEW_SIZE);
如果沒有足夠可用的記憶體用來完成重新分配,函式返回為NULL,導致buffer原來指向的記憶體被丟失。
原則2.2 重複程式碼應該儘可能提煉成函式。

說明:重複程式碼提煉成函式可以帶來維護成本的降低。

重複程式碼是我司不良程式碼最典型的特徵之一。在“程式碼能用就不改”的指導原則之下,大量的煙囪式設計及其實現充斥著各產品程式碼之中。新需求增加帶來的程式碼拷貝和修改,隨著時間的遷移,產品中堆砌著許多類似或者重複的程式碼。
專案組應當使用 ,在持續整合環境中持續檢查程式碼重複度指標變化趨勢,並對新增重複程式碼及時重構。當一段程式碼重複超過三次時,應當立刻著手消除重複。

規則2.1 避免函式過長,新增函式不超過50行(非空非註釋行)。

說明:本規則僅對新增函式做要求,對已有函式修改時,建議不增加程式碼行。

過長的函式往往意味著函式功能不單一,過於複雜(參見原則2.1:一個函式只完成一個功能)。函式的有效程式碼行數,即NBNC(非空非註釋行)應當在[1,50]區間。

例外:某些實現演算法的函式,由於演算法的聚合性與功能的全面性,可能會超過50行。
延伸閱讀材料:業界普遍認為一個函式的程式碼行不要超過一個螢幕,避免來回翻頁影響閱讀;一般的程式碼度量工具建議都對此進行檢查,例如Logiscope的函式度量:”Number of Statement” (函式中的可執行語句數)建議不超過20行,QA C建議一個函式中的所有行數(包括註釋和空白行)不超過50行。

規則2.2 避免函式的程式碼塊巢狀過深,新增函式的程式碼塊巢狀不超過4層。

說明:本規則僅對新增函式做要求,對已有的程式碼建議不增加巢狀層次。

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

示例:如下程式碼巢狀深度為5void serial (void)
{
    if (!Received)
    {
    TmoCnt = 0;
        switch (Buff)
        {
        case AISGFLG:
            if ((TiBuff.Count > 3)
            && ((TiBuff.Buff[0] == 0xff) || (TiBuf.Buff[0] == CurPa.ADDR)))
            {
                Flg7E = false;
                Received = true;
            }
            else
            {
                TiBuff.Count = 0;
                Flg7D = false;
                Flg7E = true;
            }
            break;
        default:
        break;
        }
    }
}
規則2.3 可重入函式應避免使用共享變數;若需要使用,則應通過互斥手段(關中斷、訊號量)對其加以保護。

說明:可重入函式是指可能被多個任務併發呼叫的函式。在多工作業系統中,函式具有可重入性是多個任務可以共用此函式的必要條件。共享變數指的全域性變數和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;
}
規則2.4 對引數的合法性檢查,由呼叫者負責還是由介面函式負責,應在專案組/模組內應統一規定。預設由呼叫者負責。
規則2.5 對函式的錯誤返回碼要全面處理。

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

示例:下面的程式碼導致宕機
    FILE *fp = fopen( "./writeAlarmLastTime.log","r");
    char buff[128] = "";
    fscanf(fp,“%s”,buff); /* 讀取最新的告警時間;由於檔案writeAlarmLastTime.log為空,導致buff為空 */
    fclose(fp);
    long fileTime = getAlarmTime(buff); /* 解析獲取最新的告警時間;getAlarmTime函式未檢查buff指標,導致宕機 */
正確寫法:
    FILE *fp = fopen( "./writeAlarmLastTime.log","r");
    char buff[128] = "";
    if ( EOF == fscanf(fp,“%s”,buff) ) //檢查函式fscanf的返回值,確保讀到資料
    {
        return ;
    }
    fclose(fp);
    long fileTime = getAlarmTime(buff); //解析獲取最新的告警時間;
規則2.6 設計高扇入,合理扇出(小於7)的函式。

說明:扇出是指一個函式直接呼叫(控制)其它函式的數目,而扇入是指有多少上級函式呼叫它。

這裡寫圖片描述

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

規則2.7 廢棄程式碼(沒有被呼叫的函式和變數)要及時清除。

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

建議2.1 函式不變引數使用const。

說明:不變的值更易於理解/跟蹤和分析,把const作為預設選項,在編譯時會對其進行檢查,使程式碼更牢固/更安全。

示例:C99標準 7.21.4.4strncmp 的例子,不變引數宣告為constint strncmp(const char *s1, const char *s2, register size_t n)
{
    register unsigned char u1, u2;
    while (n-- > 0)
    {
        u1 = (unsigned char) *s1++;
        u2 = (unsigned char) *s2++;
        if (u1 != u2)
        {
            return u1 - u2;
        }
        if (u1 == '\0')
        {
            return 0;
        }
    }
    return 0;
}
建議2.2 函式應避免使用全域性變數、靜態區域性變數和I/O操作,不可避免的地方應集中使用。

說明:帶有內部“儲存器”的函式的功能可能是不可預測的,因為它的輸出可能取決於內部儲存器(如某標記)的狀態。這樣的函式既不易於理解又不利於測試和維護。在C語言中,函式的static區域性變數是函式的內部儲存器,有可能使函式的功能不可預測,然而,當某函式的返回值為指標型別時,則必須是static的區域性變數的地址作為返回值,若為auto類,則返回為錯針。

示例:如下函式,其返回值(即功能)是不可預測的。

unsigned int integer_sum( unsigned int base )
{
    unsigned int index;
    static unsigned int sum = 0;    // 注意,是static型別的。
                                  // 若改為auto型別,則函式即變為可預測。
    for (index = 1; index <= base; index++)
    {
        sum += index;
    }
    return sum;
}
建議2.3 檢查函式所有非引數輸入的有效性,如資料檔案、公共變數等。

說明:函式的輸入主要有兩種:一種是引數輸入;另一種是全域性變數、資料檔案的輸入,即非引數輸入。函式在使用輸入引數之前,應進行有效性檢查。

示例:下面的程式碼導致宕機

hr = pRootNode->get_firstChild(&pLogItem); // list.xml 為空,導致讀出pLogItem為空..
hr = pLogItem->get_nextSibling(&pMediaNextNode); // pLogItem為空,導致宕機

正確寫法:確保讀出的內容非空。

hr