BELL實驗室的研究資料表明,軟件錯誤中18%左右產生於概要設計階段,15%左右產生於詳細設計階段,而編碼階段產生的錯誤佔的比例則接近50%;分析表明,編碼階段產生的錯誤當中,語法錯誤大概佔20%左右,而由於未嚴格檢查軟件邏輯導致的錯誤、函數(模塊)之間接口錯誤及由於代碼可理解度低導致優化維護階段對代碼的錯誤修改引起的錯誤則佔了一半以上。
可見,提高軟件質量必須降低編碼階段的錯誤率。如何有效降低編碼階段的錯誤呢?BELL實驗室的研究人員制定了詳細的軟件編程規範,並培訓每一位程序員,最終的結果把編碼階段的錯誤降至10%左右,同時也降低了程序的測試費用,效果相當顯著。
本文從代碼的可維護性(可讀、可理解性、可修改性)、代碼邏輯與效率、函數(模塊)接口、可測試性四個方面闡述了軟件編程規範,規範分成規則和建議兩種,其中規則部分爲強制執行項目,而建議部分則不作強制,可根據習慣取捨。
2. 編碼規範
2.1. 排版風格
<規則 1> 程序塊採用縮進風格編寫,縮進爲4個空格位。排版不混合使用空格和TAB鍵。
<規則2> 在兩個以上的關鍵字、變量、常量進行對等操作時,它們之間的操作符之前、之後或者前後要加空格;進行非對等操作時,如果是關係密切的立即操作符(如->),後不應加空格。
採用這種鬆散方式編寫代碼的目的是使代碼更加清晰。例如:
(1) 逗號、分號只在後面加空格
printf("%d %d %d" , a, b, c);
(2)比較操作符, 賦值操作符"="、 "+=",算術操作符"+"、"%",邏輯操作符"&&"、"&",位域操作符"<<"、"^"等雙目操作符的前後加空格
if(lCurrentTime >= MAX_TIME_VALUE)
a = b + c;
a *= 2;
a = b ^ 2;
(3)"!"、"~"、"++"、"--"、"&"(地址運算符)等單目操作符前後不加空格
*pApple = 'a'; // 內容操作"*"與內容之間
flag = !bIsEmpty; // 非操作"!"與內容之間
p = &cMem; // 地址操作"&" 與內容之間
i++; // "++","--"與內容之間
(4)"->"、"."前後不加空格
p->id = pId; // "->"指針前後不加空格
由於留空格所產生的清晰性是相對的,所以,在已經非常清晰的語句中沒有必要再留空格,如最內層的括號內側(即左括號後面和右括號前面)不要加空格,因爲在C/C++語言中括號已經是最清晰的標誌了。8BR>另外,在長語句中,如果需要加的空格非常多,那麼應該保持整體清晰,而在局部不加空格。
最後,即使留空格,也不要連續留兩個以上空格(爲了保證縮進和排比留空除外)。
<規則3> 函數體的開始,類的定義,結構的定義,if、for、do、while、switch及case語句中的程序都應採用縮進方式,憑捄蛻}捰稟獨佔一行並且位於同一列,同時與引用它們的語句左對齊
例如下例不符合規範。
for ( ... ) {
... // 程序代碼
}
if ( ... )
{
... // 程序代碼
}
void DoExam( void )
{
... // 程序代碼
}
應如下書寫。
for ( ... )
{
... // 程序代碼
}
if ( ... )
{
... // 程序代碼
}
void DoExam( void )
{
... // 程序代碼
}
<規則4> 功能相對獨立的程序塊之間或for、if、do、while、switch等語句前後應加一空行。
例如以下例子不符合規範。
例一:
if ( ! ValidNi( ni ) )
{
... // 程序代碼
}
nRepssnInd = SsnData[ index ].nRepssnIndex ;
nRepssnNi = SsnData[ index ].ni ;
例二:
char *pContext;
int nIndex;
long lCounter;
pContext = new (CString);
if(pContext == NULL)
{
return FALSE;
}
應如下書寫
例一:
if ( ! ValidNi( ni ) )
{
... // 程序代碼
}
nRepssnInd = SsnData[ index ].nRepssnIndex ;
nRepssnNi = SsnData[ index ].ni ;
例二:
char *pContext;
int nIndex;
long lCounter;
pContext = new (CString);
if(pContext == NULL)
{
return FALSE;
}
<規則5> if、while、for、case、default、do等語句自佔一行。
示例:如下例子不符合規範。
if(pUserCR == NULL) return;
應如下書寫:
if( pUserCR == NULL )
{
return;
}
<規則6> 若語句較長(多於80字符),可分成多行寫,劃分出的新行要進行適應的縮進,使排版整齊,語句可讀。
memset(pData->pData + pData->nCount, 0,
(m_nMax - pData->nCount) * sizeof(LPVOID));
CNoTrackObject* pValue =
(CNoTrackObject*)_afxThreadData->GetThreadValue(m_nSlot);
for ( i = 0, j = 0 ; ( i < BufferKeyword[ WordIndex ].nWordLength )
&& ( j < NewKeyword.nWordLength ) ; i ++ , j ++ )
{
... // 程序代碼
}
<規則7> 一行最多寫一條語句。
示例:如下例子不符合規範。
rect.length = 0 ; rect.width = 0 ;
rect.length = width = 0;
都應書寫成:
rect.length = 0 ;
rect.width = 0 ;
<規則8> 對結構成員賦值,等號對齊。
示例:
rect.top = 0;
rect.left = 0;
rect.right = 300;
rect.bottom = 200;
<規則9> #define的各個字段對齊
以下示例不符合規範
#define MAX_TASK_NUMBER 100
#define LEFT_X 10
#define BOTTOM_Y 400
應書寫成:
#define MAX_TASK_NUMBER 100
#define LEFT_X 10
#define BOTTOM_Y 400
<規則10> 不同類型的操作符混合使用時,使用括號給出優先級。
如本來是正確的代碼:
if( year % 4 == 0 || year % 100 != 0 && year % 400 == 0 )
如果加上括號,則更清晰。
if((year % 4) == 0 || ((year % 100) != 0 && (year % 400) == 0))
2.2. 可理解性
2.2.1.註釋
註釋的原則是有助於對程序的閱讀理解,註釋不宜太多也不能太少,太少不利於代碼理解,太多則會對閱讀產生干擾,因此只在必要的地方纔加註釋,而且註釋要準確、易懂、儘可能簡潔。註釋量一般控制在30%到50%之間。
<規則1> 程序在必要的地方必須有註釋,註釋要準確、易懂、簡潔。
例如如下注釋意義不大。
/* 如果bReceiveFlag 爲 TRUE */
if ( bReceiveFlag == TRUE)
而如下的註釋則給出了額外有用的信息。
/* 如果mtp 從連接處獲得一個消息*/
if ( bReceiveFlag == TURE)
<規則2> 註釋應與其描述的代碼相近,對代碼的註釋應放在其上方或右方(對單條語句的註釋)相鄰位置,不可放在下面,如放於上方則需與其上面的代碼用空行隔開。
示例:如下例子不符合規範。
例子1
/* 獲得系統指針和網絡指針的副本 */
nRepssnInd = SsnData[ index ].nRepssnIndex ;
nRepssnNi = SsnData[ index ].ni ;
例子2
nRepssnInd = SsnData[ index ].nRepssnIndex ;
nRepssnNi = SsnData[ index ].ni ;
/*獲得系統指針和網絡指針的副本 */
應如下書寫
/*獲得系統指針和網絡指針的副本 */
nRepssnInd = SsnData[ index ].nRepssnIndex ;
nRepssnNi = SsnData[ index ].ni ;
<規則3> 對於所有的常量,變量,數據結構聲明(包括數組、結構、類、枚舉等),如果其命名不是充分自注釋的,在聲明時都必須加以註釋,說明其含義。
示例:
/* 活動任務的數量 */
#define MAX_ACT_TASK_NUMBER 1000
#define MAX_ACT_TASK_NUMBER 1000 /*活動任務的數量 */
/* 帶原始用戶信息的SCCP接口 */
enum SCCP_USER_PRIMITIVE
{
N_UNITDATA_IND , /* 向SCCP用戶報告單元數據已經到達 */
N_UNITDATA_REQ , /* SCCP用戶的單元數據發送請求 */
} ;
<規則4> 頭文件、源文件的頭部,應進行註釋。註釋必須列出:文件名、作者、目的、功能、修改日誌等。
例如:
/*********************************************
文件名:
編寫者:
編寫日期:
簡要描述:
修改記錄:
********************************************/
說明:摷蛞枋鰯一項描述本文件的目的和功能等。撔薷募鍬紨是修改日誌列表,每條修改記錄應包括修改日期、修改者及修改內容簡述。
<規則5> 函數頭部應進行註釋,列出:函數的目的、功能、輸入參數、輸出參數、修改日誌等。
形式如下:
/*************************************************
函數名稱:
簡要描述: // 函數目的、功能等的描述
輸入: // 輸入參數說明,包括每個參數的作用、取值說明及參數間關係,
輸出: // 輸出參數的說明, 返回值的說明
修改日誌:
*************************************************/
對一些複雜的函數,在註釋中最好提供典型用法。
<規則6> 仔細定義並明確公共變量的含義、作用、取值範圍及使用方法。
在對變量聲明的同時,應對其含義、作用、取值範圍及使用方法進行註釋說明,同時若有必要還應說明與其它變量的關係。明確公共變量與操作此公共變量的函數或過程的關係,如訪問、修改及創建等。
示例:
/* SCCP轉換時錯誤代碼 */
/* 全局錯誤代碼,含義如下 */ // 變量作用、含義
/* 0 - 成功 1 - GT 表錯誤 2 -GT 錯誤 其它值- 未使用 */ // 變量取值範圍
<規則7> 對指針進行充分的註釋說明,對其作用、含義、使用範圍、注意事項等說明清楚。
在對指針變量、特別是比較複雜的指針變量聲明時,應對其含義、作用及使用範圍進行註釋說明,如有必要,還應說明其使用方法、注意事項等。
示例:
/* 學生記錄列表的頭指針 */
/* 當在此模塊中創建該列表時,該頭指針必須初始化, */
/* 這樣可以利用GetListHead()獲得這一列表。*/ //指針作用、含義
/* 該指針只在本模塊使用,其它模塊通過調用GetListHead()獲取*/
/* 當使用時必須保證它非空 */ //使用範圍、方法
STUDENT_RECORD *pStudentRecHead;
<規則8> 對重要代碼段的功能、意圖進行註釋,提供有用的、額外的信息。並在該代碼段的結束處加一行註釋表示該段代碼結束。
示例:
/* 可選通道的組合 */
if ((gsmBCIe31->radioChReq >= DUAL_HR_RCR)
&& (gsmBCIe32->radioChReq >= DUAL_HR_RCR))
{
gsmBCIe31->radioChReq = FR_RCR;
gsmBCIe32->radioChReq = FR_RCR;
}
else if ((gsmBCIe31->radioChReq >= DUAL_HR_RCR)
&& (gsmBCIe32->radioChReq == FR_RCR) )
{
gsmBCIe31->radioChReq = FR_RCR;
}
else if ((gsmBCIe31->radioChReq == FR_RCR)
&& (gsmBCIe32->radioChReq >= DUAL_HR_RCR))
{
gsmBCIe32->radioChReq = FR_RCR;
}
/* 本塊結束 ( 可選通道組合 ) */
<規則9> 在switch語句中,對沒有break語句的case分支加上註釋說明。
示例:
switch(SubT30State)
{
case TA0:
AT(CHANNEL, "AT+FCLASS=1/r", 0);
if(T30Status != 0)
{
return(1);
}
InitFax(); /* 準備發送傳真 */
AT(CHANNEL, "ATD/r",-1); /*發送CNG ,接收 CED 和 HDLC 標誌*/
T1_Flg = 1;
iResCode = 0;
/* 沒有 break; */
case TA1:
iResCode = GetModemMsg(CHANNEL);
break;
default:
break;
}
<規則 10> 維護代碼時,要更新相應的註釋,刪除不再有用的註釋。
保持代碼、註釋的一致性,避免產生誤解。
2.2 命名
本文列出Visual C++的標識符命名規範。
<規則 1> 標識符縮寫
形成縮寫的幾種技術:
1) 去掉所有的不在詞頭的元音字母。如screen寫成scrn, primtive寫成prmv。
2) 使用每個單詞的頭一個或幾個字母。如Channel Activation寫成ChanActiv,Release Indication寫成RelInd。
3) 使用變量名中每個有典型意義的單詞。如Count of Failure寫成FailCnt。
4) 去掉無用的單詞後綴 ing, ed等。如Paging Request寫成PagReq。
5) 使用標準的或慣用的縮寫形式(包括協議文件中出現的縮寫形式)。如BSIC(Base Station Identification Code)、MAP(Mobile Application Part)。
關於縮寫的準則:
1) 縮寫應該保持一致性。如Channel不要有時縮寫成Chan,有時縮寫成Ch。Length有時縮寫成Len,有時縮寫成len。
2) 在源代碼頭部加入註解來說明協議相關的、非通用縮寫。
3) 標識符的長度不超過32個字符。
<規則2> 變量命名約定
參照匈牙利記法,即
[作用範圍域前綴] + [前綴] + 基本類型 + 變量名
其中:
前綴是可選項,以小寫字母表示;
基本類型是必選項,以小寫字母表示;
變量名是必選項,可多個單詞(或縮寫)合在一起,每個單詞首字母大寫。
前綴列表如下:
前綴 意義 舉例
g_ Global 全局變量 g_MyVar
m_ 類成員變量 或 模塊級變量 m_ListBox, m_Size
s_ static 靜態變量 s_Count
h Handle 句柄 hWnd
p Pointer 指針 pTheWord
lp Long Point 長指針 lpCmd
a Array 數組 aErr
基本類型列表如下:
基本類型 意義 舉例
b Boolean 布爾 bIsOK
by Byte 字節 byNum
c Char 字符 cMyChar
i或n Intger 整數 nTestNumber
u Unsigned integer 無符號整數 uCount
ul Unsigned Long 無符號長整數 ulTime
w Word 字 wPara
dw Double Word 雙字 dwPara
l Long 長型 lPara
f Float 浮點數 fTotal
s String 字符串 sTemp
sz NULL結束的字符串 szTrees
fn Funtion 函數 fnAdd
enm 枚舉型 enmDays
x,y x,y座標
<規則3> 宏和常量的命名
宏和常量的命名規則:單詞的字母全部大寫,各單詞之間用下劃線隔開。命名舉例:
#define MAX_SLOT_NUM 8
#define EI_ENCR_INFO 0x07
const int MAX_ARRAY
<規則4> 結構和結構成員的命名
結構名各單詞的字母均爲大寫,單詞間用下劃線連接。可用或不用typedef,但是要保持一致,不能有的結構用typedef,有的又不用。如:
typedef struct LOCAL_SPC_TABLE_STRU
{
char cValid;
int nSpcCode[MAX_NET_NUM];
} LOCAL_SPC_TABLE ;
結構成員的命名同變量的命名規則。
<規則5> 枚舉和枚舉成員的命名
枚舉名各單詞的字母均爲大寫,單詞間用下劃線隔開。
枚舉成員的命名規則:單詞的字母全部大寫,各單詞之間用下劃線隔開;要求各成員的第一個單詞相同。命名舉例:
typdef enum
{
LAPD_ MDL_ASSIGN_REQ,
LAPD_MDL_ASSIGN_IND,
LAPD_DL_DATA_REQ,
LAPD_DL_DATA_IND,
LAPD_DL_UNIT_DATA_REQ,
LAPD_DL_UNIT_DATA_IND,
} LAPD_PRMV_TYPE;
<規則6> 類的命名
前綴 意義 舉例
C 類 CMyClass
CO COM類 COMMyObjectClass
CF COM class factory CFMyClassFactory
I COM interface class IMyInterface
CImpl COM implementation class CImplMyInterface
<規則7> 函數的命名
單詞首字母爲大寫,其餘均爲小寫,單詞之間不用下劃線。函數名應以一個動詞開頭,即函數名應類似摱黿峁箶。命名舉例:
void PerformSelfTest(void) ;
void ProcChanAct(MSG_CHAN_ACTIV *pMsg, UC MsgLen);
2.3. 可維護性
<規則1> 在邏輯表達式中使用明確的邏輯判斷。
示例:如下邏輯表達式不規範。
1) if ( strlen(strName) )
2) for ( index = MAX_SSN_NUMBER; index ; index -- )
3) while ( p && *p ) // 假設p爲字符指針
應改爲如下:
1) if ( strlen(strName) != 0 )
2) for ( index = MAX_SSN_NUMBER; index != 0 ; index -- )
3) while ((p != NULL) && (*p != '/0' ))
<規則2> 預編譯條件不應分離一完整的語句。
不正確:
if (( cond == GLRUN)
#ifdef DEBUG
|| (cond == GLWAIT)
#endif
)
{
}
正確:
#ifdef DEBUG
if( cond == GLRUN || cond == GLWAIT )
#else
if( cond == GLRUN )
#endif
{
}
<規則3> 在宏定義中合併預編譯條件。
不正確:
#ifdef EXPORT
for ( i = 0; i < MAX_MSXRSM; i++ )
#else
for ( i = 0; i < MAX_MSRSM; i++ )
#endif
正確:
頭文件中:
#ifdef EXPORT
#define MAX_MS_RSM MAX_MSXRSM
#else
#define MAX_MS_RSM MAX_MSRSM
#endif
源文件中:
for( i = 0; i < MAX_MS_RSM; i++ )
<規則4> 使用宏定義表達式時,要使用完備的括號。
如下的宏定義表達式都存在一定的隱患。
#define REC_AREA(a, b) a * b
#define REC_AREA(a, b) (a * b)
#define REC_AREA(a, b) (a) * (b)
正確的定義爲:
#define REC_AREA(a, b) ((a) * (b))
<規則5> 宏所定義的多條表達式應放在大括號內。
示例:下面的語句只有宏中的第一條表達式被執行。爲了說明問題,for語句的書寫稍不符規範。
#define INIT_RECT_VALUE( a, b ) /
a = 0 ; /
b = 0 ;
for ( index = 0 ; index < RECT_TOTAL_NUM ; index ++ )
INIT_RECT_VALUE( rect.a, rect.b ) ;
正確的用法應爲:
#define INIT_RECT_VALUE( a, b ) /
{ /
a = 0 ; /
b = 0 ; /
}
for ( index = 0 ; index < RECT_TOTAL_NUM ; index ++ )
{
INIT_RECT_VALUE( rect[ index ].a, rect[ index ].b ) ;
}
<規則6> 宏定義不能隱藏重要的細節,避免有return,break等導致程序流程轉向的語句。
如下例子是不規範的應用,其中隱藏了程序的執行流程。
#define FOR_ALL for(i = 0; i < SIZE; i++)
/* 數組 c 置0 */
FOR_ALL
{
c[i] = 0;
}
#define CLOSE_FILE { /
fclose(fp_local); /
fclose(fp_urban); /
return; /
}
<規則7> 使用宏時,不允許參數發生變化。
下面的例子隱藏了重要的細節,隱含了錯誤。
#define SQUARE ((x) * (x))
.
.
.
w = SQUARE(++value);
這個引用將被展開稱:
w = ((++value) * (++value));
其中value累加了兩次,與設計思想不符。正確的用法是:
w = SQUARE(x);
x++;
<規則8> 當if、while、for等語句的程序塊爲摽諗時,使用搟}敺擰_
while ( *s++ == *t++ ) ;
以上代碼不符合規範,正確的書寫方式爲:
while( *s++ == *t++ )
{
/* 無循環體 */
}
或
while( *s++ == *t++ )
{
}
<規則9> 結構中元素佈局合理,一行只定義一個元素。
如下例子不符合規範,
typedef struct
{
_UI left, top, right, bottom;
} RECT;
應書寫稱:
typedef struct
{
_UI left; /* 矩形左側 x 座標 */
_UI top;
_UI right;
_UI bottom;
} RECT;
<規則10> 枚舉值從小到大順序定義。
<規則11> 包含頭文件時,使用撓嘍月肪稊,不使用摼月肪稊。
如下引用:
#include "c:/switch/inc/def.inc"
應改爲:
#include "inc/def.inc"
或
#include "def.inc"
<規則12> 不允許使用複雜的操作符組合等。
下面用法不好,
iMaxVal = ( (a > b ? a : b) > c ? (a > b ? a : b) : c );
應該爲:
iTemp = ( a > b ? a : b);
iMaxVal = (iTemp > b ? iTemp : b);
不要把"++"、"--"操作符與其他如"+="、"-="等組合在一起形成複雜奇怪的表達式。如下的表達式那以理解。
*pStatPoi++ += 1;
*++pStatPoi += 1;
應分別改爲:
*pStatPoi += 1;
pStatPoi++;
和
++pStatPoi;
*pStatPoi += 1;
<規則13> 函數和過程中關係較爲緊密的代碼儘可能相鄰。
如初始化代碼應放在一起,不應在中間插入實現其它功能的代碼。以下代碼不符合規範,
for (uiUserNo = 0; uiUserNo < MAX_USER_NO; uiUserNo++)
{
...; /* 初始化用戶數據 */
}
pSamplePointer = NULL;
g_uiCurrentUser = 0; /* 設置當前用戶索引號 */
應必爲:
for (uiUserNo = 0; uiUserNo < MAX_USER_NO; uiUserNo++)
{
...; /* 初始化用戶數據 */
}
g_uiCurrentUser = 0; /* 設置當前用戶索引號 */
pSamplePointer = NULL;
<規則14> 每個函數的源程序行數原則上應該少於200行。
對於消息分流處理函數,完成的功能統一,但由於消息的種類多,可能超過200行的限制,不屬於違反規定。
<規則15> 語句嵌套層次不得超過5層。
嵌套層次太多,增加了代碼的複雜度及測試的難度,容易出錯,增加代碼維護的難度。
<規則16> 用sizeof來確定結構、聯合或變量佔用的空間。
這樣可提高程序的可讀性、可維護性,同時也增加了程序的可移植性。
<規則17> 避免相同的代碼段在多個地方出現。
當某段代碼需在不同的地方重複使用時,應根據代碼段的規模大小使用函數調用或宏調用的方式代替。這樣,對該代碼段的修改就可在一處完成,增強代碼的可維護性。
<規則18> 使用強制類型轉換。
示例:
USER_RECORD *pUser;
pUser = (USER_RECORD *) malloc (MAX_USER * sizeof(USER_RECORD));
<規則19> 避免使用 goto 語句。
<規則20> 避免產生摮絛蚪釘(program knots),在循環語句中,儘量避免break、goto的使用。
如下例子:
for( i = 0; i < n; i++)
{
bEof = fscanf( pInputFile, "%d;", &x[i]);
if( bEof == EOF )
{
break;
}
nSum += x[i];
}
最好按以下方式書寫,避免程序打摻釘:
for( i = 0; i < n && bEof= EOF; i++)
{
bEof = fscanf( pInputFile, "%d;", &x[i]);
if( bEof!= EOF )
{
nSum += x[i];
}
}
<規則21> 功能相近的一組常量最好使用枚舉來定義。
不推薦定義方式:
/* 功能寄存器值 */
#define ERR_DATE 1 /* 日期錯誤 */
#define ERR_TIME 2 /* 時間錯誤 */
#define ERR_TASK_NO 3 /* 任務號錯誤 */
推薦按如下方式書寫:
/*功能寄存器值 */
enum ERR_TYPE
{
ERR_DATE = 1, /*日期錯誤 */
ERR_TIME = 2, /*時間錯誤 */
ERR_TASK_NO = 3 /* 任務號錯誤 */
}
<規則22> 每個函數完成單一的功能,不設計多用途面面俱到的函數。
多功能集於一身的函數,很可能使函數的理解、測試、維護等變得困難。
使函數功能明確化,增加程序可讀性,亦可方便維護、測試。
<建議1> 循環、判斷語句的程序塊部分用花括號括起來,即使只有一條語句。
如:
if( bCondition == TRUE )
bFlag = YES;
建議按以下方式書寫:
if( bCondition == TRUE )
{
bFlag = YES;
}
這樣做的好處是便於代碼的修改、增刪。
<建議2> 一行只聲明一個變量。
不推薦的書寫方式:
void DoSomething(void)
{
int Amicrtmrs, nRC;
int nCode, nStatus;
推薦做法:
void DoSomething(void)
{
int nAmicrtmrs; /* ICR 計時器 */
int nRC; /* 返回碼 */
int nCode; /* 訪問碼 */
int nStatus; /* 處理機狀態 */
<建議3> 使用專門的初始化函數對所有的公共變量進行初始化。
<建議4> 使用可移植的數據類型,儘量不要使用與具體硬件或軟件環境關係密切的變量。
<建議5> 用明確的函數實現不明確的語句功能
示例:如下語句的功能不很明顯。
value = ( a > b ) ? a : b ;
改爲如下就很清晰了。
int max( int a, int b )
{
return ( ( a > b ) ? a : b ) ;
}
value = max( a, b ) ;
或改爲如下。
#define MAX( a, b ) ( ( ( a ) > ( b ) ) ? ( a ) : ( b ) )
value = MAX( a, b ) ;
2.4. 程序正確性、效率
<規則1> 嚴禁使用未經初始化的變量。
引用未經初始化的變量可能會產生不可預知的後果,特別是引用未經初始化的指針經常會導致系統崩潰,需特別注意。聲明變量的同時初始化,除了能防止引用未經初始化的變量外,還可能生成更高效的機器代碼。
<規則2> 定義公共指針的同時對其初始化。
這樣便於指針的合法性檢查,防止應用未經初始化的指針。建議對局部指針也在定義的同時初始化,形成習慣。
<規則3> 較大的局部變量(2K以上)應聲明成靜態類型(static),避免佔用太多的堆棧空間。
避免發生堆棧溢出,出現不可預知的軟件故障。
<規則4> 防止內存操作越界。
說明:內存操作主要是指對數組、指針、內存地址等的操作。內存操作越界是軟件系統主要錯誤之一,後果往往非常嚴重,所以當我們進行這些操作時一定要仔細小心。
A.數組越界。
char aMyArray[10];
for( i = 0; i <= 10; i++ )
{
aMyArray[i] = 0; //當i等於10時,將發生越界。
}
B.指針操作越界。
char aMyArray[10];
char *pMyArray;
pMyArray = aMyArray;
--pMyArray; // 越界
pMyArray = aMyArray;
pMyArray += 10; // 越界
<規則5> 減少沒必要的指針使用,特別是較複雜的指針,如指針的指針、數組的指針,指針的數組,函數的指針等。
用指針雖然靈活,但也對程序的穩定性造成一定威脅,主要原因是當要操作一個指針時,此指針可能正指向一個非法的地址。安安全全地使用一個指針並不是一件容易的事情。
<規則6> 防止引用已經釋放的內存空間。
在實際編程過程中,稍不留心就會出現在一個模塊中釋放了某個內存塊(如指針),而另一模塊在隨後的某個時刻又使用了它。要防止這種情況發生。
<規則7> 程序中分配的內存、申請的文件句柄,在不用時應及時釋放或關閉。
分配的內存不釋放以及文件句柄不關閉,是較常見的錯誤,而且稍不注意就有可能發生。這類錯誤往往會引起很嚴重後果,且難以定位。
<規則8> 注意變量的有效取值範圍,防止表達式出現上溢或下溢。
示例:
unsigned char cIndex = 10;
while( cIndex-- >= 0 )
{
} //將出現下溢
當cIndex等於0 時,再減1不會小於0,而是0xFF,故程序是一個死循環。
char chr = 127;
chr += 1; //127爲chr的邊界值,再加1將使chr上溢到-128,而不是128。
<規則9> 防止精度損失。
以下代碼將產生精度丟失。
#define DELAY_MILLISECONDS 10000
char time;
time = DELAY_MILLISECONDS;
WaitTime( time );
代碼的本意是想產生10秒鐘的延時,然而由於time爲字符型變量,只取DELAY_MILLISECONDS的低字節,高位字節將丟失,結果只產生了16毫秒的延時。
<規則10> 防止操易混淆的作符拼寫錯誤。
形式相近的操作符最容易引起誤用,如C/C++中的“=斢霌==敗|斢霌||敗&斢霌&&數齲羝蔥創砹耍嘁肫韃灰歡芄患觳槌隼礎_
示例:如把“&斝闖蓳&&敓蚍粗_
bRetFlag = ( pMsg -> bRetFlag & RETURN_MASK ) ;
被寫爲:
bRetFlag = ( pMsg -> bRetFlag && RETURN_MASK ) ;
<規則11> 使用無符號類型定義位域變量。
示例:
typedef struct
{
int bit1 : 1;
int bit2 : 1;
int bit3 : 1;
} bit;
bit.bit1 = 1;
bit.bit2 = 3;
bit.bit3 = 6;
printf("%d, %d, %d", bit.bit1, bit.bit2, bit.bit3 );
輸出結果爲:-1,-1, -2,不是: 1,3,6.
<規則12> switch語句的程序塊中必須有default語句。
對不期望的情況(包括異常情況)進行處理,保證程序邏輯嚴謹。
<規則13> 當聲明用於分佈式環境或不同CPU間通信環境的數據結構時,必須考慮機器的字節順序,使用的位域也要有充分的考慮。
比如Intel CPU與68360 CPU,在處理位域及整數時,其在內存存放的撍承驍,正好相反。
示例:假如有如下短整數及結構。
unsigned short int exam ;
typedef struct _EXAM_BIT_STRU
{ /* Intel 68360 */
unsigned int A1 : 1 ; /* bit 0 2 */
unsigned int A2 : 1 ; /* bit 1 1 */
unsigned int A3 : 1 ; /* bit 2 0 */
} _EXAM_BIT ;
如下是Intel CPU生成短整數及位域的方式。
內存: 0 1 2 ... (從低到高,以字節爲單位)
exam exam低字節 exam高字節
內存: 0 bit 1 bit 2 bit ... (字節的各撐粩)
_EXAM_BIT A1 A2 A3
如下是68360 CPU生成短整數及位域的方式。
內存: 0 1 2 ... (從低到高,以字節爲單位)
exam exam高字節 exam低字節
內存: 0 bit 1 bit 2 bit ... (字節的各撐粩)
_EXAM_BIT A3 A2 A1
<規則14> 編寫可重入函數時,應注意局部變量的使用(如編寫C/C++語言的可重入函數時,應使用auto即缺省態局部變量或寄存器變量)。
可重入性是指函數可以被多個任務進程調用。在多任務操作系統中,函數是否具有可重入性是非常重要的,因爲這是多個進程可以共用此函數的必要條件。另外,編譯器是否提供可重入函數庫,與它所服務的操作系統有關,只有操作系統是多任務時,編譯器纔有可能提供可重入函數庫。如DOS下BC和MSC等就不具備可重入函數庫,因爲DOS是單用戶單任務操作系統。
編寫C/C++語言的可重入函數時,不應使用static局部變量,否則必須經過特殊處理,才能使函數具有可重入性。
<規則15> 編寫可重入函數時,若使用全局變量,則應通過關中斷、信號量(即P、V操作)等手段對其加以保護。
<規則16> 結構中的位域應儘可能相鄰。結構中的位域在開始處應對齊撟紙跀或撟謹的邊界。
這樣可減少結構佔用的內存空間,減少CPU處理位域的時間,提高程序效率。
示例:如下結構中的位域佈局不合理。(假設例子在Intel CPU環境下)
typedef struct _EXAMPLE_STRU
{
unsigned int nExamOne : 6 ;
unsigned int nExamTwo : 3 ; // 此位域跨越字節摻喚訑處。
unsigned int nExamThree : 4 ;
} _EXAMPLE ;
應改爲如下(按字節對齊)。
typedef struct _EXAMPLE_STRU
{
unsigned int nExamOne : 6 ;
unsigned int nFreeOne : 2 ; // 保留bit位,使下個位域從字節開始。
unsigned int nExamTwo : 3 ; // 此位域從新的字節處開始。
unsigned int nExamThree : 4 ;
} _EXAMPLE ;
<規則17> 避免函數中不必要語句,防止程序中的垃圾代碼,預留代碼應以註釋的方式出現。
程序中的垃圾代碼不僅佔用額外的空間,而且還常常影響程序的功能與性能,很可能給程序的測試、維護等造成不必要的麻煩。
<規則18> 通過對系統數據結構的劃分與組織的改進,以及對程序算法的優化來提高空間效率。
這種方式是解決軟件空間效率的根本辦法。
示例:如下記錄學生學習成績的結構不合理。
typedef unsigned char _UC ;
typedef unsigned int _UI ;
typedef struct _STUDENT_SCORE_STRU
{
_UC szName[ 8 ] ;
_UC cAge ;
_UC cSex ;
_UC cClass ;
_UC cSubject ;
float fScore ;
} _STUDENT_SCORE ;
因爲每位學生都有多科學習成績,故如上結構將佔用較大空間。應如下改進(分爲兩個結構),總的存貯空間將變小,操作也變得更方便。
typedef struct _STUDENT_STRU
{
_UC szName[ 8 ] ;
_UC cAge ;
_UC cSex ;
_UC cClass ;
} _STUDENT ;
typedef struct _STUDENT_SCORE_STRU
{
_UI iStudentIndex ;
_UC cSubject ;
float fScore ;
} _STUDENT_SCORE ;
<規則19> 循環體內工作量最小化。
應仔細考慮循環體內的語句是否可以放在循環體之外,使循環體內工作量最小,從而提高程序的時間效率。
示例:如下代碼效率不高。
for ( i= 0 ; i< MAX_ADD_NUMBER ; i++ )
{
nSum += i;
nBackSum = nSum ; /* 備份和 */
}
語句搉BackSum = nSum ;斖耆梢苑旁趂or語句之後,如下。
for ( i = 0 ; i < MAX_ADD_NUMBER ; i ++ )
{
nSum += i ;
}
nBackSum = nSum ; /*備份和 */
<規則20> 在多重循環中,應將最忙的循環放在最內層。
<規則21> 避免循環體內含判斷語句,將與循環變量無關的判斷語句移到循環體外。
目的是減少判斷次數。循環體中的判斷語句是否可以移到循環體外,要視程序的具體情況而言,一般情況,與循環變量無關的判斷語句可以移到循環體外,而有關的則不可以。
<規則22> 儘量用乘法或其它方法代替除法,特別是浮點運算中的除法,在時間效率要求不是特別嚴格時,要優先保證程序的可讀性。
說明:浮點運算除法要佔用較多CPU資源。
示例:如下表達式運算可能要佔較多CPU資源。
#define PAI 3.1416
fRadius = fCircleLength / ( 2 * PAI ) ;
應如下把浮點除法改爲浮點乘法。
#define PAI_RECIPROCAL ( 1 / 3.1416 ) // 編譯器編譯時,將生成具體浮點數
fRadius = fCircleLength * PAI_RECIPROCAL / 2 ;
<規則23> 用“++敓瑩--敳僮鞔鎿+=1敓瑩-=1敓岣叱絛蛩俁取_
<規則24> 系統輸入(如用戶輸入)、系統輸出(如信息包輸出)、系統資源操作(如內存分配、文件及目錄操作)、網絡操作(如通信、調用等)、任務之間的操作(如通信、調用等)時必須進行錯誤、超時或者異常處理。
<建議 1> 定義字符串變量的同時將其初始化爲空即摂,以避免無限長字符串。
<建議 2> 在switch語句中將經常性的處理放在前面。
2.5. 接口
<規則1> 頭文件應採用 #ifndef / #define / #endif 的方式來防止多次被嵌入。
示例如下:
假設頭文件爲揇EF.INC",則其內容應爲:
#ifndef __DEF_INC
#define __DEF_INC
...
#endif
<規則2> 去掉沒有必要的公共變量,編程時應儘量少用公共變量。
公共變量是增大模塊間耦合的原因之一,故應減少沒必要的公共變量以降低模塊間的耦合度。應該構造僅有一個模塊或函數可以修改、創建,而其餘有關模塊或函數只訪問的公共變量,防止多個不同模塊或函數都可以修改、創建同一公共變量的現象。
<規則3> 當向公共變量傳遞數據時,要防止越界現象發生。
對公共變量賦值時,若有必要應進行合法性檢查,以提高代碼的可靠性、穩定性。
<規則4> 返回值爲指針的函數,不可將局部變量的地址作爲返回值。
當函數退出時,非static局部變量將消失,所以引用返回的指針將可能引起嚴重後果。下例將不能完成正確的功能。
char *GetFilename(int nFileNo)
{
char szFileName[20];
sprintf( szFileName, "COUNT%d", nFileNo);
return szFileName;
}
<規則5> 儘量不設計多參數函數,將不使用的參數從接口中去掉,降低接口複雜度。
減少函數間接口的複雜度。
<規則6> 對所調用函數的返回碼要仔細、全面地處理。
防止把錯誤傳遞到後面的處理流程。如有意不檢查其返回碼,應明確指明。 如:
(void)fclose(fp);
<規則7> 顯示地給出函數的返回值類型。無返回值函數定義爲void。
C、C++語言的編譯系統默認無顯示返回值函數的返回值類型爲int。
<規則8> 聲明函數原型時給出參數名稱和類型,並且與實現此函數時的參數名稱、類型保持一致,無參數的函數,用void聲明。
示例:下面聲明不正確。
int CheckData( ) ;
int SetPoint( int, int ) ;
int SetPoint( x, y )
int x, y;
應改爲如下聲明:
int CheckData( void ) ;
int SetPoint( int x, int y ) ;
<規則9> 檢查接口函數所有輸入參數的有效性。
可直接檢查或使用斷言進行檢查,尤其是指針參數。只在本模塊內使用的函數可不檢查。
<規則10> 檢查函數的所有非參數輸入,如數據文件、公共變量等。
可直接檢查或使用斷言進行檢查,尤其是指針變量。
<規則11> 聲明函數原型時,對於數組型參數,不要聲明爲指針,維護函數接口的清晰性。
示例:假設函數SortInt()完成的功能是對一組整數排序,接受的參數是一整數數組及數組中的元素個數,以下聲明不符合規範。
void SortInt(int num, int *data);
應聲明爲:
void SortInt(int num, int data[]);
2.6.代碼可測性
<規則1> 模塊編寫應該有完善的測試方面的考慮。
<規則2> 源代碼中應該設計了代碼測試的內容,如打印宏開關、變量值、函數名稱、函數值等。
在編寫代碼之前,應預先設計好程序調試與測試的方法和手段,並設計好各種調測開關及相應測試代碼如打印函數等。
程序的調試與測試是軟件生存週期中很重要的一個階段,如何對軟件進行較全面、高率的測試並儘可能地找出軟件中的錯誤就成爲很關鍵的問題。因此在編寫源代碼之前,除了要有一套比較完善的測試計劃外,還應設計出一系列代碼測試手段,爲單元測試、集成測試及系統聯調提供方便。
<規則3> 在同一項目組或產品組內,要有一套統一的爲集成測試與系統聯調準備的調測開關及相應打印函數,並且要有詳細的說明。
本規則是針對項目組或產品組的。
示例:.ext文件示例,文件名爲:EXAMPLE.EXT。
/* 頭文件開始 */
#ifndef __EXAMPLE_EXT
#define __EXAMPLE_EXT
#define _EXAMPLE_DEBUG_ // 模塊測試總開關。打開開關的含義是模塊可以
// 進行單元測試或其它功能、目的等的測試。
#ifdef _EXAMPLE_DEBUG_
#define _EXAMPLE_UNIT_TEST_ // 單元測試宏開關
#define _EXAMPLE_ASSERT_TEST_ // 斷言測試開關
... // 其它測試開關
#endif
#ifndef _EXAMPLE_UNIT_TEST_ // 若沒有定義單元測試
#include <common.h> // 各模塊共用的頭文件
#include <os.h> // 系統接口頭文件
#ifndef _SYSTEM_DEBUG_VERSION_ // 如果是發行版本(即非DEBUG版)
#undef _EXAMPLE_UNIT_TEST_
#undef _EXAMPLE_ASSERT_TEST_
... // 將所有與測試有關的開關都關掉,即編譯時不含任何測試代碼
#endif
#include <module.h> // 與另一模塊的接口頭文件
... // 其它接口頭文件
#else // 若定義了單元測試,則應構造單元測試所需的環境、結構等。
typdef unsigned char _UC ;
typdef unsigned long _UL ;
#define TRUE 1
... // 所有爲單元測試準備的環境,如宏、枚舉、結構、聯合等。
#endif
#endif /* EXAMPLE.EXT結束 */
/* 頭文件結束 */
<規則4> 在同一項目組或產品組內,調測打印出的信息串的格式要有統一的形式。信息串中至少要有所在模塊名(或源文件名)及行號。
統一的調測信息格式便於集成測試。
<規則5> 使用斷言來發現軟件問題,提高代碼可測性。
斷言是對某種假設條件進行檢查(可理解爲若條件成立則無動作,否則應報告),它可以快速發現並定位軟件問題,同時對系統錯誤進行自動報警。斷言可以對在系統中隱藏很深,用其它手段極難發現的問題進行定位,從而縮短軟件問題定位時間,提高系統的可測性。實際應用時,可根據具體情況靈活地設計斷言。
示例:下面是C語言中的一個斷言,用宏來設計的。(其中NULL爲0L)
#ifdef _EXAM_ASSERT_TEST_ // 若使用斷言測試
void ExamAssert( char * szFileName, unsigned int nLineNo )
{
printf( "/n[EXAM] Assert failed: %s, line %u/n",
szFileName, nLineNo ) ;
abort( ) ;
}
#define EXAM_ASSERT( condition ) /
if ( condition ) / // 若條件成立,則無動作
NULL ; /
else / // 否則報告
ExamAssert( __FILE__, __LINE__ )
#else // 若不使用斷言測試
#define EXAM_ASSERT( condition ) NULL
#endif /* ASSERT結束 */
<規則6> 用斷言來檢查程序正常運行時不應發生但在調測時有可能發生的非法情況。
<規則7> 不能用斷言代替錯誤處理來檢查最終產品肯定會出現且必須處理的錯誤情況。
如某模塊收到其它模塊或鏈路上的消息後,要對消息的合理性進行檢查,此過程爲正常的錯誤檢查,不能用斷言來代替。
<規則8> 用斷言確認函數的參數。
示例:假設某函數參數中有一個指針,那麼使用指針前可對它檢查,如下。
int ExamFunc( unsigned char *str )
{
EXAM_ASSERT( str != NULL ) ; // 用斷言檢查摷偕柚剛氬晃諗這個條件
... // 其它程序代碼
}
<規則9> 用斷言保證沒有定義的特性或功能不被使用。
示例:假設某通信模塊在設計時,準備提供撐櫱訑和摿訑 這兩種業務。但當前的版本中僅實現了撐櫱訑業務,且在此版本的正式發行版中,用戶(上層模塊)不應產生摿訑業務的請求,那麼在測試時可用斷言檢查用戶是否使用摿訑業務。如下。
#define EXAM_CONNECTIONLESS 0 // 無連接業務
#define EXAM_CONNECTION 1 // 連接業務
int MsgProcess( _EXAM_MESSAGE *msg )
{
unsigned char cService ; /* 消息服務類 */
EXAM_ASSERT( msg != NULL ) ;
cService = GetMsgServiceClass( msg ) ;
EXAM_ASSERT( service != EXAM_CONNECTION ) ; // 假設不使用連接業務
... // 其它程序代碼
}
<規則10> 用斷言對程序開發環境(OS/Compiler/Hardware)的假設進行檢查。
程序運行時所需的軟硬件環境及配置要求,不能用斷言來檢查,而必須由一段專門代碼處理。用斷言僅可對程序開發環境中的假設及所配置的某版本軟硬件是否具有某種功能的假設進行檢查。如某網卡是否在系統運行環境中配置了,應由程序中正式代碼來檢查;而此網卡是否具有某設想的功能,則可由斷言來檢查。
對編譯器提供的功能及特性假設可用斷言檢查,原因是軟件最終產品(即運行代碼或機器碼)與編譯器已沒有任何直接關係,即軟件運行過程中(注意不是編譯過程中)不會也不應該對編譯器的功能提出任何需求。
示例:用斷言檢查編譯器的int型數據佔用的內存空間是否爲2,如下。
EXAM_ASSERT( sizeof( int ) == 2 ) ;
<規則11> 正式軟件產品中應把斷言及其它調測代碼去掉(即把有關的調測開關關掉)。
<規則12> 用調測開關來切換軟件的DEBUG版和正式版,而不要同時存在正式版本和DEBUG版本的不同源文件,以減少維護的難度。
<規則13> 在軟件系統中設置與取消有關測試手段,不能對軟件實現的功能等產生影響。
即有測試代碼的軟件和關掉測試代碼的軟件,在功能行爲上應一致。
<規則14> 發現錯誤應該立即修改,並且若有必要記錄下來。
<規則15> 開發人員應堅持對代碼進行徹底的測試(單元測試),而不依靠他人或測試組來發現問題。
<規則16> 清理、整理或優化後的代碼要經過審查及測試。
<規則17> 代碼版本升級要經過嚴格測試。
2.7. 代碼編譯
<規則1> 打開編譯器的所有告警開關對程序進行編譯。
防止隱藏可能是錯誤的告警。
<規則2> 在同一項目組或產品組中,要統一編譯開關選項。
<規則3> 某些語句經編譯後產生告警,但如果你認爲它是正確的,那麼應通過某種手段去掉告警信息。
在Borland C/C++中,可用“#pragma warn斃垂氐艋虼蚩承└婢_
示例:
#pragma warn -rvl // 關閉告警
int DoExample( void )
{
// 程序,但無return語句。
}