代碼風格的重要性怎麼強調都不過分。一段稍長一點的無格式代碼基本上就是不可讀的。
先來看一下這方面的整體原則:
空行的使用 空行起着分隔程式段落的作用。空行得體(不過多也不過少)将使程式的布局更加清晰。空行不會浪費記憶體,雖然列印含有空行的程式是會多消耗一些紙張,但是值得。是以不要舍不得用空行。 在每個類聲明之後、每個函數定義結束之後都要加2行空行。 在一個函數體内,邏揖上密切相關的語句之間不加空行,其它地方應加空行分隔。 語句與代碼行 一行代碼隻做一件事情,如隻定義一個變量,或隻寫一條語句。這樣的代碼容易閱讀,并且友善于寫注釋。 "if"、"for"、"while"、"do"、"try"、"catch" 等語句自占一行,執行語句不得緊跟其後。不論執行語句有多少都要加 "{ }" 。這樣可以防止書寫和修改代碼時出現失誤。 縮進和對齊 程式的分界符 "{" 和 "}" 應獨占一行并且位于同一列,同時與引用它們的語句左對齊。 "{ }" 之内的代碼塊在 "{" 右邊一個制表符(4個半角空格符)處左對齊。如果出現嵌套的 "{ }",則使用縮進對齊。 如果一條語句會對其後的多條語句産生影響的話,應該隻對該語句做半縮進(2個半角空格符),以突出該語句。 例如: void
Function(int x)
{
CSessionLock iLock(mxLock);
for (初始化; 終止條件; 更新)
{
// ...
}
try
{
// ...
}
catch (const exception& err)
{
// ...
}
catch (...)
{
// ...
}
// ...
}
最大長度 代碼行最大長度宜控制在70至80個字元以内。代碼行不要過長,否則眼睛看不過來,也不便于列印(2009年更新:随着GUI開發環境和高分寬屏的普及,此規則可以視情況适當放寬)。 長行拆分 長表達式要在低優先級操作符處拆分成新行,操作符放在新行之首(以便突出操作符)。拆分出的新行要進行适當的縮進,使排版整齊,語句可讀。
例如:
if ((very_longer_variable1 >= very_longer_variable2)
&& (very_longer_variable3 <= very_longer_variable4)
&& (very_longer_variable5 <= very_longer_variable6))
{
DoSomething();
}
空格的使用 關鍵字之後要留白格。象 "const"、"virtual"、"inline"、"case" 等關鍵字之後至少要留一個空格,否則無法辨析關鍵字。象 "if"、"for"、"while"、"catch" 等關鍵字之後應留一個空格再跟左括号 "(",以突出關鍵字。 函數名之後不要留白格,緊跟左括号 "(",以與關鍵字差別。 "(" 向後緊跟。而 ")"、","、";" 向前緊跟,緊跟處不留白格。 "," 之後要留白格,如 Function(x, y, z)。如果 ";" 不是一行的結束符号,其後要留白格,如 for (initialization; condition; update)。 指派操作符、比較操作符、算術操作符、邏輯操作符、位域操作符,如"="、"+=" ">="、"<="、"+"、"*"、"%"、"&&"、"||"、"<<", "^" 等二進制操作符的前後應當加空格。 一進制操作符如 "!"、"~"、"++"、"--"、"&"(位址運算符)等前後不加空格。 象"[]"、"."、"->"這類操作符前後不加空格。 對于表達式比較長的 for、do、while、switch 語句和 if 語句,為了緊湊起見可以适當地去掉一些空格,如 for (i=0; i<10; i++) 和 if ((a<=b) && (c<=d)) 等。 例如: void Func1(int x, int y, int z); // 良好的風格
void Func1 (int x,int y,int z); // 不良的風格
// ===========================================================
if (year >= 2000) // 良好的風格
if(year>=2000) // 不良的風格
if ((a>=b) && (c<=d)) // 良好的風格
if(a>=b&&c<=d) // 不良的風格
// ===========================================================
for (i=0; i<10; i++) // 良好的風格
for(i=0;i<10;i++) // 不良的風格
for (i = 0; I < 10; i ++) // 過多的空格
// ===========================================================
x = a < b ? a : b; // 良好的風格
x=a<b?a:b; // 不好的風格
// ===========================================================
int* x = &y; // 良好的風格
int * x = & y; // 不良的風格
// ===========================================================
array[5] = 0; // 不要寫成 array [ 5 ] = 0;
a.Function(); // 不要寫成 a . Function();
b->Function(); // 不要寫成 b -> Function();
修飾符的位置 為便于了解,應當将修飾符 "*" 和 "&" 緊靠資料類型。
例如:
char* name;
int* x;
int y; // 為避免y被誤解為指針,這裡必須分行寫。
int* Function(void* p);
參見:變量、常量的風格與版式 -> 指針或引用類型的定義和聲明 注釋 注釋的位置應與被描述的代碼相鄰,可以放在代碼的上方或右方,不可放在下方。 邊寫代碼邊注釋,修改代碼同時修改相應的注釋,以保證注釋與代碼的一緻性。不再有用的注釋要删除。 注釋應當準确、易懂,防止注釋有二義性。錯誤的注釋不但無益反而有害。 當代碼比較長,特别是有多重嵌套時,應當在一些段落的結束處加注釋,便于閱讀。 與常量的比較 在與宏、常量進行 "==", "!=", ">=", "<=" 等比較運算時,應當将常量寫在運算符左邊,而變量寫在運算符右邊。這樣可以避免因為偶然寫錯把比較運算變成了指派運算的問題。
例如:
if (NULL == p) // 如果把 "==" 錯打成 "=",編譯器就會報錯
{
// ...
}
為增強代碼的可讀性而定義的宏 以下預定義宏對程式的編譯沒有任何影響,隻為了增加代碼的可讀性: 宏 說明 NOTE 需要注意的代碼 TODO 尚未實作的接口、類、算法等 UNDONE 已取消的接口、類、算法等 FOR_DBG 标記為調試友善而臨時增加的代碼 OK 僅用于調試的标記
例如: TODO class CMyClass;
TODO void Function(void);
FOR_DBG cout << "...";
類/結構 C++編碼規範與指導 類是C++中最重要也是使用頻率最高的新特性之一。類的版式好壞将極大地影響代碼品質。 注釋頭與類聲明 與檔案一樣,每個類應當有一個注釋頭用來說明該類的各個方面。
類聲明換行緊跟在注釋頭後面,"class" 關鍵字由行首開始書寫,後跟類名稱。界定符 "{" 和 "};" 應獨占一行,并與 "class" 關鍵字左對齊。
對于功能明顯的簡單類(接口小于10個),也可以使用簡單的單行注釋頭: //! <簡要說明該類所完成的功能>
class CXXX
{
// ...
};
繼承 基類直接跟在類名稱之後,不換行,通路說明符(public, private, 或 protected)不可省略。如: class CXXX : public CAAA, private CBBB
{
// ...
};
以行為為中心 沒人喜歡上來就看到一大堆私有資料,大多數使用者關心的是類的接口與 它提供的服務,而不是其實作細節。
是以應當将公有的定義和成員放在類聲明的最前面,保護的放在中間,而私有的擺在最後。
通路說明符 通路說明符(public, private, 或 protected)應該獨占一行,并與類聲明中的‘class’關鍵字左對齊。 類成員的聲明版式 對于比較複雜(成員多于20個)的類,其成員必須分類聲明。
每類成員的聲明由通路說明符(public, private, 或 protected)+ 全行注釋開始。注釋不滿全行(80個半角字元)的,由 "/" 字元補齊,最後一個 "/" 字元與注釋間要留一個半角空格符。
如果一類聲明中有很多組功能不同的成員,還應該用分組注釋将其分組。分組注釋也要與 "class" 關鍵字對齊。
每個成員的聲明都應該由 "class" 關鍵字開始向右縮進一個制表符(4個半角空格符),成員之間左對齊。
例如:
class CXXX
{
public:
/// 類型定義
typedef vector<string> VSTR;
public:
/ 構造、析構、初始化
CXXX();
~CXXX();
public:
/// 公用方法
// [[ 功能組1
void Function1(void) const;
long Function2(IN int n);
// ]] 功能組1
// [[ 功能組2
void Function3(void) const;
bool Function4(OUT int& n);
// ]] 功能組2
private:
/// 屬性
// ...
private:
/ 禁用的方法
// 禁止複制
CXXX(IN const CXXX& rhs);
CXXX& operator=(IN const CXXX& rhs);
};
正确地使用const和mutable 把不改變對象邏輯狀态的成員都标記為 const 成員不僅有利于使用者對成員的了解,更可以最大化對象使用方式的靈活性及合理性(比如通過 const 指針或 const 引用的形式傳遞一個對象)。
如果某個屬性的改變并不影響該對象邏輯上的狀态,而且這個屬性需要在 const 方法中被改變,則該屬性應該标記為 "mutable"。
例如:
class CString
{
public:
//! 查找一個子串,find() 不會改變字元串的值 ,是以為 const 函數
int find(IN const CString& str) const;
// ...
private:
// 最後一次錯誤值,改動這個值不會影響對象的邏輯狀态,
// 像 find() 這樣的 const 函數也可能修改這個值
mutable int m_nLastError;
// ...
};
也就是說,應當盡量使所有邏輯上隻讀的操作成為 const 方法,然後使用 mutable 解決那些存在邏輯沖突的屬性。 嵌套的類聲明 在相應的邏輯關系确實存在時,類聲明可以嵌套。嵌套類可以使用簡單的單行注釋頭: // ...
class CXXX
{
//! 嵌套類說明
class CYYY
{
// ...
};
};
初始化清單 應當盡可能通過構造函數的初始化清單來初始化成員和基類。初始化清單至少獨占一行,并且與構造函數的定義保持一個制表符(4個半角空格)的縮進。
例如:
CXXX::CXXXX(IN int nA, IN bool bB)
: m_nA(nA), m_bB(bB)
{
// ...
};
初始化清單的書寫順序應當與對象的構造順序一緻,即:先按照聲明順序寫基類初始化,再按照聲明順序寫成員初始化。
如果一個成員 "a" 需要使用另一個成員 "b" 來初始化,則 "b" 必須在 "a" 之前聲明,否則将會産生運作時錯誤(有些編譯器會給出警告)。
例如:
// ...
class CXXXX : public CAA, public CBB
{
// ...
CYY m_iA;
CZZ m_iB; // m_iA 必須在 m_iB 之前聲明
};
CXXX::CXXXX(IN int nA, IN int nB, IN bool bC)
: CAA(nA), CBB(nB), m_iA(bC), m_iB(m_iA) // 先基類,後成員,
// 分别按照聲明順序書寫
{
// ...
};
内聯函數的實作體 定義在類聲明之中的函數将自動成為内聯函數。但為了使類的聲明更為清晰明了,應盡量避免直接在聲明中直接定義成員函數的程式設計風格。鼓勵使用 "inline" 關鍵字将内聯函數放在類聲明的外部定義。
關于類聲明的例子,請參見:類/結構的風格與版式例子
關于類聲明的模闆,請參見:類聲明模闆
函數 C++編碼規範與指導 函數是程式執行的最小機關,任何一個有效的C/C++程式都少不了函數。 函數原型 函數原型的格式為: [存儲類] 傳回值類型
[名空間或類::]函數名(參數清單) [const說明符] [異常過濾器]
例如: static inline void
Function1(void)
int
CSem::Function2(IN const char* pcName) const throw(Exp)
其中: 以 "[ ]" 包覆的為可選項目。 除了構造/析構函數及類型轉換操作符外,"傳回值類型 " 和 "參數清單" 項不可省略(可以為 "void")。 "const說明符" 僅用于成員函數中 。 "存儲類 ", "參數清單" 和 "異常過濾器" 的說明見下文 。 函數聲明 函數聲明的格式為: 例如: //! 執行某某操作
static void
Function(void);
函數聲明和其它代碼間要有空行分割。
聲明類的成員函數時,為了緊湊,傳回值類型和函數名之間不用換行,也可以适當減少聲明間的空行。
函數定義 函數定義使用如下格式: 對于傳回值、參數意義都很明确的簡單函數(代碼不超過20行),也可以使用單行函數頭: //! 函數實作功能
函數原型
{
// ...
}
函數定義和其它代碼之間至少分開2行空行。 參數描述宏 以下預定義宏對程式的編譯沒有任何影響,隻為了增強對參數的了解: 宏 說明 IN 輸入參數。 OUT 輸出參數。 DUMMY 啞元參數-不使用參數的值,僅為幫助函數重載解析等目的而設定的參數。 OPTIONAL 可選參數-通常指可以為NULL的指針參數,帶預設值的參數不需要這樣标明。 RESERVED 保留參數-這個參數目前未被支援,留待以後擴充;或者該參數為内部使用,使用者無需關心。 OWNER 獲得參數的所有權,調用者不再負責銷毀實參指定的對象;如果用來修飾傳回值,則表示調用者獲得傳回值的所有權,并負責将其銷毀。 UNUSED 标明這個參數在此版本中已不再使用。 CHANGED 參數類型或用途與較早版本相比發生了變化。 ADDED 新增的參數。 NOTE 需要注意的參數-參數意義發生變化或者與習慣用法不同。 WRKBUF 工作緩沖區-為避免頻繁配置設定臨時資源而傳入的臨時工作區。 DEFERRED 表示指定的參數會被延後使用,調用者在目前調用傳回後仍然要保證該參數有效,直到事先在接口中約定的,某個未來的時間點。 TRANSIENT 表示參數指定的對象隻能在調用傳回前使用,調用傳回後該對象可能失效。是以不能保留此對象供以後使用。
其中: 除了空參數 "void" 和啞元參數以外,每個參數左側都必須有 "IN" 和/或 "OUT" 修飾。 既輸入又輸出的參數應記為:"IN OUT",而不是 "OUT IN"。 IN/OUT的左側還可以根據需要加入一個或多個上表中列出的其它宏 。 參數描述宏的使用思想是:隻要一個描述宏可以用在指定參數上(即:對這個參數來說,用這個描述宏修飾它是貼切的),那麼就應當使用它。
也就是說,應該把能用的描述宏都用上,以期盡量具體地描述一個參數的作用和用法等資訊。
參數清單 參數清單的格式為: 參數描述宏1 參數類型1 參數1, 參數描述宏2 參數類型2 參數2, ...
例如: IN const int nCode, OUT string& nName
OWNER IN CDatabase* piDB, OPTIONAL IN OUT int* pnRecordCount = NULL
IN OUT string& stRuleList, RESERVED IN int nOperate = 0
...
其中: "參數描述宏" 見上文 參數命名規範與變量的命名規範 相同 存儲類 "extern", "static", "inline" 等函數存儲類說明應該在聲明和定義中一緻并且顯式地使用。不允許隐式地使用一個類型聲明,也不允許一個類型聲明僅存在于函數的聲明或定義中。 成員函數的存儲類 由于C++語言的限制,類中成員函數的 "static", "virtual", "explicit" 等存儲類說明不允許出現在函數定義中。
但是為了明确起見,這些存儲類應以注釋的形式在相應的成員定義中給出。
例如:
CThread::EXITCODE
CSrvCtl::CWrkTrd::Entry(void)
{
// ...
}
inline void
stringEx::regex_free(IN OUT void*& pRegEx)
{
// ...
}
特别地, 對于在類聲明中直接實作的方法, 可以省略其 "inline" 關鍵字 。而在内聯(.inl)檔案中實作的 inline 方法則不能省略。這是因為 inline linkage 是在聲明時起效的。 預設參數 類似地,參數的預設值隻能出現在函數聲明中,但是為了明确起見,這些預設值應以注釋的形式在定義中給出。
例如:
bool
stringEx::regex_find(OUT VREGEXRESULT& vResult,
IN stringEx stRegEx,
IN size_t nIndex ,
IN size_t nStartPos ,
IN bool bNoCase ,
IN bool bNewLine ,
IN bool bExtended ,
IN bool bNotBOL ,
IN bool bNotEOL ,
IN bool bUsePerlStyle ) const
{
// ...
}
異常過濾器 對于任何可能抛出異常的函數,必須在其聲明和定義中顯式地指定異常過濾器,并在過濾器中列舉該函數可能抛出的異常。
例如:
int
Function(IN const char* pcName) throw(byExp, exception);
如果一個函數本身及其直接調用的函數不會顯式抛出異常(沒有指定異常過濾器),那麼該函數可以省略異過濾器。
特别地:如果一個函數内部顯式地捕獲了任何可能的異常(例如:使用了 "catch (...)" ),并且保證不抛出任何異常,那麼應該在其聲明和定義中顯式地指定一個空異常過濾器:"throw()"。
例如:
int
Function(IN const char* pcName) throw();
特别地:程序入口函數("main()")不應當使用異常過濾器。
除了空異常過濾器有時可幫助編譯器完成一些優化以外,異常過濾器的主要作用反倒是為程式員提供函數出錯時的行為描述。這些資訊對于函數的使用者來說十分有用。是以,即使僅作為一種文檔性質的措施,異常過濾器也應當被保留下來。
代碼段注釋 如果函數體中的代碼較長,應該根據功能不同将其分段。代碼段間以空行分離,并且每段代碼都以代碼段分割注釋作為開始。
例如:
void
CXXX::Function(IN void* pmodAddr)
{
if (NULL == pmodAddr)
{
return;
}
{ CSessionLock iLock(mxLock);
// =====================================================================
// = 判斷指定子產品是不是剛剛被裝入,由于在NT系列平台中,“A”系列函數都是
// = 由“W”系列函數實作的。 是以可能會有一次LoadLibrary産生多次本函數調
// = 用的情況。為了增加效率,特設此靜态變量判斷上次調用是否與本次相同。
static PVOID pLastLoadedModule = NULL;
if (pLastLoadedModule == pmodAddr)
{
return; // 相同,忽略這次調用
}
pLastLoadedModule = pmodAddr;
// =====================================================================
// = 檢查這個子產品是否在旁路子產品表中
stringEx stModName;
if (!BaiY_IMP::GetModuleNameByAddress(pmodAddr, stModName))
{
return;
}
if (CHookProc::sm_sstByPassModTbl.find(stModName)
!= CHookProc::sm_sstByPassModTbl.end())
{
return;
}
// =====================================================================
// = 在這個子產品中HOOK所有存在于HOOK函數表中的函數
PROCTBL::iterator p = sm_iProcTbl.begin();
for (; p!=sm_iProcTbl.end(); ++p)
{
p->HookOneModule(pmodAddr);
}
} // SessionLock
}
明顯地,如果需要反複用到一段代碼的話,這段代碼就應當 被抽取成一個函數。
當一個函數過長(超過100行)或代碼的意圖不明确時,為了便于閱讀和了解,也應當将其中的一些代碼段實作為單獨的函數。
特别地,對由于如加密及性能優化等特殊原因無法提取為一個單獨函數的代碼段,應當使用特别代碼段注釋顯式分割。當然,類似情況應當盡量使用内聯函數或編譯器提供的強制性内聯函數代替。
例如:
void
CXXX::Function(void)
{
// ...
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@ 擷取首網卡的 MAC 位址
typedef CTmpHandle<IP_ADAPTER_INFO, FreeDeletor<IP_ADAPTER_INFO> >
THADAPTERINFO;
byteEx btAddr;
THADAPTERINFO thAdapterInfo;
thAdapterInfo = (IP_ADAPTER_INFO*) malloc(sizeof(IP_ADAPTER_INFO));
ULONG ulOutBufLen = sizeof(IP_ADAPTER_INFO);
// Make an initial call to GetAdaptersInfo to get
// the necessary size into the ulOutBufLen variable
if (ERROR_SUCCESS != ::GetAdaptersInfo(thAdapterInfo, &ulOutBufLen))
{
thAdapterInfo = (IP_ADAPTER_INFO*) malloc(ulOutBufLen);
}
if (NO_ERROR != ::GetAdaptersInfo(thAdapterInfo, &ulOutBufLen))
{
#ifdef DEBUG
CLog::DebugMsg(byT("lic verifier"), byT("failed verifying license"));
#endif
CProcess::Exit(-97);
}
btAddr.assign(thAdapterInfo->Address, thAdapterInfo->AddressLength);
// @@ 擷取首網卡的 MAC 位址
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// ...
}
調用系統API 所有系統API調用前都要加上全局名稱解析符 "::"。
例如:
::MessageBoxA(NULL, gcErrorMsg, "!FATAL ERROR!", MB_ICONSTOP|MB_OK);
if (0 == ::GetTempFileName(m_basedir.c_str(), byT("bai"), 0, stR.ref()))
{
// ...
}
讓相同的代碼隻出現一次 為了使程式更容易調試、修改,盡量降低日後維護的複雜性,應該把需要在一個以上位置使用的代碼段封裝成函數。哪怕這段代碼很短,為了以後維護友善着想,也應當将其封裝為内聯函數。
關于函數的例子,請參見:函數的風格與版式例子
關于函數的模闆,請參見:函數模闆
變量、常量 C++編碼規範與指導 聲明格式 變量、常量的聲明格式如下: 其中: 以 "[ ]" 包覆的為可選項目。 "存儲類 " 的說明見下文 定義格式 變量、常量的定義格式如下: 其中: 以 "[ ]" 包覆的為可選項目。 "存儲類 " 的說明見下文 存儲類 除 "auto" 類型以外,諸如 "extern", "static", "register", "volatile" 等存儲類均不可省略,且必須在聲明和定義中一緻地使用(即:不允許僅在聲明或定義中使用)。
特别地,由于 "auto" 關鍵字在 C++11 中已被用作類型推斷,是以聲明局部變量時,"auto" 存儲類必須被省略。
成員變量的存儲類 由于C++語言的限制,成員變量的 "static" 等存儲類說明不允許出現在變量定義中。
但是為了明确起見,這些存儲類應以注釋的形式在定義中給出。
例如:
int CThread::sm_nPID = 0;
指針或引用類型的定義和聲明 在聲明和定義多個指針或引用變量/常量時,每個變量至少占一行。例如: int* pn1,
* pn2 = NULL,
* pn3;
char* pc1;
char* pc2;
char* pc3;
// 錯誤的寫法:
int* pn11, *pn12, *pn13;
常指針和指針常量 聲明/定義一個常指針(指向常量的指針)時,"const" 關鍵字一律放在類型說明的左側。
聲明/定義一個指針常量(指針本身不能改變)時,"const" 關鍵字一律放在變量左側、類型右側。
例如:
const char* pc1; // 常指針
char* const pc2; // 指針常量
const char* const pc3; // 常指針常量
// 錯誤的寫法:
char const* pc1; // 與 const char* pc1 含義相同,但不允許這樣寫
全局變量、常量的注釋 全局變量、常量的注釋獨占一行,并用 "//!" 開頭。
例如:
//! 目前程序的ID
static int sg_nPID = 0;
//! 分割符
static const char* pcDTR = "\\/";
類型轉換 禁止使用C風格的 "(類型)" 類型轉換,應當優先使用C++的 "xxx_cast" 風格的類型轉換。C++風格的類型轉換可以提供豐富的含義和功能,以及更好的類型檢查機制,這對代碼的閱讀、修改、除錯和移植有很大的幫助。其中: static_cast static_cast 用于編譯器認可的,安全的靜态轉換,比如将 "char" 轉為 "int" 等等。該操作通常在編譯時完成,但有可能調用使用者定義的類型轉換操作或非 explicit 的單參(或至少從第二個參數開始帶預設值的)構造函數 。 reinterpret_cast reinterpret_cast 用于編譯器不認可的,不安全的靜态轉換,比如将 "int*" 轉為 "int" 等等。這種轉換有可能産生可移植性方面的問題,該操作在編譯時完成。(注意:reinterpret_cast 比 C 風格的類型轉換還要野蠻,它不進行任何位址對齊和調整,也不調用任何使用者定義的類型轉換操作) const_cast const_cast 用于将一個常量轉化為相應類型的變量,比如将 "const char*" 轉換成 "char*" 等等。這種轉換可能伴随潛在的錯誤。該操作在編譯時完成 。 dynamic_cast dynamic_cast 是 C++ RTTI 機制的重要展現,用于在類層次結構中漫遊。dynamic_cast 可以對指針和引用進行自由度很高的向上、向下和交叉轉換。被正确使用的 dynamic_cast 操作将在運作時完成。反之,若編輯器關閉了 RTTI 支援,或被轉換的類層次結構中沒有抽象類存在,則此操作在編譯時完成(有些編譯器會給出警告)。
此外,對于定義了單參構造函數或類型轉換操作的類來說,應當優先使用構造函數風格的類型轉換,如:'string("test")' 等等。
通常來說,"xxx_cast" 格式的轉換與構造函數風格的類型轉換之間最大的差別在于:構造函數風格的轉換經常會生成新的臨時對象,可能伴随相當的時間和空間開銷。而 "xxx_cast" 格式的轉換隻是告訴編譯器,将指定記憶體中的資料當作另一種類型的資料看待,這些操作一般在編譯時完成,不會對程式的運作産生額外開銷。當然,"dynamic_cast" 和某些 "static_cast" 則例外。
參見:RTTI、虛函數和虛基類的開銷分析和使用指導
枚舉、聯合、typedef C++編碼規範與指導 枚舉、聯合的定義格式 枚舉、聯合的定義格式為: //! 說明(可選)
enum|union 名稱
{
内容 // 注釋(可選)
};
例如: //! 服務的狀态
enum SRVSTATE
{
SRV_INVALID = 0, // 無效(尚未啟動)
SRV_STARTING = 1,
SRV_STARTED,
SRV_PAUSING,
SRV_PAUSED,
SRV_STOPPING,
SRV_STOPPED
};
//! 32位整數
union INT32
{
unsigned char cByte[4];
unsigned short nShort[2];
unsigned long nFull;
};
typedef的定義格式 typedef 的定義格式為: //! 說明(可選)
typedef 原類型 類型别名;
例如: //! 傳回值類型
typedef int EXITCODE;
//! 字元串數組類型
typedef vector<string> VSTR;
宏 C++編碼規範與指導 宏是C/C++編譯環境提供給使用者的,在編譯開始前(編譯預處理階段)執行的唯一可程式設計邏輯。 何時使用宏 應當盡量減少宏的使用,在所有可能的地方都使用常量、模版和内聯函數來代替宏。 邊界效應 使用宏的時候應當注意邊界效應,例如,以下代碼将會得出錯誤的結果: #define PLUS(x,y) x+y
cout << PLUS(1,1) * 2;
以上程式的執行結果将會是 "3",而不是 "4",因為 "PLUS(1,1) * 2" 表達式将會被展開為:"1 + 1 * 2"。
是以在定義宏的時候,隻要允許,就應該為它的替換内容括上 "( )" 或 "{ }"。例如:
#define PLUS(x,y) (x+y)
#define SAFEDELETE(x) {delete x; x=0}
對複雜的宏實行縮進 有時為了實作諸如:對編譯器和目标平台自适應;根據使用者選項編譯不同子產品等機制,需要使用大量較為複雜的宏定義塊。在宏比較複雜(代碼塊多于5行)的地方,為了便于閱讀和了解,應當遵循與普通C++代碼相同的原則進行縮進和排版。
為了差別于其他語句和便于閱讀,宏語句的 "#" 字首不要與語句本身一起縮進,例如:
//! Windows
#if defined(__WIN32__)
# if defined(__VC__) || defined(__BC__) || defined(__GNUC__) // ...
# define BAIY_EXPORT __declspec(dllexport)
# define BAIY_IMPORT __declspec(dllimport)
# else // 編譯器不支援 __declspec()
# define BAIY_EXPORT
# define BAIY_IMPORT
# endif
//! OS/2
#elif defined(__OS2__)
# if defined (__WATCOMC__)
# define BAIY_EXPORT __declspec(dllexport)
# define BAIY_IMPORT
# elif !(defined(__VISAGECPP__) && (__IBMCPP__<400 || __IBMC__<400))
# define BAIY_EXPORT _Export
# define BAIY_IMPORT _Export
# endif
//! Macintosh
#elif defined(__MAC__)
# ifdef __MWERKS__
# define BAIY_EXPORT __declspec(export)
# define BAIY_IMPORT __declspec(import)
# endif
// Others
#else
# define BAIY_EXPORT
# define BAIY_IMPORT
#endif
名空間 C++編碼規範與指導 名空間的使用 名空間可以避免名字沖突、分組不同的接口以及簡化命名規則。應當盡可能地将所有接口都放入适當的名字空間中。 将實作和界面分離 提供給使用者的界面和用于實作的細節應當分别放入不同的名空間中。
例如:如果将一個軟體子產品的所有接口都放在名空間 "MODULE" 中,那麼這個子產品的所有實作細節就可以放入名空間 "MODULE_IMP" 中,或者 "MODULE" 内的 "IMP" 中。
異常 C++編碼規範與指導 異常使C++的錯誤處理更為結構化;錯誤傳遞和故障恢複更為安全簡便;也使錯誤處理代碼和其它代碼間有效的分離開來。 何時使用異常 異常機制隻用在發生錯誤的時候,僅在發生錯誤時才應當抛出異常。這樣做有助于錯誤處理和程式動作兩者間的分離,增強程式的結構化,還保證了程式的執行效率。
确定某一狀況是否算作錯誤有時會很困難。比如:未搜尋到某個字元串、等待一個信号量逾時等等狀态,在某些情況下可能并不算作一個錯誤,而在另一些情況下可能就是一個緻命錯誤。
有鑒于此,僅當某狀況必為一個錯誤時(比如:配置設定存儲失敗、建立信号量失敗等),才應該抛出一個異常。而對另外一些模棱兩可的情況,就應當使用傳回值等其它手段報告 。
此外,在發生錯誤的位置,已經能夠獲得足夠的資訊處理該錯誤的情況不屬于異常,應當對其就地處理。隻有無法獲得足夠的資訊來處理發生的錯誤時,才應該抛出一個異常。
用異常代替goto等其它錯誤處理手段 曾經被廣泛使用的傳統錯誤處理手段有goto風格和do...while風格等,以下是一個goto風格的例子: C++編碼規範與指導 //! 使用goto進行錯誤處理的例子
bool
Function(void)
{
int nCode, i;
bool r = false;
// ...
if (!Operation1(nCode))
{
goto onerr;
}
try
{
Operation2(i);
}
catch (...)
{
r = true;
goto onerr;
}
r = true;
onerr:
// ... 清理代碼
return r;
}
由上例可見,goto風格的錯誤處理至少存在問題如下: 錯誤處理代碼和其它代碼混雜在一起,使程式不夠清晰易讀 。 函數内的變量必須在第一個 "goto" 語句之前聲明,違反就近原則。 多處跳轉的使用破壞程式的結構化,影響程式的可讀性,使程式容易出錯 。 對每個會抛出異常的操作都需要用額外的 try...catch 塊檢測和處理。 稍微複雜一點的分類錯誤處理要使用多個标号和不同的goto跳轉(如: "goto onOp1Err", "goto onOp2Err" ...)。這将使程式變得無法了解和錯誤百出。 再來看看 do...while 風格的錯誤處理: C++編碼規範與指導 //! 使用do...while進行錯誤處理的例子
bool
Function(void)
{
int nCode, i;
bool r = false;
// ...
do
{
if (!Operation1(nCode))
{
break;
}
do
{
try
{
Operation2(i);
}
catch (...)
{
r = true;
break;
}
} while (Operation3())
r = true;
} while (false);
// ... 清理代碼
return r;
}
與 goto 風格的錯誤處理相似,do...while 風格的錯誤處理有以下問題: 錯誤處理代碼和其它代碼嚴重混雜,使程式非常難以了解 。比如上例中的外層循環用于錯誤處理,而内層的 do...while 則是正常的業務邏輯。 需要進行分類錯誤處理時非常困難,通常需要事先設定一個标志變量,并在清理時使用 "switch case" 語句進行分檢。 對每個會抛出異常的操作都需要用額外的 try...catch 塊檢測和處理 。 此外,還有一種更糟糕的錯誤處理風格——直接在出錯位置就地完成錯誤處理: C++編碼規範與指導 //! 直接進行錯誤處理的例子
bool
Function(void)
{
int nCode, i;
// ...
if (!Operation1(nCode))
{
// ... 清理代碼
return false;
}
try
{
Operation2(i);
}
catch (...)
{
// ... 清理代碼
return true;
}
// ...
// ... 清理代碼
return true;
}
這種錯誤處理方式所帶來的隐患可以說是無窮無盡,這裡不再列舉。
與傳統的錯誤處理方法不同,C++的異常機制很好地解決了以上問題。使用異常完成出錯處理時,可以将大部分動作都包含在一個try塊中,并以不同的catch塊捕獲和處理不同的錯誤:
//! 使用異常進行錯誤處理的例子
bool
Function(void)
{
int nCode, i;
bool r = false;
try
{
if (!Operation1(nCode))
{
throw false;
}
Operation2(i);
}
catch (bool err)
{
// ...
r = err;
}
catch (const excption& err)
{
// ... excption類錯誤處理
}
catch (...)
{
// ... 處理其它錯誤
}
// ... 清理代碼
return r;
}
以上代碼示例中,錯誤處理和動作代碼完全分離,錯誤分類清晰明了,好處不言而喻。 構造函數中的異常 在構造函數中抛出異常将中止對象的構造,這将産生一個沒有被完整構造的對象。
對于C++來說,這種不完整的對象将被視為尚未完成建立動作而不被認可,也意味着其析構函數永遠不會被調用。這個行為本身無可非議,就好像警察局不會為一個被流産的嬰兒發戶口然後再開個死亡證明書一樣。但有時也會産生一些問題,例如:
class CSample
{
// ...
char* m_pc;
};
CSample::CSample()
{
m_pc = new char[256];
// ...
throw -1; // m_pc将永遠不會被釋放
}
CSample::~CSample() // 析構函數不會被調用
{
delete[] m_pc;
}
解決這個問題的方法是在抛出異常以前釋放任何已被申請的資源。一種更好的方法是使用一個滿足“資源申請即初始化(RAII)”準則的類型(如:句柄類、靈巧指針類等等)來代替一般的資源申請與釋放方式,如: templete <class T>
struct CAutoArray
{
CAutoArray(T* p = NULL) : m_p(p) {};
~CAutoArray() {delete[] m_p;}
T* operator=(IN T* rhs)
{
if (rhs == m_p)
return m_p;
delete[] m_p;
m_p = rhs;
return m_p;
}
// ...
T* m_p;
};
class CSample
{
// ...
CAutoArray<char> m_hc;
};
CSample::CSample()
{
m_hc = new char[256];
// ...
throw -1; // 由于m_hc已經成功構造,m_hc.~CAutoPtr()将會
// 被調用,是以申請的記憶體将被釋放
}
注意:上述CAutoArray類僅用于示範,對于所有權語義的通用自動指針,應該使用C++标準庫中的 "auto_ptr" 模闆類。對于支援引用計數和自定義銷毀政策的通用句柄類,可以使用白楊工具庫中的 "CHandle" 模闆類。 析構函數中的異常 析構函數中的異常可能在2種情況下被抛出: 對象被正常析構時。 在一個異常被抛出後的退棧過程中——異常處理機制退出一個作用域,其中所有對象的析構函數都将被調用。 由于C++不支援異常的異常,上述第二種情況将導緻一個緻命錯誤,并使程式中止執行。例如: class CSample
{
~CSample();
// ...
};
CSample::~CSample()
{
// ...
throw -1; // 在 "throw false" 的過程中再次抛出異常
}
void
Function(void)
{
CSample iTest;
throw false; // 錯誤,iTest.~CSample()中也會抛出異常
}
如果必須要在析構函數中抛出異常,則應該在異常抛出前用 "std::uncaught_exception()" 事先判斷目前是否存在已被抛出但尚未捕獲的異常。例如: // uncaught_exception() 函數在這個頭檔案中聲明
#include <exception>
class CSample
{
~CSample();
// ...
};
CSample::~CSample()
{
// ...
if (!std::uncaught_exception()) // 沒有尚未捕獲的異常
{
throw -1; // 抛出異常
}
}
void
Function(void)
{
CSample iTest;
throw false; // 可以,iTest.~CSample()不會抛出異常
}
new 時的異常 C++ 标準(ISO/IEC 14882:2003)第 15.2 節中明确規定,在使用 new 或 new[] 操作建立對象時,如對象的構造函數抛出了異常,則該對象的所有成員和基類都将被正确析構,如果存在一個與使用的 operator new 嚴格比對的 operator delete,則為這個對象所配置設定的記憶體也會被釋放。例如: class CSample
{
CSample() { throw -1; }
static void* operator new(IN size_t n)
{ return malloc(n); }
static void operator delete(IN void* p)
{ free(p); }
static void* operator new(IN size_t n, IN CMemMgr& X)
{ return X.Alloc(n); } // 缺少比對的 operator delete
};
void
Function(void)
{
CSample* p1 = new CSample; // 有比對的 operator delete,為 p1 配置設定的記憶體會被釋放
CSample* p2 = new(iMyMemMgr) CSample; // 沒有比對的 operator delete,記憶體洩漏!為 p2 配置設定的記憶體永遠不會被釋放
}
// 編譯器實際生成的代碼像這樣:
void
Function(void)
{
CSample* p1 = CSample::operator new(sizeof(CSample));
try { p1->CSample(); } catch(...) {CSample::opertaor delete(p1); throw; }
CSample* p2 = CSample::operator new(sizeof(CSample), iMyMemMgr);
p2->CSample();
}
這裡順便提一句,delete 操作隻會比對普通的 operator delete(即:全局或類中的 operator delete(void*) 和類中的 operator delete(void*, size_t)),如果像上例中的 p2 那樣使用了一個高度自定義的 operator new,使用者就需要自己完成析構和釋放記憶體的動作,例如: // ...
p2->~CSample();
CSample::operator delete(p2, iMymemMgr);
delete 時的異常 C++ 标準中明确規定,如果在一個析構函數中中途傳回(不管通過 return 還是 throw),該析構函數不會立即傳回,而是會逐一調用所有成員和基類的析構函數後才會傳回。但是标準中并沒有說明如果這個異常是在 delete 時發生的(即:該對象是由 new 建立的),此對象本身所占用的堆存儲是否會被釋放(即:在 delete 時析構函數抛出異常會不會調用 operator delete 釋放這個對象占用的記憶體)。
在實際情況中,被 delete 的對象析構函數抛出異常後,GCC、VC 等流行的 C++ 編譯器都不會自動調用 operator delete 釋放對象占用的記憶體。這種與 new 操作不一緻的行為,其背後的理念是:在構造時抛出異常的對象尚未成功建立,系統應當收回事先為其配置設定的資源;而析構時抛出異常的對象并未成功銷毀,系統不能自動回收它使用的記憶體(意即:系統僅自動回收确定完全無用的資源)。
例如:如果一個對象在構造時申請了系統資源(比如:打開了一個裝置)并保留了 相應的句柄,但在析構時歸還該資源失敗(例如:關閉裝置失敗),則自動調用 operator delete 會丢失這個尚未關閉的句柄,導緻使用者永遠失去向系統歸還資源或者執行進一步錯誤處理的機會。反之,如果這個對象在構造時就沒能成功地申請到相應資源,則自動回收預配置設定給它的記憶體空間是安全的,不會産生任何資源洩漏。
但是應當注意到,如果一個對象在析構時抛出了異常,則這個對象很可能已經處于一個不完整 、不一緻的狀态。此時通路該對象中的任何非靜态成員都是不安全的。是以,應當在被抛出的異常中包含完成進一步處理的足夠資訊 (比如:關閉失敗的句柄)。這樣捕獲到這個異常的使用者就可以安全地釋放該對象占用的記憶體, 僅依靠異常對象完成後續處理。例如:
//! delete 時異常處理的例子
void
Function(void)
{
CSample* p1 = new CSample;
// ...
try
{
delete p1;
}
catch (const sampleExp& err)
{
CSample::operator delete(p1); // 釋放 p1 所占用的記憶體
// 使用 err 對象完成後續的錯誤處理...
}
}
異常的組織 異常類應該以繼承的方式組織成一個層次結構,這将使以不同粒度分類處理錯誤成為可能。
通常,某個軟體生産組織的所有異常都從一個公共的基類派生出來。而每個類的異常則從該類所屬子產品的公共異常基類中派生。例如:
C++編碼規範與指導 異常捕獲和重新抛出 異常捕獲器的書寫順序應當由特殊到一般(先子類後基類),最後才是處理所有異常的捕獲器("catch(...)")。否則将使某些異常捕獲器永遠不會被執行。 為避免捕獲到的異常被截斷,異常捕獲器中的參數類型應當為常引用型或指針型。 在某級異常捕獲器中無法被徹底處理的錯誤可以被重新抛出。重新抛出采用一個不帶運算對象的 "throw" 語句。重新抛出的對象就是剛剛被抛出的那個異常,而不是處理器捕獲到的(有可能被截斷的)異常。 例如: try
{
// ...
}
// 公鑰加密錯誤
catch (const CPubKeyCipher::Exp& err)
{
if (可以恢複)
{
// 恢複錯誤
}
else
{
// 完成能做到的事情
throw; // 重新抛出
}
}
// 處理其它加密庫錯誤
catch (const CryptoExp& err)
{
// ...
}
// 處理其它本公司子產品抛出的錯誤
catch (const CompanyExp& err)
{
// ...
}
// 處理 dynamic_cast 錯誤
catch (const bad_cast& err)
{
// ...
}
// 處理其它标準庫錯誤
catch (const exception& err)
{
// ...
}
// 處理所有其它錯誤
catch (...)
{
// 完成清理和日志等基本處理...
throw; // 重新抛出
}
異常和效率 對于絕大部分現代編譯器來說,在不抛出異常的情況下,異常處理的實作在運作時幾乎不會有任何額外開銷。相反,很多時候,異常機制比傳統的通過傳回值判斷錯誤的開銷還來得稍微小些。
相對于函數傳回和調用的開銷來講,異常抛出和捕獲的開銷通常會大一些。不過錯誤處理代碼通常不會頻繁調用,再說傳統的錯誤處理方式也不是沒有代價的。是以錯誤處理時開銷稍大一點基本上不是什麼問題。這也是我們提倡僅将異常用于錯誤處理的原因之一。
更多關于實作細節和效率的讨論,參見:C++異常機制的實作方式和開銷分析 和 RTTI、虛函數和虛基類的開銷分析和使用指導 等小節。
修改标記 C++編碼規範與指導 在代碼交叉審查,或使用帶完整源代碼的第三方庫時,經常需要為某些目的修改源碼。這時應當為被改動的部分添加修改标記。 何時使用修改标記 修改标記通常僅用于修改者不是被修改子產品(或項目)的主要作者時,但也可以用于在調試、重構或添加新特性時進行臨時标注。
在交叉審查中使用的修改标記,當原作者已經确認并将其合入主要版本之後,應當予以消除,以避免由于多次交叉審查累積的标記混亂。但是相應的修改應當記入檔案頭的修改記錄中。
修改标記的格式 修改标記分為單行标記和段落标記兩種,單行标記用于訓示對零星的單行代碼進行的修改,段落标記則用于指出對一組任意長度的代碼作出的修改。它們的格式如下: // 單行标記:
// code ...; // by <修改者> - <目的> [@ YYYY-MM-DD(可選的修改日期)]
// 段落标記:
// [[ by <修改者> - <目的> [@ YYYY-MM-DD(可選的修改日期)]
// 詳細說明(可選,可多行)
// ... // 被修改的代碼段落
// ]] [by <修改者>]
注意段落标記結尾的 "by <修改者>" 字段是可選的。
此外,在比較混亂或較長的代碼段中,可以将段落開始("// [[")和段落結束("// ]]")标記擴充層次結構更為明顯的:"// ---- [[" 和 "// ---- ]]"
例如:
// [[ by BaiYang - limit @ 2005-03-29
// add pre compile and delay binding support to "limit [s,]n".
void setStatementLimit(dbQuery const& q) {
// ...
}
// ]]
// ...
// ---- [[ by Mark - multithread
void dbCompiler::compileLimitPart(dbQuery& query)
{
// ...
int4* lp1 = INVPTR; // by BaiYang - limit
switch (scan())
{
case tkn_iconst:
// ...
}
// ---- ]] by Mark
修改标記的語言 修改标記當中的說明性文字應當盡量選擇與被修改項目一緻的語言書寫。例如在全英文的項目中應當盡量避免添加中文注釋。
否則能完全看懂修改後項目的程式員将會被限制于同時掌握多種自然語言的人。