來自:http://edu.yesky.com/383/2033383.shtml
今天人們越來越明白軟體設計更多地是一種工程,而不是一種個人藝術。由于大型産品的開發通常由很多的人協同作戰,如果不統一程式設計規範,最終合到一起的程式,其可讀性将較差,這不僅給代碼的了解帶來障礙,增加維護階段的工作量,同時不規範的代碼隐含錯誤的可能性也比較大。
BELL實驗室的研究資料表明,軟體錯誤中18%左右産生于概要設計階段,15%左右産生于詳細設計階段,而編碼階段産生的錯誤占的比例則接近50%;分析表明,編碼階段産生的錯誤當中,文法錯誤大概占20%左右,而由于未嚴格檢查軟體邏輯導緻的錯誤、函數(子產品)之間接口錯誤及由于代碼可了解度低導緻優化維護階段對代碼的錯誤修改引起的錯誤則占了一半以上。可見,提高軟體品質必須降低編碼階段的錯誤率。如何有效降低編碼階段的錯誤呢?BELL實驗室的研究人員制定了詳細的軟體程式設計規範,并教育訓練每一位程式員,最終的結果把編碼階段的錯誤降至10%左右,同時也降低了程式的測試費用,效果相當顯著。
本文從代碼的可維護性(可讀、可了解性、可修改性)、代碼邏輯與效率、函數(子產品)接口、可測試性四個方面闡述了軟體程式設計規範,規範分成規則和建議兩種,其中規則部分為強制執行項目,而建議部分則不作強制,可根據習慣取舍。
編碼規範
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++語言中括号已經是最清晰的标志了。
另外,在長語句中,如果需要加的空格非常多,那麼應該保持整體清晰,而在局部不加空格。
最後,即使留白格,也不要連續留兩個以上空格(為了保證縮進和排比留白除外)。
<規則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. 可了解性
1.1 注釋
注釋的原則是有助于對程式的閱讀了解,注釋不宜太多也不能太少,太少不利于代碼了解,太多則會對閱讀産生幹擾,是以隻在必要的地方才加注釋,而且注釋要準确、易懂、盡可能簡潔。注釋量一般控制在30%到50%之間。
<規則1> 程式在必要的地方必須有注釋,注釋要準确、易懂、簡潔。
例如如下注釋意義不大。
if ( bReceiveFlag == TRUE)
而如下的注釋則給出了額外有用的資訊。
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
enum SCCP_USER_PRIMITIVE
{
N_UNITDATA_IND ,
N_UNITDATA_REQ ,
} ;
<規則4> 頭檔案、源檔案的頭部,應進行注釋。注釋必須列出:檔案名、作者、目的、功能、修改日志等。
例如:
說明:摷蛞枋鰯一項描述本檔案的目的和功能等。撔薷募鍬紨是修改日志清單,每條修改記錄應包括修改日期、修改者及修改内容簡述。
<規則5> 函數頭部應進行注釋,列出:函數的目的、功能、輸入參數、輸出參數、修改日志等。
形式如下:
對一些複雜的函數,在注釋中最好提供典型用法。
<規則6> 仔細定義并明确公共變量的含義、作用、取值範圍及使用方法。
在對變量聲明的同時,應對其含義、作用、取值範圍及使用方法進行注釋說明,同時若有必要還應說明與其它變量的關系。明确公共變量與操作此公共變量的函數或過程的關系,如通路、修改及建立等。
示例:
// 變量作用、含義
// 變量取值範圍
<規則7> 對指針進行充分的注釋說明,對其作用、含義、使用範圍、注意事項等說明清楚。
在對指針變量、特别是比較複雜的指針變量聲明時,應對其含義、作用及使用範圍進行注釋說明,如有必要,還應說明其使用方法、注意事項等。
示例:
//指針作用、含義
//使用範圍、方法
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);
T1_Flg = 1;
iResCode = 0;
case TA1:
iResCode = GetModemMsg(CHANNEL);
break;
default:
break;
}
<規則 10> 維護代碼時,要更新相應的注釋,删除不再有用的注釋。
保持代碼、注釋的一緻性,避免産生誤解。
1.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);
1.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++)
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;
_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;
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 ) ;
1.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
{
unsigned int A1 : 1 ;
unsigned int A2 : 1 ;
unsigned int A3 : 1 ;
} _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語句中将經常性的處理放在前面。
1.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[]);
1.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 // 各子產品共用的頭檔案
#include // 系統接口頭檔案
#ifndef _SYSTEM_DEBUG_VERSION_ // 如果是發行版本(即非DEBUG版)
#undef _EXAMPLE_UNIT_TEST_
#undef _EXAMPLE_ASSERT_TEST_
... // 将所有與測試有關的開關都關掉,即編譯時不含任何測試代碼
#endif
#include // 與另一子產品的接口頭檔案
... // 其它接口頭檔案
#else // 若定義了單元測試,則應構造單元測試所需的環境、結構等。
typdef unsigned char _UC ;
typdef unsigned long _UL ;
#define TRUE 1
... // 所有為單元測試準備的環境,如宏、枚舉、結構、聯合等。
#endif
#endif
<規則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
<規則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> 代碼版本更新要經過嚴格測試。
1.7 代碼編譯
<規則1> 打開編譯器的所有告警開關對程式進行編譯。
防止隐藏可能是錯誤的告警。
<規則2> 在同一項目組或産品組中,要統一編譯開關選項。
<規則3> 某些語句經編譯後産生告警,但如果你認為它是正确的,那麼應通過某種手段去掉告警資訊。
在Borland C/C++中,可用“#pragma warn
示例:
#pragma warn -rvl // 關閉告警
int DoExample( void )
{
// 程式,但無return語句。
}
#pragma warn +rvl // 打開告警