版權所有 ? 1997 Rational Software Corporation。 保留所有權利。
“Rational”一詞和 Rational 產品名是 Rational Software Corporation 的商標。涉及到其他公司及其產品時將使用各自公司的商標且僅限于此目的。
此文檔由來自 Calypso Software Inc. (Vancouver, B.C., Canada) 的 Luan Doan-Minh 為 Rational Software Corp. 編寫。
目錄
第一章
簡介
大型軟件項目通常由相應的大型開發團隊承擔。大型團隊生成的代碼要有項目范圍內可評測的質量,代碼必須遵從于某一標準并以此來評價。因此,對大型的項目團隊來說,建立一個編程的標準或一組指南很重要。 使用編程標準也使以下各項成為可能:
- 增加開發過程代碼的強壯性、可讀性、易維護性;減少有經驗和無經驗開發人員編程所需的腦力工作;
- 在項目范圍內統一代碼風格;
- 通過人為以及自動的方式對最終軟件應用質量標準;
- 使新的開發人員快速適應項目氛圍;
- 支持項目資源的復用:允許開發人員從一個項目區域(或子項目團隊)移動到另一個,而不需要重新適應新的子項目團隊的氛圍。
本文的目的是表述 C++ 編程的規則、指南和提示(通常也稱之為指南),它們可用來作為編程標準的基礎。對工作在大型項目團隊的軟件工程師,這些都是需要的。 當前版本專注于程序的編制(雖然現在還很難在設計和編程間劃定明確的界限);以后將會添加設計指南。 指南包括了以下 C++ 開發的方面:
- 如何組織項目代碼;
- 編程風格(如何編寫實際的源代碼);
- 如何記錄源代碼;
- 代碼內名稱和源文件所使用的命名約定;
- 何時使用某些語言結構以及何時應避免某些語言結構。
它們從大量的行業知識中搜集而來。(請參見參考文獻:作者及參考文獻。)它們基于:
- 廣為人知的軟件原理;
- “好的”軟件實現;
- 所學課程;
- 主觀意愿。
大多會基于幾個第一種類型,也會基于大量的第二種和第三種類型。不幸的是,某些也基于最后一種類型;這主要是因為編程是一個高度主觀的活動:編程沒有普遍接受的最好或正確的萬靈藥。
基本原則
清晰、可理解的 C++ 源代碼是規則和指南的主要目標:清晰、可理解的源代碼是軟件可靠性和可維護性的主要作用因素。清晰、可理解的代碼可以表示為以下三個簡單的基礎原理 [Kruchten, 94]。 最小混淆 - 它的生存期中,源代碼的讀遠比寫多,規約更是這樣。理想情況下,源代碼讀起來應該象英語一樣描述了所要做的事,這同時還帶來了它執行的好處。程序更多是為人編寫,而不是為計算機而編寫。閱讀代碼是一個復雜的腦力過程,它可由統一標準來簡化,在本文中還指最小混淆原則。整個項目中統一樣式是軟件開發團隊在編程標準上達成一致的主要原因,它不應視為一種懲罰或對創造性和生產力的阻礙。 維護的唯一點 - 只要可能,設計決策就應在源中只表述一點,它的多數后果應程序化的派生于此點。不遵守這一原則嚴重損害了可維護性、可靠性和可理解性。 最小干擾 - 最終,應用最小干擾原則(它是易讀性的主要作用因素)。即,避免將源代碼與可視干擾(如內容較少或對理解軟件目的不起作用的信息)相混合: 指南這里所要表達的精神是不要過于苛刻;而對正確安全的使用語言特性提供指導。優秀軟件的關鍵在于: 了解每一個特性以及它的限制和潛在的危險; 確切了解此特性可安全的使用于哪一個環境中; 做出使用高度可視特性的決定; 在合適的地方小心適度的使用特性。
前提
這里的指南只有少數幾個基本前提: 讀者了解 C++。 任何有益的地方都鼓勵使用 C++ 的高級特性,而不是只允許使用一些程序員都不熟悉的低級特性。這是項目能從使用 C++ 中獲益的唯一方式。C++ 不應只當成 C 來使用,事實上 C++ 的面向對象特性使它不會象 C 一樣使用。不鼓勵將代碼直譯成注釋;相反,在任何可能的地方應當使用源代碼代替注釋。 遵從大型項目的做法。 即使只是為了項目一級或公司一級的實現和統一,大型項目中許多規則仍很有價值,它們在小型系統中也有使用。 代碼遵從于一個面向對象設計。 許多規則都會支持從面向對象 (OO) 概念到 C++ 特性和具體命名約定的系統映射。
指南分類
指南具有不同的重要性;其重要性由以下標準來衡量:
提示: 以上符號所確定的指南只是一個小提示或小建議,即使忽略它也沒關系。 建議: 以上符號所確定的指南是一個建議,它通常是建立在更加技術性的基礎之上:封裝、內聚、耦合、可移植性或可復用性與某些實現中的性能一樣都會受到影響。除非有合適的理由,否則必須遵從建議。
要求或限制: 以上符號確定的一個指南就是一個要求或限制;不遵從它肯定會導致壞的、不可靠的或不可移植的代碼。沒有棄權就不能違反要求或限制
最根本的原則
使用常識
當無法找到可用的規則或指南時;當規則明顯不適用時;或當其他都已失敗時:使用常識,并核查基本原則。這條規則勝過其它所有的規則。即使存在規則或指南時也需要常識。
第二章
代碼組織與風格
本章提供程序結構與層次的指導。
代碼結構
開發大型系統時通常將它分成許多小的功能子系統。子系統本身由許多代碼模塊組成。在 C++ 中,一個模塊通常包含了實現單一一個,少數情況下,也可能是一組緊密相關的抽象。C++ 中,一個抽象通常實現為一個類。一個類有兩個不同的構件:一個是對類客戶可見的接口,它提供了類能力和責任的聲明或規約;另一個是所聲明規約(類定義)的實現。 與類相似,模塊也有它的接口和實現:模塊接口包括對所包含模塊抽象(類聲明)的規約;模塊實現包括對抽象(類定義)的實際實現。 在系統構造時,可將子系統組織成協作的組或層來減少或控制它們間的依賴性。
不同文件中放置模塊規約與實現
模塊的規約應放置在與模塊實施文件不同的文件中,此文件指頭文件。模塊的實施文件可能放置于一個或多個實施文件中。 如果模塊實現包括擴展內嵌函數、普通私有實現聲明、測試代碼或具體平臺的代碼,那就把這些部分分開存儲,并以那部分內容來命名。 如果程序的可執行大小是一個要考慮的問題,則不常用到的函數應當置于它們各自的文件中。 以以下方式構造一部分文件名:
- 以模塊的主要抽象名作為模塊名。
- 為模塊名附加一部分類型名。選擇暗示它們類型的部分類型名。
- 模塊名和部分名由分隔符分隔(如“_”(下劃線)或“.”(句點));選擇一個分隔符,使用它要前后一致。
- 為了更好的預測,對文件名使用相同的大小寫,這與代碼內名稱的使用相同。
以下是模塊劃分與命名策略的示例:
-
module.inlines.cc - 如果一個模塊有多個潛在的內嵌函數,就將函數的定義置于一個單獨的
內嵌 文件中(請參見“將模塊內嵌函數定義置于單獨文件中”)。
-
module.private.hh - 如果模塊有許多常用的被其他部分引用的私有實現聲明,就把這些聲明分隔出去組成一個私有部分,它可由其他實施文件包含。
-
module.private.cc - 模塊的私有實施函數定義,為編輯的方便將它分離出去。
-
module.function_name.cc - 如果可執行的大小是個要考慮的問題,應當分離出許多程序不需要的特殊成員函數,組成各自的實施文件(請參見“如果程序大小需要考慮,將大型模塊分成多個變換單元”)。如果重載的函數置于不同文件中,每一文件函數名都應有一實例數字的后綴。如,function_name1 表示第一個獨立重載函數實例。
-
module.nested_class.cc - 模塊嵌套類的成員函數,置于其本身文件中。
-
module.test.[hh\cc] - 如果一個模塊需要擴展測試代碼,則測試代碼必須在友測試類中予以聲明。友測試類應稱為
Module.Test 。將測試代碼聲明為友類有助于模塊及其測試代碼的獨立開發;這還允許測試代碼從最終模塊對象代碼中刪除而源并不改變。
-
module.platform_name.cc - 分離出任意模塊的平臺依賴性并以平臺名稱命名部分名稱(請參見“分離平臺依賴性”)。
選擇一個模塊劃分和命名的機制,使用它要前后一致。
示例
SymaNetwork.hh //包括類
// “SymaNetwork”的聲明。
SymaNetwork.Inlines.cc //內嵌定義子單元
SymaNetwork.cc //模塊的主要實施單元
SymaNetwork.Private.cc //私有實施子單元
SymaNetwork.Test.cc //測試代碼子單元
基本原理
從模塊的實施中分離出其規約有助于用戶和提供者代碼的獨立開發。 將一個模塊的實施分割成多個變換單元,這可以更好的支持刪除對象代碼,并會導致更小的可執行大小。 使用規范化的文件命名與劃分約定允許在不檢查模塊的實際內容時理解其內容與組織。 將名稱從代碼讓渡給文件名增加了可預測性,有助于建立基于文件的工具而不需要復雜命名映射 [Ellemtel, 1993]。
只選擇一組文件擴展名以區分頭文件和實施文件
常用的文件擴展名是:對頭文件,為.h、.H、.hh、.hpp 和.hxx ;對實施文件,為.c、.C、.cc、.cpp 和.cxx 。選擇一組擴展名,使用它要前后一致。
示例
SymaNetwork.hh //擴展名“.hh”用來指定
//一個“SymaNetwork”模塊的頭文件。
SymaNetwork.cc //擴展名“.cc”用來指定
//一個“SymaNetwork”模塊的實施文件
注意
對由名字空間封裝的頭文件,C++ 草擬標準工作底稿也使用擴展名“.ns”。
每個模塊規約避免定義多個類
只有在極少數情況下多個類才能置于同一個模塊中;此時他們必須是緊密關聯關系的(如容器與它的迭代程序)如果所有類都需要對客戶模塊是可見的,可以把模塊的主要類及其支持類置于同一個頭文件中。
基本原理
減少模塊的接口以及其他模塊對此接口的依賴。
避免將私有實施聲明置于模塊的規約中
除了類的私有成員以外,模塊的私有實施聲明(如實施類型和支持類)不能出現在模塊的規約中。除非多個實施文件都需要這些聲明,否則它們應當置于所需的實施文件中;如果有多個實施文件,則這些聲明應置于輔助私有頭文件中。而其他實施文件在需要時應包含這個輔助私有頭文件。 這一做法保證了: 模塊規約清楚的表示了它的抽象,不再需要實施工件; 模塊規約越小越好,并因此減小了模塊間的編譯依賴(請參見“減小編譯依賴”);
示例
//模塊 foo 的規約,包含于文件“foo.hh”中;
//
class foo
{
.. 聲明
};
//“foo.hh”結束;
//模塊 foo 的私有聲明,包含于文件
// “foo.private.hh”中;所有 foo 實施文件都使用它。
... 私有聲明
//“foo.private.hh”結束;
//模塊 foo 的實施,包含于以下多個文件中
// “foo.x.cc”和“foo.y.cc”
//文件“foo.x.cc”;
//
#include "foo.hh" //包含模塊本身的頭文件
#include "foo.private.hh" //包含實施
//所需的聲明。
... 定義
//“foo.x.cc”結束
//文件“foo.y.cc”;
//
#include "foo.hh"
#include "foo.private.hh"
... 定義
//“foo.y.cc”結束
通常使用 #include 來訪問模塊的規約
模塊要使用另一模塊,則必須使用預處理 #include 指令來訪問提供者模塊的規約。相應的,模塊不能再次聲明提供者模塊規約的任何一部分。 當包含文件時,只有對“標準”頭文件使用 #include <header> 語法;對其余的使用 #include "header" 語法。 使用 #include 指令也適用于模塊自身的實施文件:模塊實施文件必須包括它本身的規約和私有輔助頭文件(請參見“不同文件中放置模塊規約與實施”);
示例
//模塊 foo 頭文件中的規約
// "foo.hh"
//
class foo
{
... 聲明
};
//“foo.hh”結束;
//模塊 foo 在文件“foo.cc”中的實施;
//
#include "foo.hh" // 實施文件包含
//它本身的規約
... foo 成員的定義
//“foo.cc”結束
#include 規則的一個例外是當模塊只通過引用(使用指針或引用類型聲明)使用或包含提供者模塊的類型(類);這種情況下引用或包含通過使用預先聲明來規約(請參見“減小編譯依賴”),而不使用 #include 指令。 只包含絕對需要的文件:這意味著模塊頭文件不應包含只有模塊實施需要的其他頭文件。
示例
#include "a_supplier.hh"
class needed_only_by_reference;//對只需要
//指針或引用的類
//使用
//預先聲明來訪問
void operation_requiring_object(a_supplier required_supplier, ...);
//
//操作需要一個實際的提供者對象;
//提供者規約必須已有 #include 指令。
void some_operation(needed_only_by_reference& a_reference, ...);
//
//某些操作只需要對對象進行引用;
//因此為提供者使用預先聲明。
基本原理
這一規則保證了:
- 只存在唯一的模塊接口聲明,所有的客戶只看到同一接口;
- 模塊間的編譯依賴最大程度的減小了;
- 客戶不會為代碼無故產生不需要的編譯開支。
將模塊內嵌函數的定義置于單獨的文件中
當模塊有許多內嵌函數時,它們的定義應置于一個分離的只有內嵌函數的文件中。在模塊頭文件的末尾應包含內嵌函數文件。 請參見“使用 No_Inline 條件編譯符破壞內嵌編譯”。
基本原理
這種技術使實施細節不會聚集到模塊頭文件中;因此,保留了一個清晰的規約。當不進內嵌編譯時,它也有助于減少代碼復制:使用條件編譯,可將內嵌函數編譯成一個對象文件而不是靜態的編譯成每一個使用的模塊。相應的,內嵌函數定義不應在類定義中定義,除非它們非常瑣碎。
如果程序規模是個要求考慮的問題,就把大的模塊分割成多個變換單元
將大型模塊分割成多個變換單元有助于在程序鏈接中刪除未經引用的代碼。很少引用的成員函數應當分割成獨立文件,與經常使用的函數相分離。最后,各成員函數可置于他們自己的文件中 [Ellemtel, 1993]。
基本原理
鏈接器在對象文件中消除無引用代碼的能力各不相同。將大型模塊分割成多個文件允許這些鏈接器通過消除整個對象文件中的鏈接來減少可執行大小 [Ellemtel, 1993]。
注意
先考慮模塊是否能分割成小的抽象,這也是很值得的。
分離平臺依賴性
從獨立于平臺的代碼中分離出依賴于平臺的代碼;這有助于提高可移植性。依賴于平臺的模塊應當具有受平臺名限制的文件名以突出對平臺的依賴。
示例
SymaLowLevelStuff.hh //“LowLevelStuff”
//規約
SymaLowLevelStuff.SunOS54.cc // SunOS 5.4 實施
SymaLowLevelStuff.HPUX.cc // HP-UX 實施
SymaLowLevelStuff.AIX.cc // AIX 實施
注意
從構架與維護角度,將對平臺的依賴包含在少數幾個低層子系統中也是一個好方法。 采納一個標準文件內容結構,使用它要前后一致。 建議文件內容就構包括以下次序的以下部分:
1. 重復包含保護(只適用于規約)。 2. 確定可選文件與版本控制。 3. 本單元所需包含的文件。 4. 模塊紀錄(只適用于規約)。 5. 聲明(類、類型、常量、對象、函數)和其他文本規約(前置及后置條件,常量)。 6. 對這種模塊內嵌函數定義的包含。 7. 定義(對象與函數)和私有實施聲明。 8. 版權聲明。 9. 可選版本控制歷史。
基本原理
以上文件的內容次序,首先呈現給了客戶相關的信息;這與安排類的公共、保護和私有部分次序的基本原理相一致。
注意
根據公司的政策,版權信息應置于文件的首部。
防止重復文件包含的保護
通過在每個頭文件中使用以下結構可以防止重復包含與編譯文件:
#if !defined(module_name) //使用預處理符
#define module_name //防止重復
//包含...//聲明到此
#include "module_name.inlines.cc" //可選的內嵌結構
//包含到此。
//包含模塊內嵌函數之后
//沒有其他聲明。
#endif //module_name.hh 結束
對包含保護符使用模塊文件名。對模塊名與操作符使用同一大小寫。
使用"No_Inline "條件編譯符破壞內嵌編譯
使用以下條件編譯結構控制可內嵌函數的內嵌(相對于外部的)編譯。
//在文件 module_name.inlines.hh 首部,
#if !defined(module_name_inlines)
#define module_name_inlines
#if defined(No_Inline)
#define inline //使內嵌關鍵詞無效
#endif
... //內嵌定義到此
#endif //文件 module_name.inlines.hh 結束
//文件 module_name.hh 尾
//
#if !defined(No_Inline)
#include "module_name.inlines.hh"
#endif
//包含module_name.hh之后
// module_name.cc 文件首
//
#if defined(No_Inline)
#include "module_name.inlines.hh"
#endif
條件編譯結構與多包含保護結構相似。如果未定義 No_Inline ,則內嵌函數與模塊規約一同編譯并自動從模塊實施中排除出去。如果定義了 No_Inline ,則模塊規約不包含內嵌定義,但廢除 inline 關鍵詞的模塊實施包含了它。
基本原理
以上技術允許內嵌函數在外部編譯時的精簡代碼復制。使用條件編譯,內嵌函數的簡單復制編譯成了定義模塊;而當由編譯器開關規約外部編譯時,所復制的代碼在每個使用的模塊里編譯成“靜態”(內部鏈接)函數。
注意
條件編譯增加了維護構建依賴關系的復雜性。對這種復雜性的管理通常是利用把頭文件和內嵌函數定義視為一個邏輯單元:實施文件因此依賴于頭文件以及內嵌函數定義文件。
代碼風格
對嵌套語句使用小的、一致的縮進風格
使用一致縮進來清楚的界定嵌套語句;為達到這一目的,兩到四個空格的縮進是最有效的方式。我們推薦使用規則的兩空格縮進。 混合語句或塊語句的分界符 ({} ),應當與周圍的語句處于縮進的同一級上(這意味著垂直布置{} )。通過選擇空格的數量對塊內的語句進行縮進。
switch 語句的 case 標號應當與 switch 語句處于同一縮進級;switch 語句內語句應比 switch 語句本身和 case 標號多縮進一級。
示例
if (true)
{ //新程序塊
foo(); //程序塊內語句
//縮進兩個空格
}
else
{
bar();
}
while (expression)
{
statement();
}
switch (i)
{
case 1:
do_something();//相對 switch 語句本身
//縮進
break; //一層
case 2:
//...
default:
//...
}
基本原理
能夠輕易識別程序塊,還要在不超出顯示屏或打印頁面界限的前提下有足夠多的嵌套,使用兩空格的縮進是一個折衷的方案。
相對于函數名或作用域名縮進函數參數
如果一行之內難以容下函數聲明,則將函數的第一個參數放在函數名所在的那一行;其余參數每個占一新行,使用與第一個參數相同的縮進。下面所示的聲明與縮進的風格,在函數返回類型和名稱下留有空格;因此,提高了它們的可見性。
示例
void foo::function_decl( some_type first_parameter,
some_other_type second_parameter,
status_type and_subsequent);
如果按照以上的指南會導致換行,或參數縮進的太多,則以函數名或作用域名(類、名字空間)縮進所有的參數,每一參數獨占一行:
示例
void foo::function_with_a_long_name( //函數名是
//不易看到的
some_type first_parameter,
some_other_type second_parameter,
status_type and_subsequent);
請參照下面的布置規則。
使用能夠適合標準打印紙大小的最大行寬
應限制程序行的寬度以防止在使用標準(信紙)或缺省打印紙張進行打印時丟失信息。
注意
如果縮進使深層的嵌套語句處于太右邊,語句又太長以致超出了右邊的空白,這時就應當考慮將代碼分成更小的更易管理的函數。
使用一致換行
當函數聲明、定義、調用或 enum 聲明中枚舉操作符的參數列表不能置于一行,則將每一個列表元素置于單獨的一行(請參見“相對函數名和作用域縮進函數參數”)。
示例
enum color { red,
orange,
yellow,
green,
//...
violet
};
如果一個類或函數模板聲明太長,則在模板的實參列表后進行連續換行。例如(標準迭代程序庫的聲明,[X3J16, 95]):
template <class InputIterator, class Distance>
void advance(InputIterator& i, Distance n);
第三章
注釋
本章對代碼注釋的使用提供指導。 注釋應當作為源代碼的補充,而不是直譯源代碼:
- 它們應當解釋不能直接從源代碼看出東西;它們不應復制語言的語法或語義。
- 它們應當幫助讀者掌握背景中的概念、依賴性、特別是復雜的數據代碼和算法。
- 它們應當突出:與代碼或設計標準的不同點、受限特性的使用、以及特殊的“技巧”。
對每一行注釋,程序員都應能夠輕松回答:這一注釋有何價值?通常,合理選擇名稱就可不要注釋。除非注釋出現在正規的程序設計語言中 (PDL),否則編譯器不對其進行編譯;因此,根據單點維護原則,應當以源代碼的方式表達設計決策,而不是以注釋的方式,即使這樣需要更多的聲明。
使用 C++ 風格的注釋而非 C 風格的注釋
應使用 C++ 風格注釋分界符"// ",而非 C 風格的"/*...*/ "。
基本原理
C++ 風格的注釋更易理解,它減少了由于偶然缺少一個注釋結束分隔符而造成大量代碼被注釋掉的風險。
反例
/*注釋開始,但缺少注釋結束分隔符
do_something();
do_something_else(); /*對 do_something_else 的注釋*/
//注釋到此結束。
// do_something 和
// do_something_else
//都意外的被注釋掉了!
Do_further();
注釋與源代碼盡可能靠攏
注釋應緊貼它們所要注釋的代碼;它們使用相同的縮進,使用一個空注釋行接于代碼之后。 對多行連續語句的注釋應置于語句的上方作為語句的介紹性說明。而對單個語句的注釋應置于語句的下方。
示例
//置于語句前的注釋
//它對下面多個語句進行注釋
//
...
void function();
//
//語句之后的注釋
//它對前一個語句進行注釋。
避免行末注釋
注釋應避免與源結構處于同一行:否則會使注釋與其所注釋的源代碼不對齊。但在描述長聲明中的元素(如 enum 聲明中的枚舉操作符)時,也能容忍這種不好的注釋方式。
避免注釋頭
避免使用包含作者、電話號碼、創建和修改日期的注釋頭:作者和電話號碼很快就過時了;而創建和修改日期以及修改的原因則最好由配置管理工具來維護(或其他形式的版本歷史文件)。 即使對主結構(如函數和類),也要避免使用垂直滾動條,閉合的欄或框。它們搞亂了視覺效果,這樣就很難保持一致性。使用空行而不是多個注釋行來分割相關的源代碼塊。使用一個空行來分離函數或類中的結構。使用兩個空行將函數與函數相分離。 框架或表單看起來具有較好的一致性,還提醒了程序員來注釋代碼,但它們會導致直譯的風格 [Kruchten, 94]。
使用空注釋行分離注釋段
在一個注釋塊中,使用空注釋行而非空行來分割注釋段
示例
//在下一段中
//需要繼續這里的解釋
//
//空注釋行使得
//同一注釋塊中
//不同段間的界限分明。
避免冗余
注釋中應避免重復程序標識符,避免復制別處有的信息(此時可使用一個指向信息的指針)。否則程序中的任何一處改動都可能需要多處進行相應的變動。如果其他地方沒有進行所需的注釋改動,將會導致誤注釋:這種結果比根本沒有注釋還要糟糕。
編寫自記錄代碼而非注釋
時刻注意要編寫自記錄代碼而非注釋。這可通過選擇合理的名稱、使用特殊的臨時變量或重新安排代碼的結構來實施。注意注釋中的風格、語法和拼寫。使用自然語言注釋而不是電報或加密格式。
示例
替換如下代碼:
do
{
...
} while (string_utility.locate(ch, str) != 0);
//當找到時退出查找循環。
將以上代碼改寫成:
do
{
...
found_it = (string_utility.locate(ch, str) == 0);
} while (!found_it);
記錄類與函數
雖然自記錄代碼比注釋好;但通常還需要提供一些超出解釋代碼復雜部分以外的信息。至少需要記錄以下信息:
- 每個類的目的;
- 函數名不能清楚說明它的目的時,則記錄函數相應的目的;
- 返回值的含義;如不可預測函數布爾返回值的含義:如,ture 值是否表示函數執行成功;
- 出現異常的條件;
- 如果有的話,參數的前置或后置條件;
- 其他數據訪問,特別是當數據被修改時:對有副作用的函數特別重要;
- 合理使用類或函數所需的限制或其他信息;
- 對類型和對象來說,語言無法表達的任何不變量或其他限制。
基本原理
與聲明相結合的代碼記錄應當足以使客戶使用代碼;因為只使用 C++ 不能完全表達類、函數、類型和對象的完整語義,所以需要記錄存檔。
第四章
命名
本章為不同 C++ 實體命名指南。
概述
為程序實體(類、函數、類型、對象、常量、異常、名字空間)取一個好名稱并不是一件容易的事。對中大型的應用程序來說,問題就顯得更加困難:這里名稱容易沖突,并且還缺少一些近義詞來區分相似但不相同的概念,這就更增加了困難的程度。 使用名稱約定可減少創造合適名稱時的腦力勞動。除了這一好處以外,名稱約定還帶來了代碼的一致性。一個有用的名稱約定必須在以下方面提供向導:排版風格(或如何書寫名稱)以及名稱的構建(或如何選擇名稱)。
選擇一個名稱約定,使用它要前后一致
只要能夠前后一致,使用哪一個名稱約定并不重要。命名的一致性比實際的約定重要的多:一致性支持“最小混淆”原則。 因為 C++ 是一個區分大小寫的語言,并且在 C++ 應用界有多種不同的命名約定,幾乎不可能實現命名的絕對一致。我們推薦基于主機的環境(如 UNIX 或 Windows)以及項目所使用的主要的庫選擇項目的命名約定;盡可能實現代碼的一致性:
- 主機使用 UNIX 且不常使用商業庫(如 X Window 庫、X Toolkit Intrinsics 和 Motif)的項目可能傾向于使用全部為小寫字符、以下劃線分隔的約定:這是 UNIX 系統調用以及 C++ 草擬標準工作底稿使用的約定。
- 以商業庫為中心的 UNIX 主機項目可能更傾向于使用大寫風格,通常也稱為 Smalltalk 風格 - 一種首字母大寫,字間直接相連而無分隔符的書寫風格。
- Microsoft 基于 Windows 的項目可能會推薦使用并不常用的 Microsoft? “匈牙利”命名法我們不推薦使用這種命名風格,因為它違背了本文指南的基本原則。
注意:
細心的讀者會發現本文的示例現在并未遵循所有的指南。這部分是因為示例的來源有多個,還因為希望減少篇幅。因此,沒有嚴格遵照格式化的指南來做。但需聲明:照我說的去做,不要學我的實際做法。
永遠不要聲明以一個或多個下劃線 ('_') 開頭的名稱
開頭帶有一個下劃線(“_”)的名稱通常由庫函數(“_main ”和“_exit ”)使用。開頭帶有兩個下劃線(“__”)或一個下劃線后接一個大寫字母的名稱保留給編譯器內部使用。 名稱還要避免下劃線相鄰,否則很難識別下劃線的確切個數。
避免使用只靠字母大小寫才能區分的名稱
只通過大小寫才能區分的類型名稱,它們間的差異是很難記憶的,也就很容易造成混淆。
避免使用縮寫
應用領域中常用的縮寫(如 FFT 指快速傅立葉變換),或在項目縮寫清單中有定義的縮寫,才能使用相應的縮寫。否則,很有可能相似但不相同的縮寫到處出現,之后就引進了混淆和錯誤(如將 track_identification 縮寫成 trid、trck_id、tr_iden、tid、tr_ident等)。
避免使用后綴來表明程序語言結構
使用后綴劃分實體種類(如對type(類型)使用 type,對 exceptions(異常)使用 error)對幫助理解代碼通常并不有效。后綴如 array 和 struct 還意味著一個具體的實現;在實現改變時(結構或數組表示的改變)對客戶代碼要么會有反面的結果,要么會起誤導的作用。 但后綴在有限幾種情況下也會有一定用處:
- 當可供選擇的標識符非常有限時,選擇一個最佳名稱并用后綴表明它的類型。
- 當它表示一個應用領域的概念如 aircraft_type(飛行器類型)時。
選擇清晰的、易辨認的、有意義的名稱
從應用的角度選擇名稱,名詞使用形容詞修飾來提高局部(具體上下文)的含義。確保名稱與其類型保持一致。 選擇合適的名稱,使以下結構:
object_name.function_name(...);
object_name->function_name(...);
易于閱讀并有實際含義。 短名稱或縮寫名稱打字速度快不是使用它們的可以接受的理由。單字母或很短幾個字母的標識符通常出于選擇不當或懶惰。但使用人們熟知的 E 作為自然對數的底數或 Pi 作為圓周率就是例外了。 不幸的是,編譯器和支持的工具有時限制名稱的長度。因此,應當注意:長名稱不應只在結尾的幾個字符有所不同,因為結尾的幾個用于區分的字符可能會被這些工具截斷。
示例
void set_color(color new_color)
{
...
the_color = new_color;
...
}
優于
void set_foreground_color(color fg)
和:
oid set_foreground_color(color foreground);{
...
the_foreground_color = foreground;
...
}
第一個示例中名稱的選擇要優于其它兩個:對 new_color 進行了限定使它與其類型一致;因此增強了函數的語義。 第二個示例中,讀者直覺上會認為 fg 意指 foreground(前景);但是,任何一個好的編程風格都不應留給讀者作直覺推導的余地。 第三個示例中,當使用參數 foreground (遠離其聲明)時,讀者會認為 foreground 實際上就是指 foreground 的顏色??梢韵胂?,它能代表任何一種可暗中轉化為 color 的類型。
注意:
使用名詞和形容詞構成名稱、確保名稱與類型相符、遵循自然語言以增強代碼的可讀性和語義。
使用名稱的正確拼寫
英語字符名稱部分應正確的拼寫,遵守項目要求的形式,如使用一致的英國英語或美國英語,但不能同時使用。這對注釋也同樣適用。
布爾值使用正值謂詞從句
對布爾值類型的對象、函數及函數實參使用正值形式的判定句式,如 found_it 、is_available ,但不使用 is_not_available 。
基本原理
當否定謂詞時,雙重否定更難以理解。
名字空間
使用名字空間由子系統或庫劃分潛在全局名稱
如果將系統解構成子系統,使用子系統名稱作為名字空間的名稱,這樣可劃分系統的全局名字空間并使其最小化。如果系統是一個庫,對整個庫使用一個最外層的名字空間。 為每一個子系統或庫名字空間取一個有意義的名稱;此外,為它取一個簡化的或縮寫的別名。選擇不會引起沖突的簡寫或縮寫的別名,如 ANSI C++ draft standard library(ANSI C++ 草擬標準庫)[Plauger, 95] 將 std 定義為 iso_standard_library 的別名。 如果編譯器尚未支持名字空間結構,使用名稱前綴模擬名字空間。例如,系統管理子系統接口中的公共名稱應帶有前綴 syma (System Management(系統管理)的簡寫)。
基本原理
使用名字空間包含可能的全局名稱,這樣有助于子項目團隊或廠商獨立開發代碼時避免名稱沖突。這必然導致只有名字空間是全局的。
類
類的名稱使用名詞或名詞短語
使用簡單形式的常用名詞或名詞短語,為類取名時要表達出它的抽象含義。基類使用更通用的名稱,而對派生類使用更專用的名稱。
typedef ... reference; //出自標準庫
typedef ... pointer; //出自標準庫
typedef ... iterator; //出自標準庫
class bank_account {...};
class savings_account : public bank_account {...};
class checking_account : public bank_account {...};
當對象和類型的名稱沖突或缺少合適的名稱時,對象使用簡單名稱,類型名稱添加后綴 mode、kind、code 等。 表明是對對象集合的抽象時使用復數形式。
typedef some_container<...>yellow_pages;
當需要對象集合外的其他語義時,使用以下標準庫中的數據結構作為行為模式和名稱后綴:
- vector(向量) - 一種可隨機訪問的順序容器;
- list(表) - 一種有序的順序容器;
- queue(隊列) - 一種先入先出的順序容器;
- deque(雙端隊列) - 一種兩個端口都可進行操作的隊列;
- stack(堆棧) - 一種后入先出的順序容器;
- set(集合) - 一種關鍵詞訪問(關聯關系)容器;
- map(圖) - 一種關鍵詞訪問(關聯關系)容器;
函數
過程類型的函數名稱使用動詞
對無返回值的函數(函數聲明為 void 返回類型)或返回值使用指針或引用類型的函數,使用動詞或動詞短語。 對使用非 void 返回類型返回唯一一個值的函數,使用名詞或名詞短語。 對帶有常用操作(行為模式)的類,使用項目選項列表中的操作名。例如:begin, end, insert, erase (標準庫中的容器操作)。 避免使用“get”和“set”來命名表明狀態的函數(給函數添加前綴“get”和“set”),特別是對取出和設置對象屬性的公共操作更是這樣。操作命名應當停留在類抽象和服務提供一級上;取出和設置對象屬性是低層次的實現細節,如果使它們公共化,則會降低封裝的完整性。 返回布爾值(謂詞)的函數使用形容詞(或過去分詞)。謂詞經常使用名詞前添加前綴 is 或 has 的形式使名稱表達一個肯定的聲明。當對象、類型名或枚舉型常量也使用簡單名稱時,這也同樣非常有用。時態上要保持一致,力求準確。
示例
void insert(...);
void erase(...);
Name first_name();
bool has_first_name();
bool is_found();
bool is_available();
不要使用否定意義的名稱,因為這會導致雙重否定表達式出現(如 !is_not_found );這使得代碼更難以理解。有些情況下,通過使用反義詞,如“is_invalid ”代替“is_not_valid ”,可以使否定謂詞變成肯定的而不需改變其語義。
示例
bool is_not_valid(...);
void find_client(name with_the_name, bool& not_found);
Should be re-defined as:
bool is_valid(...);
void find_client(name with_the_name, bool& found);
當需要使用同一通用含義時,使用函數的重載
當操作目的相同時,使用重載而不使用同義詞;這盡量減少了系統中概念的數量和不同的操作,因此也就降低了整體的復雜性。 當重載操作符時,要確保操作符的語義保留了下來;如果不能保留約定俗成的操作符含義,則為函數選用另一個名稱而不使用操作符重載方式。
對象與函數參數
實參名使用語法元素強調其含義
為表明其唯一性或表明本實體是活動的主要核心,在對象或參數名稱前加前綴“the ”或“this ”。要表明次要、臨時、輔助對象,則加前綴“a ”或“current ”:
示例
void change_name( subscriber& the_subscriber,
const subscriber::name new_name)
{
...
the_subscriber.name = new_name;
...
}
void update(subscriber_list& the_list,
const subscriber::identification with_id,
structure& on_structure,
const value for_value);
void change( object& the_object,
const object using_object);
異常
異常名選用否定的含義
只有在處理錯誤時才必須使用異常,故使用名詞或名詞短語來清楚的表達一個否定的含義:
overflow, threshold_exceeded, bad_initial_value
異常名使用項目定義過的形容詞
使用項目定義列表中以下詞的一個 bad、incomplete、invalid、wrong、missing 或 illegal 作為名稱的一部分,而不要機械的套用 error 或 exception,因為它們并不表達具體的信息。
其他事項
浮點指數和十六進制數使用大寫字母。
浮點數中的字母“E”和十六進制數從“A”到“F”應當一直保持大寫。
第五章
聲明
本章為不同 C++ 聲明類型的使用及形式提供指南。
名字空間
C++ 語言中名字空間存在之前,管理名稱的作用域只有有限的幾種手段;因此,全局名字空間的使用過于泛濫,導致眾多的沖突,這使同一程序中難以同時使用一些庫。新的名字空間語言特征解決了全局名字空間的干擾問題。
將全局聲明限定在名字空間中
這意味著只有名字空間的名稱可以是全局的;所有其他的聲明都必須在某個名字空間的作用域內。 忽略了這一規則可能最終導致名稱沖突。
使用名字空間劃分非類功能
邏輯上劃分非類功能類的類別,或作用域遠大于類的功能(如庫或子系統),使用名字空間邏輯上統一的聲明(請參見“使用名字空間由系統或庫劃分潛在的全局名稱”)。 名稱表達了功能的邏輯劃分。
示例
namespace transport_layer_interface { /* ...*/ };
namespace math_definitions { /* ...*/ };
盡量不使用全局和名字空間范圍的數據。
使用全局和名字空間范圍內數據與封裝原則是背道而馳的。
類
C++ 中類是基本的設計實施單元。使用它們紀錄域與設計的抽象,并作為實施抽象數據類型 (ADT) 的封裝機制。
使用 class 而不是 struct 來實現抽象數據類型
使用 class (類的關鍵字)而不是 struct 來實現一個表示抽象數據類型的類。 使用 struct (類的關鍵字)來定義類似 C 中的舊式數據結構 (POD),尤其是在與 C 代碼程序接口時。 雖然 class 和 struct 是等價的,可以互換,但 class 具有更好的默認訪問控制 (private),這樣就提供了更好的封裝性能。
基本原理
區分 class 與 struct 的的一致做法引入了以上的語義差別,并且還超出了語言規則的范圍:class 是紀錄抽象與封裝最先考慮的結構;而 struct 代表一個純數據結構,它可以在混合編程語言程序中交換使用。
以可訪問權限逐次降低的順序聲明類的成員
類聲明中的訪問限定符應以順序 public, protected, private 出現。
基本原理
以 public、protected、private 順序排列的成員聲明保證了類用戶最感興趣的信息置于最前列,因此減少了類用戶訪問不相關信息或實現細節。
抽象數據類型避免聲明公共或保護數據成員
公共或保護數據成員的使用降低了類的封裝性并影響了系統抵抗變化的能力:公共數據成員將類的實現暴露給了它的用戶;保護數據成員將類的實現暴露給了從它繼承的類。類公共數據成員或保護數據成員的任何改變都會影響到用戶和繼承類。
使用友元保留封裝性
初次碰到時,它好像是違反直覺的:友元關系將一個類的私有部分暴露給了它的友元,還如何實現封裝呢?當類之間高度相互依賴,需要相互間的內部信息時,最好是賦予它們間友元關系而不是通過類的接口將其內部細節暴露出來。 不希望如同公共成員一樣暴露類的內部細節以提供給類客戶訪問權。將保護成員暴露給潛在的繼承類,鼓勵了分級設計,但這也是不希望的。友元關系有選擇性的賦予了對私有成員的訪問權,而不用實現子類限制。因此再所需訪問之外保留了封裝。 將友元關系賦予友元測試類很好的說明了友元關系保留了封裝特性。友元測試類通過訪問類的內部可完成相應的測試代碼,之后,友元測試類可從交付的代碼中刪除。因此,沒有丟失封裝,也沒有向可交付代碼增加代碼。
避免在類聲明中定義函數
類的聲明應當只包含函數的聲明,永遠不要進行函數定義(實現)。
基本原理
類聲明中定義函數干擾了類對其實現細節的規約;使類的接口難以辨認并難以閱讀;并增加了對編譯的依賴。 類聲明中的函數定義也減少了對內嵌函數的控制(請參照“使用 No_Inline 條件編譯符禁止內嵌編譯”)。
明確聲明構造函數的類使用默認構造函數
為了在數組中或任何 STL 容器中使用類,類必須提供公共的默認構造函數或允許編譯器生成一個構造函數。
注意:
當類有非靜態引用類型的數據成員時是以上規則的一個例外,這種情況下通常不可能創建一個有意義的默認構造函數。因此,使用對象數據成員的引用就是不可靠的。
帶有指針類型數據成員的類要聲明其復制構造函數和賦值操作符
如果需要,并且沒有明確聲明時,編譯器會為類暗中生成一個復制構造函數和一個賦值操作符。編譯器定義了復制構造函數和賦值操作符,在 Smalltalk 術語中這通常稱為“shallow-copy”(淺拷貝):明確的說,即按成員拷貝以及對指針按位拷貝。使用編譯器生成的復制構造函數和默認的賦值操作符肯定會造成內存泄漏。
示例
//摘自 [Meyers, 92].
void f()
{
String hello("Hello");//假設 String 類型
//由指向 char 型
//數組的指針來實現。
{ //進入新的域(程序塊)
String world("World");
world = hello; //賦值語句使 world 丟失了
//它最初指向的內存單元
} //從域中退出時
//解構 world;
//此時也間接的解構了 hello
String hello2 = hello; //將已解構過的 hello 賦給
// hello2
}
以上代碼中,儲存字符串“World ”的內存單元在賦值語句之后丟失了。結束內部程序塊時,銷毀了world ;因此,由 hello 引用的內存單元也丟失了。已經解構的 hello 賦給了 hello2 。
示例
//摘自 [Meyers, 1992]。
void foo(String bar) {};
void f()
{
String lost = "String that will be lost!";
foo(lost);
}
以上代碼中,當調用函數 foo 使用實參 lost 時,利用編譯器定義的復制構造函數將 lost 復制到 foo 中。因為復制 lost 時對指向“String that will be lost! ”的指針進行逐位復制,當從 foo 中退出時,對 lost 的復制(即形參 bar,譯者注)將會被銷毀(假設析構函數正確的釋放了內存單元),同時存儲字符串“String that will be lost! ”的內存單元也會被釋放。
不要重復聲明構造函數參數有默認值
示例
//示例摘自 [X3J16, 95; section 12.8]
class X {
public:
X(const X&, int); // int 參數沒有
//初始化
//沒有用戶聲明的復制構造函數,因此
//編譯器暗中聲明了一個。
};
//int 參數滯后的初始化
//將構造函數變成了復制構造函數。
//
X::X(const X& x, int i = 0) { ...}
基本原理
編譯器在類聲明中沒有發現“標準”的復制構造函數時會暗中聲明一個復制構造函數。但是,默認參數滯后的初始化可能將構造函數改變成復制構造函數:使用復制構造函數時導致混淆。因為這種混淆,任何復制構造函數的使用都因此而變為病態的。[X3J16, 95; section 12.8].
將析構函數總是聲明為 virtual 類型
除非明確將類的設計為不可繼承的,否則應將析構函數聲明為 virtual 類型。
基本原理
如果基類的析構函數沒有聲明為 virtual 類型,則通過基類的指針或引用刪除派生類對象將導致不確定的行為。
示例
//為了簡便起見,這里使用了不好的命名風格
class B {
public:
B(size_t size) { tp = new T[size]; }
~B() { delete [] tp; tp = 0; }
//...
private:
T* tp;
};
class D : public B {
public:
D(size_t size) : B(size) {}
~D() {}
//...
};
void f()
{
B* bp = new D(10);
delete bp; //由于基類
//的析構函數沒有定義成 virtual 類型,
//這里的行為是不確定的
}
避免聲明太多的轉換操作符和單參數構造函數
使用限定符 explicit 可防止單參數構造函數用于隱式轉換。
不要重定義非虛函數
非虛函數實現固定的行為,并且不希望專用于派生類。違反這一原則將導致不可預料的行為:同一對象不同時間可能表現不同的行為。 非虛函數是靜態綁定的;以下示例中,對象函數的調用由變量(指向 A 或 B 的指針)的靜態類型控制,而不是對象的實際類型。
示例
// Adapted from [Meyers, 92].
class A {
public:
oid f(); //非虛函數:靜態綁定
};
class B : public A {
public:
void f(); //非虛函數:靜態綁定
};
void g()
{
B x;
A* pA = &x; //靜態類型,指向 A 的指針
B* pB = &x; //靜態類型,指向 B 的指針
pA->f(); //調用 A::f
pB->f(); //調用 B::f
}
謹慎使用非虛函數
因為非虛函數通過限制特殊化(即重載,譯者注)和多態的使用來限制子類,聲明為非虛擬之前,必須注意確保操作對所有子類確實是固定不變的。
使用初始化構造函數而不是使用構造函數中的賦值語句
對象構造時其狀態的初始化應由初始化構造函數(一個成員初始化列表)完成,而不是通過構造函數內的賦值操作符完成。
示例
代碼如下:
class X
{
public:
X();
private
Y the_y;
};
X::X() : the_y(some_y_expression) { }
//
// “the_y”由初始化構造函數進行初始化
而不是如下進行初始化
X::X() { the_y = some_y_expression; }
//
// “the_y”由賦值操作符進行初始化
基本原理
對象的構造涉及到在執行構造函數體之前構造所有的基類以及數據成員。使用初始化構造函數只需要一個操作(由初值構造),而在構造函數體中初始化數據成員需要兩個操作(構造以及賦值)。 對有多重嵌套的類(類包含類,所包含的類又包含其他的類),多個操作(構造+成員賦值)造成的性能的開支就十分重要了。
初始化構造函數不要調用成員函數
示例
class A
{
public:
A(int an_int);
};
class B : public A
{
public:
int f();
B();
};
B::B() : A(f()) {}
//不確定:調用成員函數時A
//尚未初始化[X3J16, 95].
基本原理
當所有基類的成員初始化完成之前如果初始化構造函數直接或間接的調用了成員函數,此操作的結果是不確定的。[X3J16, 95].
注意構造函數和析構函數調用時的情況
構造函數中調用成員函數時要格外注意;即使調用的是虛函數時也要注意。執行的將是在類或其基類的構造函數、析構函數中定義的操作。
整形類常量使用 static const
定義整形(整數)類常量時,使用 static const 數據成員,不要使用 #define 或全局常量。如果編譯器不支持 static const ,則使用 enum 。
示例
代碼如下:
class X {
static const buffer_size = 100;
char buffer[buffer_size];
};
static const buffer_size;
或:
class C {
enum { buffer_size = 100 };
char buffer[buffer_size];
};
但不要使用:
#define BUFFER_SIZE 100
class C {
char buffer[BUFFER_SIZE];
};
函數
一定要明確聲明函數的返回值類型
這可以防止編譯器發現函數未聲明返回值類型時所造成的模糊不清。
函數聲明中要提供正規參數名稱
函數的聲明和定義中要使用相同的名稱;這可使混淆程度降至最小。提供參數名稱有利于提高代碼的易注釋性和易讀性。
函數要盡量只有一個返回點
返回語句在程序體中自由放置與 goto 語句的情況類似,會造成代碼難以閱讀和維護。 只有在少數幾種函數中才能容忍出現多個返回語句,此時可以同時看到所有的返回語句 return 并且代碼還有非常規則的結構:
type_t foo()
{
if (this_condition)
return this_value;
else
return some_other_value;
}
函數的返回類型為 void 時無返回語句。
避免創建對全局有副作用的函數
應盡量避免創建對全局有副作用的函數,因為它可能改變并不知情的數據而非它們內部對象的狀態,如全局和名字空間數據(另請參見“盡量不使用全局或名字空間范圍的數據”)但如果這是不可避免的,則應明確記錄下所有的副作用以作為函數規約的一部分。 只在所需的對象中傳遞參數增加了代碼的上下文無關性,并提高了它的強壯性和易理解性。
以重要性和活躍性遞減的順序聲明函數的參數
從調用者的角度講,參數聲明的順序是非常重要的:
- 首先以重要性遞減的順序定義非默認參數;
- 再以被修改可能性遞減的順序定義有默認值的參數。
這種順序使用了參數默認值的優點來減少函數調用中實參的個數。
避免聲明帶有不定參數個數的函數
帶有不定參數個數的函數無法對其實參進行類型檢查。
避免重復聲明帶有默認參數的函數
函數的進一步重復聲明中應避免添加默認值:遠離先前聲明,一個函數只應聲明一次。否則,如果讀者沒有意識到此后的聲明,這將會造成混淆。
函數聲明中盡可能使用 const
檢查函數是否有常量行為(返回常數值;接受常量實參;或其操作不帶有副作用),如果有則使用限定符 const 表明其行為。
示例
const T f(...); //函數返回一個常量
//對象。
T f(T* const arg); //函數接受一個常量指針
//為其參數。
//可以改變指針所指的對象,
//但不可以改變指針本身。
T f(const T* arg); //函數參數為指針類型
T f(const T& arg); // 函數參數為引用類型
//它們都指向一個常量對象。指針可以
//改變,但所指的對象
//不能改變。
T f(const T* const arg); //函數參數為
//常量指針,它指向常量對象。
//指針和它所指的對象
//都不改變。
T f(...) const; //函數沒有副作用:
//不改變它對象的狀態
//所以可應用于
//常量對象。
避免利用值傳遞對象
利用值傳遞和返回對象可能帶來構造函數和析構函數的大量開支??赏ㄟ^引用傳遞和返回對象,這樣可避免構造函數和析構函數的開支。 可用 Const 引用指定通過引用傳遞的實參是不可改變的。復制構造函數和賦值操作符是典型的示例:
C::C(const C& aC);
C& C::operator=(const C& aC);
示例:
考慮以下例子:
the_class the_class::return_by_value(the_class a_copy)
{
return a_copy;
}
the_class an_object;
return_by_value(an_object);
調用函數 return_by_value ,其實參為 an_object ,此時調用 the_class 的復制構造函數將 an_object 復制到 a_copy 。再次調用 the_class 的復制構造函數將 a_copy 復制到函數返回的臨時對象中。從函數返回時調用 the_class 的析構函數銷毀 a_copy 。一段時間之后再次調用 the_class 的析構函數銷毀 return_by_value 返回的對象。以上不完成任何事的函數的所有開支是兩個構造函數和兩個析構函數。 如果 the_class 是個派生類且包含其他類的成員數據,情況會更糟:會調用基類以及所包含類的構造函數和析構函數,因此函數調用所帶來的構造函數和析構函數的調用就會隨之急劇增長。
注意:
以上指南似乎旨在建議開發人員在傳遞和返回對象時總是使用引用類型,但應格外當心不要返回引用到局部對象上,當需要對象時,也不要返回引用。返回引用到局部對象會造成災難性后果,因為函數返回時,所返回的引用綁定到了所銷毀的對象上。
不能返回引用到局部對象
離開函數作用域時會銷毀局部對象;使用銷毀了的對象會造成災難。
不可返回由 new 初始化,之后又已解除引用的指針
違背這一原則將導致內存泄漏
示例
class C {
public:
...
friend C& operator+( const C& left,
const C& right);
};
C& operator+(const C& left, const C& right)
{
C* new_c = new C(left..., right...);
return *new_c;
}
C a, b, c, d;
C sum;
sum = a + b + c + d;
因為在計算加和 sum 值時沒有存儲操作符“+”的中間結果,因此中間對象不能刪除,否則會導致內存泄漏。
不要返回非常量引用或指針到成員數據上
違背了這一原則也就違背了數據的封裝性,可能導致不好的結果。
宏擴展使用內嵌定義函數不使用 #define
但要有選擇的使用內嵌定義:只對非常小的函數才使用;內嵌定義大型函數可能導致代碼膨脹。 由于用戶代碼編譯時需要內嵌定義函數的實現,因此內嵌定義函數也增加了模塊間編譯的依賴。 [Meyers, 1992] 提供了以下不良宏使用極端示例的詳細討論:
示例
不要這樣做:
#define MAX(a, b) ((a) > (b) ?(a) : (b))
而應這樣做:
inline int max(int a, int b) { return a > b ? a : b; }
宏 MAX 有好幾個問題:它不是安全的類型;它的行為不是確定的:
int a = 1, b = 0;
MAX(a++, b); // a 增 1 了兩次
MAX(a++, b+10); // a 增 1 了一次
MAX(a, "Hello"); //比較整形和指針
使用默認參數而不使用函數重載
當只使用一個算法并且此算法可由少量幾個參數進行參數化時,使用默認參數而不使用函數重載。 使用默認參數有利于減少重載函數的數量、提高可維護性、減少函數調用時所需的實參數量、以及提高代碼的可讀性。
使用函數重載表達常用語義
當同一語義操作需要多個實現時使用函數重載,但重載使用不同的實參類型。 重載操作符時保留其傳統含義。不要忘記定義關系操作符,如操作符 operator== 和 operator!= 。
避免重載以指針和整形數為實參的函數
避免使用帶單一整形實參的函數來重載帶單一指針實參的函數:
void f(char* p);
void f(int i);
以下調用可能會導致混淆:
PRE>f(NULL); f(0);
此重載解析為 f(int) 而不是 f(char*) 。
令操作符 operator= 返回對 *this 的引用
C++ 允許賦值鏈:
String x, y, z;
x = y = z = "A string";
因為復制操作符是右結合的,將字符串“A string ”賦給 z,再將 z 賦給 y,最后將 y 賦給 x。以從右到左的順序,對每個在 = 右端的表達式有效的調用一次 operator= 。這也意味著每一次 operator= 運算的結果都是一個對象,但其左端或右端的對象都有可能成為返回值。 使用復制操作符好的做法總有以下形式:
C& C::operator=(const C&);
只有左端的對象是可能的(rhs 為常量引用,lhs 為變量引用),因此應返回 *this 。詳情請參見[Meyers, 1992]。
使 operator= 檢查自賦值
執行檢查有兩點重要的理由:首先,派生類對象的賦值涉及到調用每一個基類(在繼承層次結構中位于此類的上方)的賦值操作符,跳過這些操作符就可以節省很多運行時間。其次,在復制“rvalue”對象前,賦值涉及到解構“lvalue”對象。在自賦值時,rvalue 對象在賦值前就已銷毀了,因此賦值的結果是不確定的。
盡可能減少復雜性
不要書寫過長的函數,例如其代碼超過 60 行。 盡可能減少返回語句的數量,1 是理想值。 盡量使循環復雜性降到 10 以下(對單一退出語句函數為判定語句總數加 1)。 盡量使擴展循環復雜性降到 15 以下(對單一退出語句函數為判定語句+邏輯操作符+1的和)。 盡量減小引用的平均最大作用范圍(局部對象聲明與對象第一次使用間的行距)。
類型
定義項目范圍的全局系統類型
大型項目中通常有整個系統中經常使用的類型的集合;這種情況下,將這些類型收集入一個或多個低級全局實用程序的名字空間中,這樣做是明智的(請參見“避免使用基本類型”的示例)。
避免使用基本類型
當對可移植性要求較高、需要控制數字對象占據的內存空間或需要一個具體范圍的值時,不使用基本類型。這種情況下最好通過使用適當的基本類型明確聲明帶有大小限制的類型的名稱。 確?;绢愋筒粫ㄟ^循環計數器、數組下標等潛回代碼中。
示例
namespace system_types {
typedef unsigned char byte;
typedef short int integer16; //16 位有符號整數
typedef int integer32; //32 位有符號整數
typedef unsigned short int natural16; //16 位無符號整數
typedef unsigned int natural32; //32 位無符號整數
...
}
基本原理
基本類型的表示依賴于具體實施。
使用 typedef 創建同義詞來加強局部含義
對現有名稱使用 typedef 創建同義詞,提供更有意義的局部名稱并提高易讀性(這樣做不會增加運行時間)。
typedef 也可用來提供受限名稱的速記法。
示例
//標準庫的向量聲明
//
namespace std {
template <class T, class Alloc = allocator>
class vector {
public:
typedef typename
Alloc::types<T>reference reference;
typedef typename
Alloc::types<T>const_reference const_reference;
typedef typename
Alloc::types<T>pointer iterator;
typedef typename
Alloc::types<T>const_pointer const_iterator;
...
}
}
使用 typedef 創建的名稱時,同一代碼段中不要混合使用原名稱和它的同義詞。
常量與對象
避免使用常量值
使用引用形式的已命名常量
定義常量時避免使用 #define 預處理指示符
代之以 const 或 enum 。 不要這樣做:
#define LIGHT_SPEED 3E8
而應這樣做:
const int light_speed = 3E8;
或調整數組大小如下:
enum { small_buffer_size = 100,
large_buffer_size = 1000 };
基本原理
因為由 #defines 引入的名稱在編譯預處理時會被替換掉,不出現在符號表中,因此調試就更加困難了。
對象聲明靠近其第一次使用點
聲明時總要初始化 const 對象
未聲明為 extern 的 const 對象有內部鏈接,聲明時初始化這些常量對象允許在編譯時使用初始化函數。
永遠不要忘記常量對象的“不變性”
常量對象可能存在于只讀存儲器中。
定義時初始化對象
如果對象不是自初始化的,在對象定義中指定初始值。如果不可能指定一個有意義的初始值,則賦值“nil”或考慮后面再作定義。 對大型對象,通常不建議先構造對象,此后再使用賦值語句初始化的作法。因為這樣做的代價高昂(請參見“使用初始化構造函數而不使用構造函數中的賦值語句”)。 如果構造時不可能合適的初始化對象,則使用傳統的“nil”值進行對象初始化,它意味著“未初始化”。只在初始化過程中聲明“不可用但值已知”的對象時才使用 nil 值。但算法在受控模式下可以拒絕接受:在對象合適的初始化之前使用它時可以指出這是未經初始化的變量錯誤。 注意有時不可能所有類型都聲明為 nil 值,尤其是在模運算類型中,例如角度。I這種情況下選擇最不可能出現的值。
第六章
表達式和語句
本章對不同種類的 C++ 表達式和語句提供指南。
表達式
使用冗余的圓括號使復合表達式含義更加清晰
避免表達式的過深嵌套
表達式的嵌套層數定義為:忽略操作符優先級原則時,從左到右求表達式的值所需圓括號對的嵌套數。 嵌套層數過多會使表達式難以理解。
不要假定任何特殊的表達式計算次序
除非計算次序由操作符指定(如逗號操作符、三元表達式、以及連接和分離),否則不要假定任何特殊的計算次序,因為這種假定可能導致嚴重的混淆并降低可移植性。 例如,同一語句中不要混合使用變量的增 1 和減 1 操作。
示例
foo(i, i++);
array[i] = i--;
空指針使用 0 而不使用 NULL
空指針應使用 0 還是 NULL 是一個有高度爭議的論題。 C 和 C++ 中定義任何零值常量表達式都可解釋為空指針。因為零難以閱讀,并且不贊成使用常量,傳統上程序員使用宏 NULL 作為空指針。不幸的是,NULL 沒有可移植的定義。一些 ANSI C 編譯器使用 (void *)0,但對 C++ 來說這不是一個好的選擇:
char* cp = (void*)0; /* C 合法但 C++ 不合法*/
因此任何形如 (T*)0 而非一個 0 的 NULL 定義,在C++ 中都需要類型轉換。歷史上,很多指南都擁護使用 0 代替空指針以減少類型轉換、增強代碼的可移植性。但許多 C++ 開發人員還是感覺使用 NULL 比 0 更加舒服,也爭辯說當今多數的編譯器(準確的說是多數頭文件)將 NULL 實施為 0。 本指南傾向使用 0,因為不管 NULL 的值是多少,0 都是有效的。但由于爭議,這一點降級為一個小技巧,可在適當的時候遵循或忽略它。
不要使用舊類型轉換
使用新類型轉換符(dynamic_cast、static_cast、reinterpret_cast、const_cast )而不要使用舊類型轉換符。 如果您沒有新類型轉換符,避免同時轉換,特別是向下轉換(將基類對象轉換成派生類對象)。 使用轉換操作符如下:
-
dynamic_cast - 類同一層次(子類型)成員間的的轉換,使用運行類型信息(對帶虛函數的類,運行類型信息是可用的)。這種類間的轉換一定是安全的。
-
static_cast - 類同一層次成員間的的轉換,不使用運行類型信息;所以這可能是不安全的。如果程序員不能保證類型的安全性,則使用 dynamic_cast。
-
reinterpret_cast - 不相關指針類型和整形(整數)間的轉換;這是不安全的,只應在所提及的類型間使用。
-
const_cast - 將指定為 const 型函數實參的“不變性”轉換掉。注意:const_cast 不打算將確實定義為常量對象(它可能位于只讀存儲器中)的“不變性”轉換掉。
不要使用 typeid 實現類型轉換邏輯:使類型轉換操作符完成類型檢查和轉換的操作不可再分,詳情請參見 [Stroustrup, 1994]。
示例
不要這樣做:
void foo (const base& b)
{
if (typeid(b) == typeid(derived1)) {
do_derived1_stuff();
else if (typeid(b) == typeid(derived2)) {
do_derived2_stuff();
else if () {
}
}
基本原理
舊類型轉換使類型系統失效,可能導致編譯器難以察覺的缺陷:內存管理系統可能會崩潰,虛函數表可能遭嚴重錯誤修改,當對象作為派生類對象訪問時無關對象可能會遭破壞。注意即使讀訪問也可能造成破壞,因為有可能引用了并不存在的指針或區域。 新類型轉換操作符使類型轉換更加安全(多數情況下)、更加明確。
Boolean(布爾)表達式使用新的 bool 類型
不要使用舊的 Boolean 宏或常數:沒有標準的布爾值 true(真);代之以新的 bool 類型。
布爾值 true(真)之間不要直接比較
因為傳統上 true 值 (1 或 !0) 沒有標準值,非零表達式和真值的比較就可能無效。 代之以使用布爾表達式。
示例
不要這樣做:
if (someNonZeroExpression == true)
//可能不會解釋為真值條件
最好這樣做:
if (someNonZeroExpression)
//一定會解釋為一個真值條件
指針不要與不在同一數組中的對象比較
這種操作的結果幾乎都是毫無意義的。
已刪除的對象指針要賦予空指針值
設置已刪除對象的指針為空指針可避免災難的發生:重復刪除非空指針是有害的,但重復刪除空指針是無害的。 即使在函數返回前,刪除操作后也總要賦一個空指針,因為以后可能添加新的代碼。
語句
當從布爾表達式分支時使用 if 語句
當從離散值分支時使用 switch 語句
當分支條件為離散值時使用 switch 語句而不使用一系列的“else if”。
一定為 switch 語句提供一個 default 分支以記錄錯誤
switch 語句應總包含一個 default 分支,default 分支應用以捕獲錯誤。 這一原則保證了當引入新的 switch 值而處理這一新值的分支被刪除時,現有的 default 分支可以捕獲到錯誤。
當循環需要迭代前測試時使用 for 語句或 while 語句
當迭代和循環的結束是根據循環計數器的值時使用 for 語句不使用 while 語句。
當循環需要迭代后測試時使用 do while 語句
循環中避免使用 jump 語句
避免使用除循環結束條件以外的循環退出方式(使用 break、return 或 goto ),不要使用 continue 不成熟的跳轉到下一迭代上。這減少了控制路徑流程的數量,使代碼更易理解。
不要使用 goto 語句
這是一條通用原則。
避免嵌套作用域內隱藏標識符
這會使讀者模糊不清,維護時造成潛在的危險。
第七章
特殊主題
本章為內存管理與錯誤報告提供指南
內存管理
C 和 C++ 內存操作要避免混合使用
C 庫函數 malloc 、calloc 和 realloc 不應用于分配對象空間:此時應使用 C++ 操作符 new。 只有在內存傳遞給C 庫函數處理時,才使用 C 函數分配內存。 不要使用 delete 來釋放由 C 函數分配的內存,或釋放由 new 創建的對象。
刪除由 new 創建的數組對象時總使用 delete[]
使用 delete 刪除數組對象時如果不使用空方括號(“[]”),則只刪除數組的第一個元素,因此導致內存泄漏。
錯誤處理和異常
由于使用 C++ 異常機制的經驗不足,此處的指南將來可能會作較大的改變。 C++ 草擬標準定義了兩大類錯誤:邏輯錯誤和運行錯誤。邏輯錯誤是可避免的編程錯誤。運行錯誤定義為程序范圍外事件導致的錯誤。 使用異常的通用規則為:正常條件下的系統如果沒有重載或硬件錯誤則不應當產生任何異常。
開發過程中多使用聲明語句以發現錯誤
開發過程中使用函數的前置和后置條件聲明語句以發現“倒斃”錯誤。 在實現最終錯誤處理代碼前,聲明語句提供了簡單有用的臨時錯誤監測機制。聲明語句還有額外的好處,使用“NDEBUG”預處理符,可在編譯時去掉這些聲明(請參見“用具體值定義 NDEBUG 預處理符”)。 傳統上使用宏 assert 達到這一目的;但文獻 [Stroustrup, 1994] 提供了一個可替代的模板如下。
示例
template<class T, class Exception>
inline void assert ( T a_boolean_expression,
Exception the_exception)
{
if (! NDEBUG)
if (! a_boolean_expression)
throw the_exception;
}
只在真實的異常情況時使用異常
對經常的、預料中的事件不要使用異常:異常打斷了代碼的正常控制流程,使它難以理解和維護。 預料中的事件應用代碼的正??刂屏鞒烫幚恚恍枰獣r使用函數返回值或“out”參數狀態碼。 異常也不應用于實現控制結構:這會是另一種形式的“goto”語句。
從標準異常中派生項目異常
這保證了所有異常都支持常用操作的最小集合并可由一小組高級處理程序處理。 使用邏輯錯誤(域錯誤、非法實參錯誤、長度錯誤和越界錯誤)表示應用程序域錯誤、函數調用傳遞了非法實參、對象構造超出了它們允許的大小以及實參值不在允許范圍內。 使用運行錯誤(范圍錯誤和溢出錯誤)表示只有在運行時才能查出的算術和配置錯誤、數據已遭破壞或資源耗盡錯誤。
給定抽象盡量少使用異常
大型系統中,每一層都不得不處理大量的異常會使代碼難以閱讀和維護。異常處理可能會嚴重影響了正常處理。 減少使用異常的方法有: 通過使用少量的異常種類在抽象間共享異常。 引發派生于標準異常的特殊異常,處理更通用的異常。 為對象添加“異?!睜顟B,提供明確檢查對象合法性的原語。
將所有異常聲明為已引發
產生異常(而不僅僅是傳遞異常)的函數應在它們的異常規約中將所有異常聲明為已引發:它們不應平靜的產生異常而不警告它們的用戶。
在異常首次出現時就報告它
開發過程中,采用合適的記錄機制盡早報告異常,包括在“異常引發點”。
按照從派生結構最底端到最頂端的順序定義異常處理
定義異常處理要按照從派生結構最底端到最頂端(即從最終子類到最初父類)的順序,這樣可以避免編寫無法到達的處理過程;請參見下面的示例 how-not-to-it。因為是以聲明的順序匹配處理過程的,所以這也保證了異常由最合適的處理過程捕獲。
示例
不要這樣做:
class base { ...};
class derived : public base { ...};
...
try {
...
throw derived(...);
//
//引發一個派生類異常
}
catch (base& a_base_failure)
//
//但基類異常處理過程“捕獲”了它,因為
//基類處理過程首先匹配!
{
...
}
catch (derived& a_derived_failure)
//
//這一處理過程是無法達到的!
{
...
}
避免使用捕獲所有異常的處理過程
避免使用捕獲所有異常的處理過程(處理過程聲明使用 “…”),除非重引發了異常只有在做本地內務管理時才使用捕獲所有異常的處理過程,這時應重引發異常以避免掩蓋了本層不能處理此異常的事實。
try {
...
}
catch (...)
{
if (io.is_open(local_file))
{
io.close(local_file);
}
throw;
}
確保函數狀態代碼有合適的值
當狀態代碼作為函數參數返回時,要賦值給參數作為程序體中的第一個可執行語句。系統的設置所有狀態的默認值為成功或失敗。考慮函數所有可能的出口,包括異常處理。
本地執行安全檢查;不要希望您的客戶會這樣做
如果沒有合適的輸入,函數就會產生錯誤輸出,在函數中裝載代碼以受控方式監測和報告非法輸入。不要依賴于標注來告知客戶傳遞合適的值。事實上標注遲早會被忽略掉,如果檢測到非法參數就會導致難以調試的錯誤。
第八章
可移植性
本章論述先驗不可移植的語言特征。
路徑名
不要使用絕對代碼文件路徑名
各操作系統中的路徑名不是以標準形式表示的。使用它們將會引入對平臺的依賴。
示例
#include "somePath/filename.hh" // Unix
#include "somePath\filename.hh" // MSDOS
數據表示
類型的表示和排列高度依賴于機器的結構。對表示和排列的假設會導致混淆,降低可移植性。
不要假設類型的表示
特別的,不可以在 int 、長整形或任何其他數字類型中存儲指針類型,因為這是高度不可移植的。
不要假設類型的排列
不要依賴于一個特殊的下溢或上溢行為
盡可能使用“可伸縮”常量
可伸縮常量避免了字長變化的問題。
示例
const int all_ones = ~0;
const int last_3_bits = ~0x7;
類型轉換
不要從一個“短”類型轉換成一個“長類型”
機器結構可能指定了某種類型的排列方式。從需要較松散排列的類型轉換為需要較嚴密排列的類型可能會導致程序錯誤。
第九章
復用
本章為 C++ 代碼復用提供指南。
盡可能使用標準庫構件
如果沒有標準庫,則基于標準庫接口創建類,這有利于將來的移植。
使用模板復用獨立于數據的行為
當行為不依賴于一個具體的數據類型時,使用模板復用行為。
使用公共繼承復用類接口(子類型化)
使用 public 繼承表達“is a”關系并復用基類接口,還可有選擇的復用它們的實現。
使用包容而不是私有繼承復用類的實現
當復用實現或建?!安糠?整體”關系時,避免使用私有繼承。復用未重新定義的實現最好由包容而非私有繼承來實現。 當需要重新定義基類的操作時使用私有繼承。
謹慎使用多繼承
多繼承應謹慎使用,因為它帶來了許多額外的復雜性。[Meyers, 1992] 提供了對潛在名稱歧義和重復繼承所帶來復雜性的詳細討論。復雜性來自于: 歧義,當多個類使用相同的名稱時,任何對名稱不加限定的引用天生就是產生歧義的??赏ㄟ^使用類名稱限定其成員名稱來解決歧義的問題。但這也帶來了不幸的后果:使多態無效且將虛函數準變成了靜態綁定函數。 從同一基類重復繼承(派生類通過繼承結構的不同路徑多次繼承一個基類)多組數據成員產生了如下問題:應當使用多組數據成員中的哪一個? 可使用虛繼承(對虛基類的繼承)防止多繼承數據成員。那么為什么不總使用虛繼承呢?虛繼承有負面影響,會改變基礎對象表示并降低訪問的效率。 要求所有的繼承都是虛擬的,同時也就強加了包羅一切的空間,帶來了時間上的低效,實現這樣的政策過于獨斷了。 因此,為了決定是使用虛繼承還是非虛繼承,多繼承需要類的設計者對將來類的使用有敏銳的洞察力。
第十章
編譯問題
本章為編譯問題指南
盡量減少對編譯的依賴
模塊規約中不要包含只是此模塊實施所需的其他頭文件。 避免在只需要指針或引用可見時就為了能看到其他類而在規約中包含頭文件;代之以預先聲明。
示例
//模塊 A 規約,包含于文件“A.hh”中
#include "B.hh" //當只有實施需要時
//不要包含。
#include "C.hh" //只有引用需要時不要包含;
//代之以預先聲明。
class C;
class A
{
C* a_c_by_reference; //有 a 的引用。
};
// “A.hh”結束
注意:
盡量減少對編譯的依賴是某些設計代碼模式或模式的基本原理,如不同的命名:Handle(句柄)或 Envelope(信包)[Meyers, 1992],或 Bridge(橋)[Gamma] 類。將類抽象的責任在兩個關聯類間分割,一個提供類接口,另一個提供類實現;因為任何實現(實現類)上的改變都不再需要客戶重新編譯,因此類與其客戶間的依賴關系就最小化了。
示例
//模塊 A 規約,包含于文件“A.hh”中
class A_implementation;
class A
{
A_implementation* the_implementation;
};
//“A.hh”結束
這一做法也允許接口類和實現類特殊化為兩個單獨的類層次結構。
用具體值定義 NDEBUG
通常使用符號 NDEBUG 在編譯過程中去掉由宏 assert 實現的聲明代碼。傳統做法是當需要消除聲明語句時定義這一符號;但是,開發人員經常沒有意識到聲明的存在,因此沒有定義符號。 我們支持使用聲明的模板版本;這種情況下符號 NDEBUG 必須明確的賦值:需要聲明代碼時為 0;要消除時為非 0。任何沒有提供符號 NDEBUG 以具體值的聲明代碼最終編譯時將產生編譯錯誤;因此,提醒了開發人員注意聲明代碼的存在。
指南總結
以下是本手冊所有指南的總結。
要求或限制
使用常識
通常使用 #include 來訪問模塊的規約
永遠不要聲明以一個或多個下劃線 ('_') 開頭的名稱
將全局聲明限定在名字空間中
明確聲明構造函數的類使用默認構造函數
帶有指針類型數據成員的類要聲明其復制構造函數和賦值操作符
不要重復聲明構造函數參數有默認值
將析構函數總是聲明為 virtual 類型
不要重定義非虛函數
初始化構造函數不要調用成員函數
不能返回引用到局部對象
不可返回由 new 初始化,之后又已解除引用的指針
不要返回非常量引用或指針到成員數據上
令操作符 operator= 返回對 *this 的引用
使 operator= 檢查自賦值
永遠不要忘記常量對象的“不變性”
不要假定任何特殊的表達式計算次序
不要使用舊類型轉換
Boolean(布爾)表達式使用新的 bool 類型
布爾值 true(真)之間不要直接比較
指針不要與不在同一數組中的對象比較
已刪除的對象指針要賦予空指針值
一定為 switch 語句提供一個 default 分支以記錄錯誤
不要使用 goto 語句
C 和 C++ 內存操作要避免混合使用
刪除由 new 創建的數組對象時總使用 delete[]
不要使用絕對代碼文件路徑名
不要假設類型的表示
不要假設類型的排列
不要依賴于一個特殊的下溢或上溢行為
不要從一個“短”類型轉換成一個“長類型”
用具體值定義 NDEBUG
建議
不同文件中放置模塊規約與實現
只選擇一組文件擴展名以區分頭文件和實施文件
每個模塊規約避免定義多個類
避免將私有實施聲明置于模塊的規約中
將模塊內嵌函數的定義置于單獨的文件中
如果程序規模是個要求考慮的問題,就把大的模塊分割成多個變換單元
分離平臺依賴性
防止重復文件包含的保護
對嵌套語句使用小的、一致的縮進風格
相對于函數名或作用域名縮進函數參數
使用能夠適合標準打印紙大小的最大行寬
使用一致換行
使用 C++ 風格的注釋而非 C 風格的注釋
注釋與源代碼盡可能靠攏
避免行末注釋
避免注釋頭
使用空注釋行分離注釋段
避免冗余
編寫自記錄代碼而非注釋
記錄類與函數
選擇一個名稱約定,使用它要前后一致
避免使用只靠字母大小寫才能區分的名稱
避免使用縮寫
避免使用后綴來表明程序語言結構
選擇清晰的、易辨認的、有意義的名稱
使用名稱的正確拼寫
布爾值使用正值謂詞從句
使用名字空間由子系統或庫劃分潛在全局名稱
類的名稱使用名詞或名詞短語
過程類型的函數名稱使用動詞
當需要使用同一通用含義時,使用函數的重載
實參名使用語法元素強調其含義
異常名選用否定的含義
異常名使用項目定義過的形容詞
浮點指數和十六進制數使用大寫字母
使用名字空間劃分非類功能
盡量不使用全局和名字空間范圍的數據
使用 class 而不是 struct 來實現抽象數據類型
以可訪問權限逐次降低的順序聲明類的成員
抽象數據類型避免聲明公共或保護數據成員
使用友元保留封裝性
避免在類聲明中定義函數
避免聲明太多的轉換操作符和單參數構造函數
謹慎使用非虛函數
使用初始化構造函數而不是使用構造函數中的賦值語句
注意構造函數和析構函數調用時的情況
整形類常量使用 static const
一定要明確聲明函數的返回值類型
函數聲明中要提供正規參數名稱
函數要盡量只有一個返回點
避免創建對全局有副作用的函數
以重要性和活躍性遞減的順序聲明函數的參數
避免聲明帶有不定參數個數的函數
避免重復聲明帶有默認參數的函數
函數聲明中盡可能使用 const
避免利用值傳遞對象
宏擴展使用內嵌定義函數不使用 #define
使用默認參數而不使用函數重載
使用函數重載表達常用語義
避免重載以指針和整形數為實參的函數
盡可能減少復雜性
避免使用基本類型
避免使用常量值
定義常量時避免使用 #define 預處理指示符
對象聲明靠近其第一次使用點
聲明時總要初始化 const 對象
定義時初始化對象
當從布爾表達式分支時使用 if 語句
當從離散值分支時使用 switch 語句
當循環需要迭代前測試時使用 for 語句或 while 語句
當循環需要迭代后測試時使用 do while 語句
循環中避免使用 jump 語句
避免嵌套作用域內隱藏標識符
開發過程中多使用聲明語句以發現錯誤
只在真實的異常情況時使用異常
從標準異常中派生項目異常
給定抽象盡量少使用異常
將所有異常聲明為已引發
按照從派生結構最底端到最頂端的順序定義異常處理
避免使用捕獲所有異常的處理過程
確保函數狀態代碼有合適的值
本地執行安全檢查;不要希望您的客戶會這樣做
盡可能使用“可伸縮”常量
盡可能使用標準庫構件
提示
定義項目范圍的全局系統類型
使用 typedef 創建同義詞來加強局部含義
使用冗余的圓括號使復合表達式含義更加清晰
避免表達式的過深嵌套
空指針使用 0 而不使用 NULL
在異常首次出現時就報告它
[Cargill, 92] |
Cargill, Tom.1992. C++ Programming Styles Addison-Wesley.
|
|
[Coplien, 92] |
Coplien, James O. 1992. Advanced C++ Programming Styles and Idioms, Addison-Wesley.
|
|
[Ellemtel, 93] |
Ellemtel Telecommunications Systems Laboratories.June 1993. Programming in C++ Rules and Recommendations.
|
|
[Ellis, 90] |
Ellis, Margaret A. and Stroustrup, Bjarne.1990.The Annotated C++ Reference Manual, Addison-Wesley.
|
|
[Kruchten, 94] |
Kruchten, P. May 1994. Ada Programming Guidelines for the Canadian Automated Air Traffic System.
|
|
[Lippman, 96] |
Lippman, Stanley, B. 1996. Inside the C++ Object Model, Addison-Wesley.
|
|
[Meyers, 92] |
Meyers, Scott.1992. Effective C++, Addison-Wesley.
|
|
[Meyers, 96] |
Meyers, Scott.1996. More Effective C++, Addison-Wesley.
|
|
[Plauger, 95] |
Plauger, P.J. 1995. The Draft Standard C++ Library, Prentice Hall, Inc.
|
|
[Plum, 91] |
Plum, Thomas and Saks, Dan.1991. C++ Programming Guidelines, Plum Hall Inc.
|
|
[Rational, 92] |
Rational Software Corporation, December 1992. Rose C++ Programming Style Guidelines.
|
|
[Stroustrup, 94] |
Stroustrup, Bjarne.1994. The Design and Evolution of C++, Addison-Wesley.
|
|
[X3J16, 95] |
X3J16/95-0087 | WG21/N0687.April 1995. Working Paper for Draft Proposed International Standard for Information Systems-Programming Language C++. |
? 1987 - 2001 Rational Software Corporation。版權所有。
|