本系列是開源書C++ Best Practises[1]的中文版,全書從工具、代碼風(fēng)格、安全性、可維護性、可移植性、多線程、性能、正確性等角度全面介紹了現(xiàn)代C++項目的最佳實踐。本文是該系列的第二篇。
C++最佳實踐:
1. 工具
2. 代碼風(fēng)格(本文)
3.安全性
4.可維護性
5.可移植性及多線程
6.性能
7.正確性和腳本
代碼風(fēng)格
代碼風(fēng)格最重要的是一致性,其次是遵循C++程序員習(xí)慣的閱讀風(fēng)格。
C++允許任意長度的標(biāo)識符名稱,因此在命名時沒必要非要保持簡潔,建議使用描述性名稱,并在風(fēng)格上保持一致。
CamelCase(駝峰命名法)
snake_case(蛇形命名法)
這兩種是很常見的命名規(guī)范,snake_case的優(yōu)點是,在需要的時候可以適配拼寫檢查器。
建立代碼風(fēng)格指南
無論建立什么樣的代碼風(fēng)格指南,一定要實現(xiàn)指定期望風(fēng)格的.clang-format文件。雖然這對命名沒有幫助,但對于開源項目來說,保持一致的風(fēng)格尤為重要。
許多IDE、編輯器都支持內(nèi)置的clang-format,或者可以很方便的通過加載項安裝。
VSCode: Microsoft C/C++ extension for VS Code[2]
CLion: ClangFormat as alternative formatter?
VisualStudio: ClangFormat[3]
Resharper++: Using Clang-Format[4]
Vim
Format your C family code[5]
vim-autoformat[6]
XCode: ClangFormat-Xcode[7]
通用C++命名約定
類以大寫字母開頭: MyClass。
函數(shù)和變量以小寫字母開頭: myMethod。
常量全部大寫: const double PI=3.14159265358979323。
C++標(biāo)準(zhǔn)庫(以及其他著名C++庫,如Boost[8])使用以下指導(dǎo)原則:
宏使用大寫和下劃線: INT_MAX。
模板參數(shù)名使用駝峰命名法: InputIterator。
所有其他名稱都使用蛇形命名法: unordered_map。
區(qū)分私有對象數(shù)據(jù)
使用m_前綴命名私有數(shù)據(jù),以區(qū)別于公共數(shù)據(jù),m_代表“member(成員)”數(shù)據(jù)。
區(qū)分函數(shù)參數(shù)
最重要的是保持代碼庫的一致性,這是一種有助于保持一致性的方式。
使用t_前綴命名函數(shù)參數(shù),t_可以被認為是“the”,但其可以表示任意含義,關(guān)鍵是要將函數(shù)參數(shù)與作用域內(nèi)的其他變量區(qū)分開來,同時遵循一致的命名策略。
可以為團隊選擇任何前綴或后綴,下面是一個例子,提出了一個有爭議的建議,相關(guān)討論見issue #11[9]。
structSize { intwidth; intheight; Size(intt_width,intt_height):width(t_width),height(t_height){} }; //Thisversionmightmakesenseforthreadsafetyorsomething, //butmoretothepoint,sometimesweneedtohidedata,sometimeswedon't. classPrivateSize { public: intwidth()const{returnm_width;} intheight()const{returnm_height;} PrivateSize(intt_width,intt_height):m_width(t_width),m_height(t_height){} private: intm_width; intm_height; };
不要用下劃線(_)作為名字的開頭
_ 開頭的名字有可能與編譯器或標(biāo)準(zhǔn)庫的保留名發(fā)生沖突: What are the rules about using an underscore in a C++ identifier?[10]
良好代碼風(fēng)格示例
classMyClass { public: MyClass(intt_data) :m_data(t_data) { } intgetData()const { returnm_data; } private: intm_data; };
使Out-of-Source-Directory構(gòu)建
確保構(gòu)建生成的文件存放在與源文件夾分離的輸出文件夾中。
使用nullptr
C++11引入了nullptr表示空指針,應(yīng)該用來代替0或NULL來指示空指針。
注釋
注釋塊應(yīng)該使用//,而不是/* */,使用//可以更容易的在調(diào)試時注釋掉代碼塊。
//thisfunctiondoessomething intmyFunc() { }
要在調(diào)試期間注釋掉這個函數(shù)塊,可以這樣做:
/* //thisfunctiondoessomething intmyFunc() { } */
如果函數(shù)頭注釋使用/* */,這么做就會有沖突。
永遠不要在頭文件中使用using namespace
這會導(dǎo)致正在using的命名空間被強行拉入到包含頭文件的所有文件的命名空間中,從而造成命名空間污染,并可能在導(dǎo)致名稱沖突。在實現(xiàn)文件中using命名空間就足夠了。
Include保護符
頭文件必須包含名稱清晰的include保護符,從而避免同一頭文件被多次include的問題,并防止與其他項目的頭文件發(fā)生沖突。
#ifndefMYPROJECT_MYCLASS_HPP #defineMYPROJECT_MYCLASS_HPP namespaceMyProject{ classMyClass{ }; } #endif
此外還可以考慮使用#pragma once指令,這是許多編譯器的準(zhǔn)標(biāo)準(zhǔn),內(nèi)容簡短,意圖明確。
代碼塊必須包含{}
省略{}可能會導(dǎo)致代碼語義錯誤。
//BadIdea //Thiscompilesanddoeswhatyouwant,butcanleadtoconfusing //errorsifmodificationaremadeinthefutureandcloseattention //isnotpaid. for(inti=0;i15;?++i) ??std::cout?<
保持每行代碼長度合理
//BadIdea //hardtofollow if(x&&y&&myFunctionThatReturnsBool()&&caseNumber3&&(15>12||23))?{ } //?Good?Idea //?Logical?grouping,?easier?to?read if?(x?&&?y?&&?myFunctionThatReturnsBool() ????&&?caseNumber3 ????&&?(15?>12||23))?{ }
許多項目和編碼標(biāo)準(zhǔn)都對此制定了軟規(guī)則,即每行字符應(yīng)該少于80或100個,這樣的代碼通常更容易閱讀,此外還可以把兩個文件并排顯示在一個屏幕上,不用小字體也能看到全部代碼。
使用""表示include本地文件
...<>表示include系統(tǒng)文件[11]。
//BadIdea.Requiresextra-Idirectivestothecompiler //andgoesagainststandards. #include#include //WorseIdea //Requirespotentiallyevenmorespecific-Idirectivesand //makescodemoredifficulttopackageanddistribute. #include #include //GoodIdea //Requiresnoextraparamsandnotifiestheuserthatthefile //isalocalfile. #include #include"MyHeader.hpp"
初始化成員變量
...使用成員初始化列表。
對于POD類型,初始化列表的性能與手動初始化相同,但對于其他類型,有明顯的性能提升,見下文。
//BadIdea classMyClass { public: MyClass(intt_value) { m_value=t_value; } private: intm_value; }; //BadIdea //Thisleadstoanadditionalconstructorcallform_myOtherClass //beforetheassignment. classMyClass { public: MyClass(MyOtherClasst_myOtherClass) { m_myOtherClass=t_myOtherClass; } private: MyOtherClassm_myOtherClass; }; //GoodIdea //Thereisnoperformancegainherebutthecodeiscleaner. classMyClass { public: MyClass(intt_value) :m_value(t_value) { } private: intm_value; }; //GoodIdea //Thedefaultconstructorform_myOtherClassisnevercalledhere,so //thereisaperformancegainifMyOtherClassisnotis_trivially_default_constructible. classMyClass { public: MyClass(MyOtherClasst_myOtherClass) :m_myOtherClass(t_myOtherClass) { } private: MyOtherClassm_myOtherClass; };
在C++11中,可以為每個成員初始化默認值(使用=或使用{})。
使用=設(shè)置默認值
//...// private: intm_value=0;//allowed unsignedm_value_2=-1;//narrowingfromsignedtounsignedallowed //...//
這樣可以確保不會出現(xiàn)構(gòu)造函數(shù)“忘記”初始化成員對象的情況。
用大括號初始化默認值
用大括號初始化不允許在編譯時截斷數(shù)據(jù)長度。
//BestIdea //...// private: intm_value{0};//allowed unsignedm_value_2{-1};//narrowingfromsignedtounsignednotallowed,leadstoacompiletimeerror //...//
除非有明確的理由,否則優(yōu)先使用{}初始化,而不是=。
忘記初始化成員會導(dǎo)致未定義行為錯誤,而這些錯誤通常很難發(fā)現(xiàn)。
如果成員變量在初始化后不會更改,則將其標(biāo)記為const。
classMyClass { public: MyClass(intt_value) :m_value{t_value} { } private: constintm_value{0}; };
由于不能給const成員變量賦值,拷貝賦值操作可能對這樣的類沒有意義。
總是使用命名空間
幾乎沒有理由需要全局命名空間中聲明標(biāo)識符。相反,函數(shù)和類應(yīng)該存在于適當(dāng)命名的命名空間中,或者存在于命名空間里的類中。放在全局命名空間中的標(biāo)識符有可能與來自其他庫(主要是沒有命名空間的C庫)的標(biāo)識符發(fā)生沖突。
為標(biāo)準(zhǔn)庫特性使用正確的整數(shù)類型
標(biāo)準(zhǔn)庫通常使用std::size_t來處理與尺寸相關(guān)的內(nèi)容,size_t的大小由實現(xiàn)定義。
一般來說,使用auto可以避免大部分問題。
請確保使用正確的整數(shù)類型,并與C++標(biāo)準(zhǔn)庫保持一致,否則有可能在當(dāng)前使用的平臺上不會發(fā)出警告,但如果切換到其他平臺,可能會發(fā)出警告。
注意,在對無符號數(shù)執(zhí)行某些操作時,可能會導(dǎo)致整數(shù)下溢。例如:
std::vectorv1{2,3,4,5,6,7,8,9}; std::vector v2{9,8,7,6,5,4,3,2,1}; constautos1=v1.size(); constautos2=v2.size(); constautodiff=s1-s2;//diffunderflowstoaverylargenumber
使用.hpp和.cpp作為文件擴展名
歸根結(jié)底,這是個人喜好問題,但是.hpp和.cpp已被各種編輯器和工具廣泛認可。因此,這是一個務(wù)實的選擇。具體來說,Visual Studio只自動識別.cpp和.cxx為C++文件,而Vim不一定會把.cc識別為C++文件。
某個特別大的項目(OpenStudio[12])使用.hpp和.cpp表示用戶生成的文件,而使用.hxx和.cxx表示工具生成的文件。兩者都能被很好的識別,并且區(qū)分開來有很大的幫助。
不要混用tab和空格
某些編輯器喜歡在默認情況下使用tab和空格的混合縮進,這使得沒有使用完全相同的tab縮進設(shè)置的人很難閱讀代碼。請配置好編輯器,確保不會發(fā)生這種情況。
不要將有副作用的代碼放在assert()中
assert(registerSomeThing());//makesurethatregisterSomeThing()returnstrue
上述代碼在debug模式下構(gòu)建時可以成功運行,但在進行release構(gòu)建時會被編譯器刪除,從而造成debug和release構(gòu)建的行為不一致,原因在于assert()是一個宏,它在release模式下展開為空。
不要害怕模板
模板可以幫助我們堅持DRY原則[13]。由于宏有不遵守命名空間等問題,因此能用模板的地方就不要用宏。
明智的使用操作符重載
運算符重載是為了支持表達性語法。比如讓兩個大數(shù)相加看起來像a + b,而不是a.add(b)。另一個常見的例子是std::string,通常使用string1 + string2連接兩個字符串。
但是,使用過多或錯誤的操作符重載很容易寫出可讀性不強的表達式。在重載操作符時,要遵循stackoverflow文章[14]中描述的三條基本規(guī)則。
具體來說,記住以下幾點:
處理資源時必須重載operator=(),參見下面Rule of Zero章節(jié)。
對于所有其他操作符,通常只有在需要在上下文中使用時才重載。典型的場景是用+連接事物,負號可以被認為是“真”或“假”的表達式,等等。
一定要注意操作符優(yōu)先級[15],盡量避免不直觀的結(jié)構(gòu)。
除非實現(xiàn)數(shù)字類型或遵循特定域中可識別的語法,否則不要重載~或%這樣的外部操作符。
永遠不要重載```operator,()```[16](逗號操作符)。
處理流時使用非成員函數(shù)operator>>()和operator<<()。例如,可以重載operator<<(std::ostream &, MyClass const &),從而允許將類“寫入”到一個流中,例如std::cout或std::fstream或std::stringstream,后者通常用于創(chuàng)建值的字符串表示。
這篇文章描述了更多需要重載的常見操作符: What are the basic rules and idioms for operator overloading?[17]。
更多關(guān)于自定義操作符實現(xiàn)細節(jié)的技巧可以參考: C++ Operator Overloading Guidelines[18]。
避免隱式轉(zhuǎn)換
單參數(shù)構(gòu)造函數(shù)
可以在編譯時應(yīng)用單參數(shù)構(gòu)造函數(shù)在類型之間自動轉(zhuǎn)換,比如像std::string(const char *),這樣的轉(zhuǎn)換很方便,但通常應(yīng)該避免,因為可能會增加額外的運行時開銷。
相反,可以將單參數(shù)構(gòu)造函數(shù)標(biāo)記為explicit,從而要求顯式調(diào)用。
轉(zhuǎn)換操作符
與單參數(shù)構(gòu)造函數(shù)類似,編譯器可以調(diào)用轉(zhuǎn)換操作符,同樣也會引入額外開銷,也應(yīng)該被標(biāo)記為explicit。
//badidea structS{ operatorint(){ return2; } }; //goodidea structS{ explicitoperatorint(){ return2; } };
考慮Rule of Zero
Rule of Zero規(guī)定,除非所構(gòu)造的類具有某種新的所有權(quán)形式,否則不提供編譯器可以提供的任何函數(shù)(拷貝構(gòu)造函數(shù)、拷貝賦值操作符、移動構(gòu)造函數(shù)、移動賦值操作符、析構(gòu)函數(shù))。
目標(biāo)是讓編譯器提供在添加更多成員變量時自動維護的最佳版本。
這篇文章介紹了這一原則的背景,并解釋了幾乎可以覆蓋所有情況的實現(xiàn)技術(shù): C++'s Rule of Zero[19]。
審核編輯:彭靜
-
開源
+關(guān)注
關(guān)注
3文章
3309瀏覽量
42471 -
C++
+關(guān)注
關(guān)注
22文章
2108瀏覽量
73618 -
代碼
+關(guān)注
關(guān)注
30文章
4779瀏覽量
68521 -
編輯器
+關(guān)注
關(guān)注
1文章
805瀏覽量
31163
原文標(biāo)題:C++最佳實踐 | 2. 代碼風(fēng)格
文章出處:【微信號:C語言與CPP編程,微信公眾號:C語言與CPP編程】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論