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