軟體開發規範
在研究項目團隊協作開發的情況下(這裡的團隊協作也适合于應用項目的開發),程式設計時應該強調的一個重要方面是程式的易讀性,在保證軟體速度等性能名額能滿足使用者需求的情況下,能讓其他程式員容易讀懂你所編寫的程式。若研究項目小組的所有開發人員都遵循統一的、鮮明的一套程式設計風格,可以讓協作者、後繼者和自己一目了然,在很短的時間内看清楚程式結構,了解設計的思路,大大提高代碼的可讀性、可重用性、程式健壯性、可移植性、可維護性。
制定本程式設計規範的目的是為了提高軟體開發效率及所開發軟體的可維護性,提高軟體的品質。本規範由程式風格、命名規範、注釋規範、程式健壯性、可移植性、錯誤處理以及軟體的子產品化規範等部分組成。
本軟體開發規範适合讨論C/C++程式設計。
每個C++/C程式通常分為兩個檔案。一個檔案用于儲存程式的聲明(declaration),稱為頭檔案。另一個檔案用于儲存程式的實作(implementation),稱為定義(definition)檔案。
C++/C程式的頭檔案以“.h”為字尾,C程式的定義檔案以“.c”為字尾,C++程式的定義檔案通常以“.cpp”為字尾(也有一些系統以“.cc”或“.cxx”為字尾)。
檔案資訊聲明位于頭檔案和定義檔案的開頭(參見示例1-1),主要内容有:
(1) 版權資訊;
(2) 檔案名稱,項目代碼,摘要,參考文獻;
(3) 目前版本号,作者/修改者,完成日期;
(4) 版本曆史資訊;
(5) 主要函數描述。
////////////////////////////////////////////////////////////////////////////
// Copyright (c) 2004, Department of Mathematics, Zhejiang University
// All rights reserved.
//
// Filename :filename.h
// Project Code :The project code about this file
// Abstract :Describe the content of this file summarily
// Reference :......
// Version :1.1
// Author :the name of author(mender)
// Accomplished date : September 2, 2004
// Replaced version : 1.0
// Original Author : the name of original author(mender)
// Accomplished date : September 10, 2003
// Main functions :
// Function 1 Return code Function name(Parameter Explain)
// Function 2 Return code Function name(Parameter Explain)
// ...
// Function n Return code Function name(Parameter Explain)
示例1-1 檔案資訊聲明
☆ 【規則1.1-1】 檔案資訊聲明以兩行斜杠開始,以兩行斜杠結束,每一行都以兩個斜杠開始;
☆ 【規則1.1-2】 檔案資訊聲明包含五個部分,各部分之間以一空行間隔;
☆ 【規則1.1-3】 在主要函數部分描述了檔案所包含的主要函數的聲明資訊,如果是頭檔案,這一部分是可以省略的。
頭檔案由三部分内容組成:
(1) 頭檔案開頭處的檔案資訊聲明(參見示例1-1);
(2) 預處理塊;
(3) 函數和類結構聲明等。
假設頭檔案名稱為 filesystem.h,頭檔案的結構參見示例1-2。
☆ 【規則1.2-1】 為了防止頭檔案被重複引用,應當用ifndef/define/endif結構産生預處理塊;“#ifndef”或者“#define”後以TAB鍵代替SPACE鍵做空格;如果頭檔案名稱是由多個單詞組成,則各單詞間以下劃線“_”連接配接,例如有頭檔案名稱為“filesystem.h”,則定義如下:“#ifndef _FILE_SYSTEM_H_”;
☆ 【規則1.2-2】 用 #include <filename.h> 格式來引用标準庫的頭檔案(編譯器将從标準庫目錄開始搜尋);
☆ 【規則1.2-3】 用 #include “filename.h” 格式來引用非标準庫的頭檔案(編譯器将從使用者的工作目錄開始搜尋);
☆ 【建議1.2-1】 頭檔案中隻存放“聲明”而不存放“定義”;
☆ 【建議1.2-1】 頭檔案中應包含所有定義檔案所定義的函數聲明,如果一個頭檔案對應多個定義檔案,則不同定義檔案内實作的函數要分開聲明,并作注釋以解釋所聲明的函數從屬于那一個定義檔案;
☆ 【建議1.2-3】 宏定義和函數聲明分離,在兩個頭檔案中定義,如果沒有類成員函數,可以将類和結構的定義與函數聲明分離,也就是說一個頭檔案專用于宏定義,一個頭檔案專用于類和結構的定義,一個頭檔案專用于函數聲明;
☆ 【建議1.2-4】 在C++ 文法中,類的成員函數可以在聲明的同時被定義,并且自動成為内聯函數。這雖然會帶來書寫上的友善,但卻造成了風格不一緻,弊大于利。建議将成員函數的定義與聲明分開,不論該函數體有多麼小。
頭檔案的結構如下:
//檔案資訊聲明見示例1-1,此處省略。
#ifndef _FILE_SYSTEM_H_ //avoid referencing the file filesystem.h repeat
#define _FILE_SYSTEM_H_
#include <math.h> //reference standard head file
…
#include “myheader.h” //reference non-standard head file
void Function1(…); //global function declare
class CBox //class structure decalre
{
};
#endif
示例1-2 C++/C頭檔案的結構
定義檔案有三部分内容:
(1) 定義檔案開頭處的檔案資訊聲明(參見示例1-1);
(2) 對一些頭檔案的引用;
(3) 程式的實作體(包括資料和代碼)。
假設定義檔案的名稱為 filesystem.c,定義檔案的結構參見示例1-3。
//檔案資訊聲明見示例1-1,此處省略。
#include “filesystem.h” //reference a head file
//global function realization
void Function1(…)
}
//class member function realization
void CBox::Draw(…)
示例1-3 C++/C定義檔案的結構
早期的程式設計語言如Basic、Fortran沒有頭檔案的概念,C++/C語言的初學者雖然會用使用頭檔案,但常常不明其理。這裡對頭檔案的作用略作解釋:
(1) 通過頭檔案來調用庫功能。在很多場合,源代碼不便(或不準)向使用者公布,隻要向使用者提供頭檔案和二進制的庫即可。使用者隻需要按照頭檔案中的接口聲明來調用庫功能,而不必關心接口怎麼實作的。編譯器會從庫中提取相應的代碼;
(2) 頭檔案能加強類型安全檢查。如果某個接口被實作或被使用時,其方式與頭檔案中的聲明不一緻,編譯器就會指出錯誤,這一簡單的規則能大大減輕程式員調試、改錯的負擔。
如果一個軟體的頭檔案數目比較多(如超過十個),通常應将頭檔案和定義檔案分别儲存于不同的目錄,以便于維護。
例如可将頭檔案儲存于include目錄,将定義檔案儲存于source目錄(可以是多級目錄)。
如果某些頭檔案是私有的,它不會被使用者的程式直接引用,則沒有必要公開其“聲明”。為了加強資訊隐藏,這些私有的頭檔案可以和定義檔案存放于同一個目錄。
比較著名的命名規則當推“匈牙利” 命名法,該命名規則的主要思想是“在變量和函數名中加入字首以增進人們對程式的了解”。例如所有的字元變量均以ch為字首,若是指針變量則追加字首p。如果一個變量由ppch開頭,則表明它是指向字元指針的指針。
“匈牙利”法最大的缺點是煩瑣,例如
int i, j, k;
float x, y, z;
倘若采用“匈牙利”命名規則,則應當寫成
int iI, iJ, ik; // 字首 i表示int類型
float fX, fY, fZ; // 字首 f表示float類型
如此煩瑣的程式會讓絕大多數程式員無法忍受。
總的說來,沒有一種命名規則可以讓所有的程式員贊同,且命名規則對軟體産品而言并不是“成敗悠關”的事,而且在不同的平台和不同的環境下編寫的程式所應遵循的規則也不盡相同,是以我們隻是追求制定一種令大多數項目成員滿意的命名規則,并在項目中貫徹實施。
本節論述的共性規則是被大多數程式員采納的,我們應當在遵循這些共性規則的前提下,再擴充特定的規則,如2.2節
☆ 【規則2.1-1】 辨別符應當直覺且可以拼讀,可望文知意,不必進行“解碼”;
☆ 【規則2.1-2】 辨別符的長度應當符合“min-length && max-information”原則;
☆ 【規則2.1-3】 命名規則盡量與所采用的作業系統或開發工具的風格保持一緻;
☆ 【規則2.1-4】 程式中不要出現僅靠大小寫區分的相似的辨別符。
☆ 【規則2.1-5】 程式中不要出現辨別符完全相同的局部變量和全局變量,盡管兩者的作用域不同而不會發生文法錯誤,但會使人誤解;
☆ 【規則2.1-6】 變量的名字應當使用“名詞”或者“形容詞+名詞”;
☆ 【規則2.1-7】 全局函數的名字應當使用“動詞”或者“動詞+名詞”(動賓詞組);
☆ 【規則2.1-8】 用正确的反義詞組命名具有互斥意義的變量或相反動作的函數等;
☆ 【建議2.1-1】 盡量避免名字中出現數字編号,如Value1,Value2等,除非邏輯上的确需要編号;
注:
2.1.1 辨別符最好采用英文單詞或其組合,便于記憶和閱讀,切忌使用漢語拼音來命名,程式中的英文單詞一般不要太複雜,用詞應當準确,例如不要把CurrentValue寫成NowValue;
2.1.2 标示符的長度應當以最小的長度實作最多資訊,一般來說,長名字能更好地表達含義,但并非長的變量名就一定要比短的變量名要好,此外單字元的名字也是有用的,常見的如i,j,k,m,n,x,y,z等,它們通常可用作函數内的局部變量;
2.1.3 不同的作業系統的程式設計風格是不一樣的,例如Windows應用程式的辨別符通常采用“大小寫”混排的方式,如AddChild,而Unix應用程式的辨別符通常采用“小寫加下劃線”的方式,如add_child,别把這兩類風格混在一起使用;
☆ 【規則2.2-1】 變量的命名規則要求采用“匈牙利法則”,即開頭字母用變量的類型,其餘部分用變量的英文意思或其英文意思的縮寫,盡量避免采用中文拼音,要求單詞的第一個字母大寫;
即:變量名=變量類型+變量英文意思(或縮寫)
變量類型請參見附表1-變量類型表;
☆ 【規則2.2-2】 類名和函數名用大寫字母開頭的單詞組合而成;對struct、union、class變量的命名要求定義的類型用大寫,結構采用S開頭,聯合體采用U開頭,類采用C開頭;
例如:
struct SPoint
int m_nX;
int m_nY;
union URecordLen
BYTE m_byRecordNum;
BYTE m_byRecordLen;
class CNode
{
//類成員變量或成員函數
};
☆ 【規則2.2-3】 指針變量命名的基本原則為:
一重指針變量的基本原則為:
變量名= “p”+變量類型字首+命名
對多重指針變量的基本原則為:
二重指針:
變量名=“pp”+變量類型字首+命名
三重指針:
變量名=“ppp”+變量類型字首+命名
......
例如一個short*型的變量應該表示為pnStart;
☆ 【規則2.2-4】 全局變量用g_開頭;例如一個全局的長型變量定義為g_lFileNum,
即:變量名=g_+變量類型+變量的英文意思(或縮寫);
☆ 【規則2.2-5】 靜态變量采用s_開頭;例如一個靜态的指針變量定義為s_plPrevInst,
即:變量名=s_+變量類型+變量的英文意思(或縮寫);
☆ 【規則2.2-6】 類成員變量采用m_開頭;例如一個長型成員變量定義為m_lCount,
即:變量名=m_+變量類型+變量的英文意思(或縮寫);
☆ 【規則2.2-7】 對const的變量要求在變量的命名規則前加入c_(若作為函數的輸入參數,可以不加),
即:變量名=c_+變量命名規則,例如:
const char* c_szFileName;
☆ 【規則2.2-8】 對枚舉類型(enum)中的變量,要求用枚舉變量或其縮寫做字首,且用下劃線隔離變量名,所有枚舉類型都要用大寫,例如:
enum EMDAYS
EMDAYS_MONDAY;
EMDAYS_TUESDAY;
......
};
☆ 【規則2.2-9】 對常量(包括錯誤的編碼)命名,要求常量名用大寫,常量名用英文意思表示其意思,用下劃線分割單詞,例如:
#define CM_7816_OK 0x9000;
☆ 【規則2.2-10】 為了防止某一軟體庫中的一些辨別符和其它軟體庫中的沖突,可以為各種辨別符加上能反映軟體性質的字首。例如三維圖形标準OpenGL的所有庫函數均以gl開頭,所有常量(或宏定義)均以GL開頭。
程式風格雖然不會影響程式的功能,但會影響程式的可讀性,追求清晰、美觀,是程式風格的重要構成因素。
空行起着分隔程式段落的作用。空行得體(不過多也不過少)将使程式的布局更加清晰。空行不會浪費記憶體,雖然列印含有空行的程式是會多消耗一些紙張,但是值得。
☆ 【規則3.1-1】 在每個類聲明之後、每個函數定義結束之後都要加空行。參見示例3.1(a);
☆ 【規則3.1-2】 在一個函數體内,邏揖上密切相關的語句之間不加空行,其它地方應加空行分隔。參見示例3.1(b);
// blank line
…
void Function2(…)
void Function3(…)
// blank line
while (condition)
statement1;
// blank line
if (condition)
{
statement2;
}
else
statement3;
statement4;
}
示例3.1(a) 函數之間的空行 示例3.1(b) 函數内部的空行
☆ 【規則3.2-1】 一行代碼隻做一件事情,如隻定義一個變量,或隻寫一條語句,這樣的代碼容易閱讀,并且友善于寫注釋;
☆ 【規則3.2-2】 if、for、while、do等語句自占一行,執行語句不得緊跟其後,不論執行語句有多少都要加{},這樣可以防止書寫失誤;
☆ 【規則3.2-3】 if、for、while、do等語句的“{”要單獨占用一行;
☆ 【建議3.2-1】 所有函數内的變量都在函數開始處定義;
☆ 【建議3.2-2】 盡可能在定義變量的同時初始化該變量(就近原則),如果變量的引用處和其定義處相隔比較遠,變量的初始化很容易被忘記。如果引用了未被初始化的變量,可能會導緻程式錯誤,本建議可以減少隐患。
示例3.2(a)為風格良好的代碼行,示例3.2(b)為風格不良的代碼行。
int nWidth; // width
int nHeight; // height
int nDepth; // depth
int nWidth,nHight,nDepth;//width,height,depth
x = a + b;
y = c + d;
z = e + f;
X = a + b; y = c + d; z = e + f;
if (nWidth < nHight)
DoSomething();
if (nWidth < nHight) DoSomething();
for (initialization; condition; update)
DoSomething();
Other();
示例3.2(a) 風格良好的代碼行 示例3.2(b) 風格不良的代碼行
☆ 【規則3.3-1】 關鍵字之後要留白格,象const、virtual、inline、case 等關鍵字之後至少要留一個空格,否則無法辨析關鍵字,象if、for、while等關鍵字之後應留一個空格再跟左括号‘(’,以突出關鍵字;
☆ 【規則3.3-2】 函數名之後不要留白格,緊跟左括号‘(’,以與關鍵字差別;
☆ 【規則3.3-3】 ‘(’向後緊跟,‘)’、‘,’、‘;’向前緊跟,緊跟處不留白格;
☆ 【規則3.3-4】 ‘,’之後要留白格,如Function(x, y, z),如果‘;’不是一行的結束符号,其後要留白格,如for (initialization; condition; update);
☆ 【規則3.3-5】 指派操作符、比較操作符、算術操作符、邏輯操作符、位域操作符,如“=”、“+=” “>=”、“<=”、“+”、“*”、“%”、“&&”、“||”、“<<”,“^”等二進制操作符的前後應當加空格;
☆ 【規則3.3-6】 一進制操作符如“!”、“~”、“++”、“--”、“&”(位址運算符)等前後不加空格;
☆ 【規則3.3-7】 象“[]”、“.”、“->”這類操作符前後不加空格;
☆ 【建議3.3-1】 對于表達式比較長的for語句和if語句,為了緊湊起見可以适當地去掉一些空格,如for (i=0; i<10; i++)和if ((a<=b) && (c<=d))
void Func1(int x, int y, int z); // favorable style
void Func1 (int x,int y,int z); // ill style
if (year >= 2000) // favorable style
if(year>=2000) // ill style
if ((a>=b) && (c<=d)) // favorable style
if(a>=b&&c<=d) // ill style
for (i=0; i<10; i++) // favorable style
for(i=0;i<10;i++) // ill style
for (i = 0; I < 10; i ++) // favorable style
x = a < b ? a : b; // favorable style
x=a<b?a:b; // ill style
int *x = &y; // favorable style
int * x = & y; // ill style
array[5] = 0; // Do not use array [ 5 ] = 0;
a.Function(); // Do not use a . Function();
b->Function(); // Do not use b -> Function();
示例3.3 代碼行内的空格
☆ 【規則3.4-1】 程式的分界符‘{’和‘}’應獨占一行并且位于同一列,同時與引用它們的語句左對齊;
☆ 【規則3.4-2】 { }之内的代碼塊在‘{’右邊數格處左對齊;
☆ 【規則3.4.3】 代碼的的對齊采用TAB鍵而不采用空格鍵對齊,一般TAB鍵設定為向後空4個空格。
示例3.4(a)為風格良好的對齊,示例3.4(b)為風格不良的對齊。
void Function(int x)
… // program code
void Function(int x){
if (condition)
else
if (condition){
else {
for (initialization; condition; update){
While (condition)
while (condition){
如果出現嵌套的{},則使用縮進對齊,如:
{
…
{
…
}
…
示例3.4(a) 風格良好的對齊 示例3.4(b) 風格不良的對齊
☆ 【規則3.5-1】 代碼行最大長度宜控制在70至80個字元以内;
☆ 【規則3.5-2】 長表達式要在低優先級操作符處拆分成新行,操作符放在新行之首(以便突出操作符),拆分出的新行要進行适當的縮進,使排版整齊,語句可讀。
if ((very_longer_variable1 >= very_longer_variable12)
&& (very_longer_variable3 <= very_longer_variable14)
&& (very_longer_variable5 <= very_longer_variable16))
DoSomething();
virtual CMatrix CMultiplyMatrix (CMatrix leftMatrix,
CMatrix rightMatrix);
for (very_longer_initialization;
very_longer_condition;
very_longer_update)
示例3.5 長行的拆分
修飾符 * 和 & 應該靠近資料類型還是該靠近變量名,是個有争議的活題,若将修飾符 * 靠近資料類型,例如:int* x; 從語義上講此寫法比較直覺,即x是int 類型的指針,上述寫法的弊端是容易引起誤解,例如:int* x, y; 此處y容易被誤解為指針變量。雖然将x和y分行定義可以避免誤解,但并不是人人都願意這樣做。
☆ 【規則3.6-1】 應當将修飾符 * 和 & 緊靠變量名;
C語言的注釋符為“/*…*/”。C++語言中,程式塊的注釋常采用“/*…*/”,行注釋一般采用“//…”。注釋通常用于:
(1)版本、版權聲明;
(2)函數接口說明;
(3)重要的代碼行或段落提示。
雖然注釋有助于了解代碼,但注意不可過多地使用注釋。參見示例3.7。
☆ 【規則3.7-1】 注釋是對代碼的“提示”,而不是文檔,程式中的注釋不可喧賓奪主,注釋太多了會讓人眼花缭亂,注釋的花樣要少;
☆ 【規則3.7-2】 如果代碼本來就是清楚的,則不必加注釋;例如
i++; // i 加 1,多餘的注釋
☆ 【規則3.7-3】 邊寫代碼邊注釋,修改代碼同時修改相應的注釋,以保證注釋與代碼的一緻性,不再有用的注釋要删除;
☆ 【規則3.7-4】 注釋應當準确、易懂,防止注釋有二義性,錯誤的注釋不但無益反而有害;
☆ 【規則3.7-5】 盡量避免在注釋中使用縮寫,特别是不常用縮寫;
☆ 【規則3.7-6】 注釋的位置應與被描述的代碼相鄰,可以放在代碼的上方或右方,不可放在下方;
☆ 【規則3.7-8】 當代碼比較長,特别是有多重嵌套時,應當在一些段落的結束處加注釋,便于閱讀;
☆ 【建議3.7-1】 對于多行代碼的注釋,盡量不采用“/*...*/”,而采用多行“//”注釋,這樣雖然麻煩,但是在做屏蔽調試時不用查找配對的“/*...*/”。
////////////////////////////////////////////////////////////////////
// Function capacity:
// Parameter declare:
// Return value :
////////////////////////////////////////////////////////////////////
void Function(float x, float y, float z)
if (…)
while (…)
{
} // end of while
} // end of if
示例3.7 程式的注釋
檔案頭的注釋請參見1.1,檔案頭的注釋是以兩行斜杠開始,以兩行斜杠結束(以差別于函數的注釋)。
一般說來每個函數都應該做詳細的注釋,函數頭的注釋是以一行斜杠開始,以一行斜杠結束,注釋的内容包括“功能”,“參數”,“傳回值”,“設計思想”,“調用函數”,“日期”,“修改記錄”等幾個方面,函數頭的注釋格式如下:
//////////////////////////////////////////////////////////////////////////////////////////////
// Function capacity : Describe the function capacity
// Parameter declare :
// parameter 1: Describe the function of parameter ( input/output parameter )
// parameter 2: Describe the function of parameter ( input/output parameter )
// ......
// Return value : Describe the possible return value
// Designed idea : Describe designed idea about the function
// Author :
// Creation date : Creation date(YY-MM-DD)
// Transferred function: List the sub-function in the function
// Modification record:
// (一)Mender 1: Modified date: modified content
函數是C++/C程式的基本功能單元,其重要性不言而喻。函數設計的細微缺點很容易導緻該函數被錯用,是以光使函數的功能正确是不夠的。本章重點論述函數的接口設計和内部實作的一些規則。
函數接口的兩個要素是參數和傳回值。C語言中,函數的參數和傳回值的傳遞方式有兩種:值傳遞(pass by value)和指針傳遞(pass by pointer)。C++ 語言中多了引用傳遞(pass by reference)。由于引用傳遞的性質象指針傳遞,而使用方式卻象值傳遞,初學者常常迷惑不解,容易引起混亂,請先閱讀6.6節“引用與指針的比較”。
☆ 【規則4.1-1】 參數的書寫要完整,不要貪圖省事隻寫參數的類型而省略參數名字,如果函數沒有參數,則用void填充;例如:
void SetValue(int nWidth, int nHeight); // 良好的風格
void SetValue(int, int); // 不良的風格
float GetValue(void); // 良好的風格
float GetValue(); // 不良的風格
☆ 【規則4.1-2】 參數命名要恰當,順序要合理;
例如編寫字元串拷貝函數StringCopy,它有兩個參數,如果把參數 名字起為str1和str2,例如:
void StringCopy(char *str1, char *str2);
那麼我們很難搞清楚究竟是把str1拷貝到str2中,還是剛好倒過來,可以把參數名字起得更有意義,如叫strSource和strDestination。這樣從名字上就可以看出應該把strSource拷貝到strDestination。還有一個問題,這兩個參數那一個該在前那一個該在後?參數的順序要遵循程式員的習慣。一般地,應将目的參數放在前面,源參數放在後面。如果将函數聲明為:
void StringCopy(char *strSource, char *strDestination);
别人在使用時可能會不假思索地寫成如下形式:
char str[20];
StringCopy(str, “Hello World”); // 參數順序颠倒
☆ 【規則4.1-3】 如果參數是指針,且僅作輸入用,則應在類型前加const,以防止該指針在函數體内被意外修改。例如:
void StringCopy(char *strDestination,const char *strSource);
☆ 【規則4.1-4】 如果輸入參數以值傳遞的方式傳遞對象,則宜改用“const &”方式來傳遞,這樣可以省去臨時對象的構造和析構過程,進而提高效率;
☆ 【建議4.1-1】 避免函數有太多的參數,參數個數盡量控制在5個以内。如果參數太多,在使用時容易将參數類型或順序搞錯;
☆ 【建議4.1-2】 盡量不要使用類型和數目不确定的參數;
C标準庫函數printf是采用不确定參數的典型代表,其原型為:
int printf(const chat *format[, argument]…);
這種風格的函數在編譯時喪失了嚴格的類型安全檢查。
☆ 【規則4.2-1】 不要省略傳回值的類型;
C語言中,凡不加類型說明的函數,一律自動按整型處理,這樣做不會有什麼好處,卻容易被誤解為void類型;
C++語言有很嚴格的類型安全檢查,不允許上述情況發生。由于C++程式可以調用C函數,為了避免混亂,規定任何C++/ C函數都必須有類型。如果函數沒有傳回值,那麼應聲明為void類型
☆ 【規則4.2-2】 函數名字與傳回值類型在語義上不可沖突;
違反這條規則的典型代表是C标準庫函數getchar。
char c;
c = getchar();
if (c == EOF)
按照getchar名字的意思,将變量c聲明為char類型是很自然的事情。但不幸的是getchar的确不是char類型,而是int類型,其原型如下:
int getchar(void);
由于c是char類型,取值範圍是[-128,127],如果宏EOF的值在char的取值範圍之外,那麼if語句将總是失敗,這種“危險”人們一般哪裡料得到!導緻本例錯誤的責任并不在使用者,是函數getchar誤導了使用者
☆ 【規則4.2-3】 不要将正常值和錯誤标志混在一起傳回。正常值用輸出參數獲得,而錯誤标志用return語句傳回;
☆ 【建議4.2-1】 有時候函數原本不需要傳回值,但為了增加靈活性如支援鍊式表達,可以附加傳回值;
例如字元串拷貝函數strcpy的原型:
char *strcpy(char *strDest,const char *strSrc);
strcpy函數将strSrc拷貝至輸出參數strDest中,同時函數的傳回值又是strDest。這樣做并非多此一舉,可以獲得如下靈活性:
int nLength = strlen( strcpy(str, “Hello World”) );
☆ 【建議4.2-2】 如果函數的傳回值是一個對象,有些場合用“引用傳遞”替換“值傳遞”可以提高效率。而有些場合隻能用“值傳遞”而不能用“引用傳遞”,否則會出錯;
對于建議4.2-2,如果函數的傳回值是一個對象,有些場合用“引用傳遞”替換“值傳遞”可以提高效率,而有些場合隻能用“值傳遞”而不能用“引用傳遞”,否則會出錯,例如:
class String
{…
// 指派函數
String & operate=(const String &other);
// 相加函數,如果沒有friend修飾則隻許有一個右側參數
friend String operate+( const String &s1, const String &s2);
private:
char *m_data;
String的指派函數operate = 的實作如下:
String & String::operate=(const String &other)
if (this == &other)
return *this;
delete m_data;
m_data = new char[strlen(other.data)+1];
strcpy(m_data, other.data);
return *this; // 傳回的是 *this的引用,無需拷貝過程
對于指派函數,應當用“引用傳遞”的方式傳回String對象。如果用“值傳遞”的方式,雖然功能仍然正确,但由于return語句要把 *this拷貝到儲存傳回值的外部存儲單元之中,增加了不必要的開銷,降低了指派函數的效率。例如:
String a,b,c;
…
a = b; // 如果用“值傳遞”,将産生一次 *this 拷貝
a = b = c; // 如果用“值傳遞”,将産生兩次 *this 拷貝
String的相加函數operate + 的實作如下:
String operate+(const String &s1, const String &s2)
String temp;
delete temp.data; // temp.data是僅含‘\0’的字元串
temp.data = new char[strlen(s1.data) + strlen(s2.data) +1];
strcpy(temp.data, s1.data);
strcat(temp.data, s2.data);
return temp;
對于相加函數,應當用“值傳遞”的方式傳回String對象。如果改用“引用傳遞”,那麼函數傳回值是一個指向局部對象temp的“引用”。由于temp在函數結束時被自動銷毀,将導緻傳回的“引用”無效。例如:
c = a + b;
此時 a + b 并不傳回期望值,c什麼也得不到,流下了隐患。
不同功能的函數其内部實作各不相同,看起來似乎無法就“内部實作”達成一緻的觀點。但根據經驗,我們可以在函數體的“入口處”和“出口處”從嚴把關,進而提高函數的品質。
☆ 【規則4.3-1】 在函數體的“入口處”,對參數的有效性進行檢查;
很多程式錯誤是由非法參數引起的,我們應該充分了解并正确使用“斷言”(assert)來防止此類錯誤。詳見4.5節“使用斷言”
☆ 【規則4.3-2】 在函數體的“出口處”,對return語句的正确性和效率進行檢查;
注意事項如下:
(1) return語句不可傳回指向“棧記憶體”的“指針”或者“引用”,因為該記憶體在函數體結束時被自動銷毀,例如:
char * Func(void)
char str[] = “hello world”; // str的記憶體位于棧上
return str; // 将導緻錯誤
(2) 要搞清楚傳回的究竟是“值”、“指針”還是“引用”;
(3) 如果函數傳回值是一個對象,要考慮return語句的效率,例如:
return String(s1 + s2);
這是臨時對象的文法,表示“建立一個臨時對象并傳回它”,不要以為它與“先建立一個局部對象temp并傳回它的結果”是等價的,如
String temp(s1 + s2);
return temp;
實質不然,上述代碼将發生三件事。
首先,temp對象被建立,同時完成初始化;
然後拷貝構造函數把temp拷貝到儲存傳回值的外部存儲單元中;
最後,temp在函數結束時被銷毀(調用析構函數)。
然而“建立一個臨時對象并傳回它”的過程是不同的,編譯器直接把臨時對象建立并初始化在外部存儲單元中,省去了拷貝和析構的化費,提高了效率。
類似地,我們不要将
return int(x + y); // 建立一個臨時變量并傳回它
寫成
int temp = x + y;
由于内部資料類型如int,float,double的變量不存在構造函數與析構函數,雖然該“臨時變量的文法”不會提高多少效率,但是程式更加簡潔易讀。
☆ 【建議4.4-1】 函數的功能要單一,不要設計多用途的函數;
☆ 【建議4.4-2】 函數體的規模要小,盡量控制在150行代碼之内;
☆ 【建議4.4-3】 盡量避免函數帶有“記憶”功能。相同的輸入應當産生相同的輸出帶有“記憶”功能的函數,其行為可能是不可預測的,因為它的行為可能取決于某種“記憶狀态”。這樣的函數既不易了解又不利于測試和維護。在C/C++語言中,函數的static局部變量是函數的“記憶”存儲器。建議盡量少用static局部變量,除非必需。
☆ 【建議4.4-4】 不僅要檢查輸入參數的有效性,還要檢查通過其它途徑進入函數體内的變量的有效性,例如全局變量、檔案句柄等;
☆ 【建議4.4-5】 用于出錯處理的傳回值一定要清楚,讓使用者不容易忽視或誤解錯誤情況。
程式一般分為Debug版本和Release版本,Debug版本用于内部調試,Release版本發行給使用者使用。
斷言assert是僅在Debug版本起作用的宏,它用于檢查“不應該”發生的情況。示例4.5是一個記憶體複制函數。在運作過程中,如果assert的參數為假,那麼程式就會中止(一般地還會出現提示對話,說明在什麼地方引發了assert)。
void *memcpy(void *pvTo, const void *pvFrom, size_t size)
assert((pvTo != NULL) && (pvFrom != NULL)); // 使用斷言
byte *pbTo = (byte *) pvTo; // 防止改變pvTo的位址
byte *pbFrom = (byte *) pvFrom; // 防止改變pvFrom的位址
while(size -- > 0 )
*pbTo ++ = *pbFrom ++ ;
return pvTo;
示例4.5 複制不重疊的記憶體塊
assert不是一個倉促拼湊起來的宏。為了不在程式的Debug版本和Release版本引起差别,assert不應該産生任何副作用。是以assert不是函數,而是宏。程式員可以把assert看成一個在任何系統狀态下都可以安全使用的無害測試手段。如果程式在assert處終止了,并不是說含有該assert的函數有錯誤,而是調用者出了差錯,assert可以幫助我們找到發生錯誤的原因。
☆ 【規則4.5-1】 使用斷言捕捉不應該發生的非法情況,不要混淆非法情況與錯誤情況之間的差別,後者是必然存在的并且是一定要作出處理的;
☆ 【規則4.5-2】 在函數的入口處,使用斷言檢查參數的有效性(合法性);
☆ 【建議4.5-1】 在編寫函數時,要進行反複的考查,并且自問:“我打算做哪些假定?”一旦确定了的假定,就要使用斷言對假定進行檢查;
☆ 【建議4.5-2】 一般教科書都鼓勵程式員們進行防錯設計,但要記住這種程式設計風格可能會隐瞞錯誤。當進行防錯設計時,如果“不可能發生”的事情的确發生了,則要使用斷言進行報警。
引用是C++中的概念,初學者容易把引用和指針混淆一起。一下程式中,n是m的一個引用(reference),m是被引用物(referent)。
int m;
int &n = m;
n相當于m的别名(綽号),對n的任何操作就是對m的操作。是以n既不是m的拷貝,也不是指向m的指針,其實n就是m它自己。
引用的一些規則如下:
(1) 引用被建立的同時必須被初始化(指針則可以在任何時候被初始化);
(2) 不能有NULL引用,引用必須與合法的存儲單元關聯(指針則可以是NULL);
(3) 一旦引用被初始化,就不能改變引用的關系(指針則可以随時改變所指的對象)。
以下示例程式中,k被初始化為i的引用。語句k = j并不能将k修改成為j的引用,隻是把k的值改變成為6。由于k是i的引用,是以i的值也變成了6。
int i = 5;
int j = 6;
int &k = i;
k = j; // k和i的值都變成了6;
上面的程式看起來象在玩文字遊戲,沒有展現出引用的價值。引用的主要功能是傳遞函數的參數和傳回值。C++語言中,函數的參數和傳回值的傳遞方式有三種:值傳遞、指針傳遞和引用傳遞。
以下是“值傳遞”的示例程式。由于Func1函數體内的x是外部變量n的一份拷貝,改變x的值不會影響n, 是以n的值仍然是0。
void Func1(int x)
x = x + 10;
int n = 0;
Func1(n);
cout << “n = ” << n << endl; // n = 0
以下是“指針傳遞”的示例程式。由于Func2函數體内的x是指向外部變量n的指針,改變該指針的内容将導緻n的值改變,是以n的值成為10。
void Func2(int *x)
(* x) = (* x) + 10;
Func2(&n);
cout << “n = ” << n << endl; // n = 10
以下是“引用傳遞”的示例程式。由于Func3函數體内的x是外部變量n的引用,x和n是同一個東西,改變x等于改變n,是以n的值成為10。
void Func3(int &x)
Func3(n);
對比上述三個示例程式,會發現“引用傳遞”的性質象“指針傳遞”,而書寫方式象“值傳遞”。
類 型
規 則
範 例
bool(BOOL)
用b開頭
bIsParent
byte(BYTE)
用by開頭
byFlag
short(SHORT)
用n開頭
nFileLen
int(INT)
nStepCount
long(LONG)
用l開頭
lSize
char(CHAR)
用ch開頭
chCount
unsigned short(WORD)
用w開頭
wLength
unsigned long(DWORD)
用dw開頭
dwBroad
void(VOID)
用v開頭
vVariant
用0結尾的字元串
用sz開頭
szFileName
LPCSTR(LPCTSTR)
用str開頭
strString
HANDLE(HINSTANCE)
用h開頭
hHandle
struct
用blk開頭
blkTemplate
BYTE*
用pb開頭
pbValue
WORD*
用pw開頭
pwValue
LONG*
用pl開頭
plValue