c++代碼整潔之道
整潔的代碼在團(tuán)隊(duì)中無(wú)疑是很受歡迎的,可以高效的被其它成員理解和維護(hù),本文參考《C++代碼整潔之道》和《Google C++編碼規(guī)范》,結(jié)合自己的一些想法整理如下:
C++本身作為面向?qū)ο笳Z(yǔ)言,首先介紹下面向?qū)ο笠话闵婕暗降拈_(kāi)發(fā)原則。
面向?qū)ο箝_(kāi)發(fā)原則
依賴(lài)倒置原則:針對(duì)接口編程,依賴(lài)于抽象而不依賴(lài)于具體,抽象(穩(wěn)定)不應(yīng)依賴(lài)于實(shí)現(xiàn)細(xì)節(jié)(變化),實(shí)現(xiàn)細(xì)節(jié)應(yīng)該依賴(lài)于抽象,因?yàn)榉€(wěn)定態(tài)如果依賴(lài)于變化態(tài)則會(huì)變成不穩(wěn)定態(tài)。
開(kāi)放封閉原則:對(duì)擴(kuò)展開(kāi)放,對(duì)修改關(guān)閉,業(yè)務(wù)需求是不斷變化的,當(dāng)程序需要擴(kuò)展的時(shí)候,不要去修改原來(lái)的代碼,而要靈活使用抽象和繼承,增加程序的擴(kuò)展性,使易于維護(hù)和升級(jí),類(lèi)、模塊、函數(shù)等都是可以擴(kuò)展的,但是不可修改。
單一職責(zé)原則:一個(gè)類(lèi)只做一件事,一個(gè)類(lèi)應(yīng)該僅有一個(gè)引起它變化的原因,并且變化的方向隱含著類(lèi)的責(zé)任。
里氏替換原則:子類(lèi)必須能夠替換父類(lèi),任何引用基類(lèi)的地方必須能透明的使用其子類(lèi)的對(duì)象,開(kāi)放關(guān)閉原則的具體實(shí)現(xiàn)手段之一。
接口隔離原則:接口最小化且完備,盡量少public來(lái)減少對(duì)外交互,只把外部需要的方法暴露出來(lái)。
最少知道原則:一個(gè)實(shí)體應(yīng)該盡可能少的與其他實(shí)體發(fā)生相互作用。
將變化的點(diǎn)進(jìn)行封裝,做好分界,保持一側(cè)變化,一側(cè)穩(wěn)定,調(diào)用側(cè)永遠(yuǎn)穩(wěn)定,被調(diào)用側(cè)內(nèi)部可以變化。
優(yōu)先使用組合而非繼承,繼承為白箱操作,而組合為黑箱,繼承某種程度上破壞了封裝性,而且父類(lèi)與子類(lèi)之間耦合度比較高。
針對(duì)接口編程,而非針對(duì)實(shí)現(xiàn)編程,強(qiáng)調(diào)接口標(biāo)準(zhǔn)化。
C++開(kāi)發(fā)原則
通過(guò)上述面向?qū)ο箝_(kāi)發(fā)原則的理解可以細(xì)化到具體C++開(kāi)發(fā)原則。
保持簡(jiǎn)單和直接原則(KISS, Keep it simple and stupid):保持代碼盡可能簡(jiǎn)單,如果需求需要的話(huà),才在代碼中引入靈活的可變點(diǎn),只添加那些可使整體變得更簡(jiǎn)單的局部復(fù)雜的東西。
不需要原則(YAGNI, You're not gonna need it):總是在你真正需要的時(shí)候再實(shí)現(xiàn)他們,而不是在你只是預(yù)見(jiàn)到你將來(lái)會(huì)需要他們而去實(shí)現(xiàn),在真正需要的時(shí)候再寫(xiě)代碼,那時(shí)再重構(gòu)也來(lái)得及。
避免復(fù)制原則(DRY, Do not repeat yourself):不要復(fù)制,不要重復(fù),這是相當(dāng)危險(xiǎn)的操作,你修改一處代碼的時(shí)候總能記得去修改另外一處或另外多處你曾經(jīng)復(fù)制的代碼嗎?
信息隱藏原則:一段代碼調(diào)用了另外一段代碼,調(diào)用者不應(yīng)該知道被調(diào)用者代碼的實(shí)現(xiàn),否則調(diào)用者就有可能修改被調(diào)用者的實(shí)現(xiàn)來(lái)實(shí)現(xiàn)某些功能,而這有可能引發(fā)其它調(diào)用者的bug。
高內(nèi)聚低耦合原則:類(lèi)似單一職責(zé)原則,明確每個(gè)模塊的具體責(zé)任,盡量少的依賴(lài)于其它模塊。
最少驚訝原則:函數(shù)功能要與函數(shù)名字功能一致,難道你要在一個(gè)getter()函數(shù)去更改成員變量的值嗎?
更干凈原則(自命名):離開(kāi)露營(yíng)地的時(shí)候,應(yīng)讓露營(yíng)地比你來(lái)之前還要干凈,當(dāng)發(fā)現(xiàn)代碼中有需要改進(jìn)或者風(fēng)格不好的地方,應(yīng)該立刻改掉,不要care這段代碼的原作者是誰(shuí),也不要care這是誰(shuí)的模塊,代碼所有權(quán)是集體的,每個(gè)團(tuán)隊(duì)成員在任何時(shí)候都應(yīng)該可以對(duì)任何代碼進(jìn)行更改和擴(kuò)展。
關(guān)于面向?qū)ο笤O(shè)計(jì)原則可以參考一文讓你搞懂設(shè)計(jì)模式
注重單元測(cè)試
重要性就不多說(shuō)了,防患于未然,構(gòu)建大型系統(tǒng)尤其需要進(jìn)行單元測(cè)試,保證代碼質(zhì)量,可以防患于未然。一般都講究測(cè)試驅(qū)動(dòng)開(kāi)發(fā),開(kāi)發(fā)一個(gè)功能首先要想好怎么測(cè)試,先把測(cè)試代碼寫(xiě)好,再去開(kāi)發(fā)對(duì)應(yīng)的需求。通過(guò)單元測(cè)試也有利于開(kāi)發(fā)者更好的進(jìn)行接口的設(shè)計(jì),主要說(shuō)下良好的單元測(cè)試的原則。
單元測(cè)試的原則
保證單元測(cè)試的代碼的質(zhì)量,單元測(cè)試的代碼也是代碼,不應(yīng)該和產(chǎn)品代碼區(qū)別對(duì)待,而且單元測(cè)試的代碼再寫(xiě)出bug更影響測(cè)試效率。
單元測(cè)試的命名, 每個(gè)測(cè)試單元需要根據(jù)具體測(cè)試內(nèi)容進(jìn)行相應(yīng)的命名,方便定位分析問(wèn)題,好的命名如果出現(xiàn)問(wèn)題時(shí)通過(guò)測(cè)試單元的名字基本就可以定位問(wèn)題。
保證單元測(cè)試的獨(dú)立性,每個(gè)測(cè)試單元都是獨(dú)立的,不依賴(lài)于其它測(cè)試單元,不要構(gòu)建測(cè)試單元的上下文,上面的測(cè)試單元出問(wèn)題影響到下面的單元測(cè)試的設(shè)計(jì)是很不友好的。
盡量保證一個(gè)測(cè)試單元使用一個(gè)斷言,保證測(cè)試單元內(nèi)部的一個(gè)相對(duì)獨(dú)立性,上面的斷言阻礙了下面的斷言測(cè)試也是不好的設(shè)計(jì)。
保證單元測(cè)試環(huán)境的獨(dú)立,保證每個(gè)測(cè)試單元都有獨(dú)立的環(huán)境,不依賴(lài)于其它環(huán)境,每個(gè)測(cè)試單元都要是個(gè)獨(dú)立的可運(yùn)行的實(shí)例,每個(gè)單元測(cè)試結(jié)束后記得清理環(huán)境。
沒(méi)必要對(duì)第三方庫(kù)和外部系統(tǒng)做單元測(cè)試,只對(duì)自己寫(xiě)的代碼進(jìn)行測(cè)試。
單元測(cè)試盡量不要涉及數(shù)據(jù)庫(kù),數(shù)據(jù)庫(kù)的狀態(tài)是全局的,測(cè)試不能保證獨(dú)立性,而且數(shù)據(jù)庫(kù)的訪(fǎng)問(wèn)也是緩慢的,影響單元測(cè)試的速度,如果真的需要可以模擬數(shù)據(jù)庫(kù)在內(nèi)容中進(jìn)行測(cè)試,其實(shí)通常是在系統(tǒng)集成和系統(tǒng)測(cè)試級(jí)別時(shí)去測(cè)試數(shù)據(jù)庫(kù)。
不要混淆測(cè)試代碼和產(chǎn)品代碼,產(chǎn)品代碼中不應(yīng)依賴(lài)測(cè)試代碼。
測(cè)試必須要快速執(zhí)行,確保秒級(jí)別,大型系統(tǒng)的單元測(cè)試也就幾分鐘而已,單元測(cè)試不要訪(fǎng)問(wèn)數(shù)據(jù)庫(kù)、磁盤(pán)、網(wǎng)絡(luò)等外設(shè)。
找一些測(cè)試替身,例如有些數(shù)據(jù)需要通過(guò)網(wǎng)絡(luò)獲取,那可以利用依賴(lài)注入做一個(gè)網(wǎng)絡(luò)替身的類(lèi)模擬這些數(shù)據(jù)的產(chǎn)生,可以研究研究Google mock。
良好的命名
無(wú)論是什么語(yǔ)言,函數(shù)和變量的良好命名都是很有必要的,通過(guò)函數(shù)的名字我們就可以知道這個(gè)函數(shù)里代碼的作用,而不是通過(guò)寫(xiě)注釋?zhuān)瑐€(gè)人一直傾向于用代碼自解釋。
文件命名
文件名字要全部小寫(xiě),中間用_相連,后綴名為.cc和.h
類(lèi)型命名
類(lèi)型名稱(chēng)的每個(gè)單詞首字母均大寫(xiě), 不包含下劃線(xiàn): MyExcitingClass, MyExcitingEnum.
變量命名
不要將變量的類(lèi)型在名字中體現(xiàn),這樣以后變量類(lèi)型改變的話(huà)還需要去改動(dòng)變量名,充分利用IDE的功能,變量 (包括函數(shù)參數(shù)) 和數(shù)據(jù)成員名一律小寫(xiě), 單詞之間用下劃線(xiàn)連接. 類(lèi)的成員變量以下劃線(xiàn)結(jié)尾, 但結(jié)構(gòu)體的就不用, 如: a_local_variable, a_struct_data_member, a_class_data_member_.
class TableInfo {
...
private:
string table_name_; // 好 - 后加下劃線(xiàn).
string tablename_; // 好.
static Pool<TableInfo>* pool_; // 好.
int i_table; // 不好,不要將變量的類(lèi)型在名字中體現(xiàn)
};
常量命名
聲明為 constexpr 或 const 的變量, 或在程序運(yùn)行期間其值始終保持不變的, 命名時(shí)以 “k” 開(kāi)頭, 大小寫(xiě)混合
const int kDaysInAWeek = 7;
函數(shù)命名
常規(guī)函數(shù)使用大小寫(xiě)混合, 取值和設(shè)值函數(shù)則要求與變量名匹配: MyExcitingFunction(), MyExcitingMethod(), my_exciting_member_variable(), set_my_exciting_member_variable().
枚舉命名
和常量一致
enum UrlTableErrors {
kOK = 0,
kErrorOutOfMemory,
kErrorMalformedInput,
};
Tip:
除非像swap函數(shù)里tmp那種一目了然,否則不要搞無(wú)意義的命名,函數(shù)名變量名字寧可特別長(zhǎng)也要寫(xiě)清楚究竟是什么意思,不要用縮寫(xiě),一個(gè)變量盡量在臨近使用前才定義,可讀性強(qiáng)也可更好利用cpu cache。
編輯器
團(tuán)隊(duì)可以統(tǒng)一使用相同的編輯器,個(gè)人目前使用的是VS Code編輯器,同時(shí)每個(gè)項(xiàng)目使用統(tǒng)一的.clang_format文件,統(tǒng)一規(guī)范代碼格式,所有的換行符都要用LF格式,不要用CRLF格式,在右下角可以設(shè)置。
個(gè)人的.clang-format文件如下,是在google風(fēng)格的基礎(chǔ)上做了些修改:
BasedOnStyle: Google
IndentWidth: 4
ColumnLimit: 120
SortIncludes: true
MaxEmptyLinesToKeep: 2
C++編碼規(guī)范要點(diǎn)小總結(jié)
每個(gè)頭文件都要使用#define避免被重復(fù)引用
命名格式 <PROJECT>_<PATH>_<FILE>_H_
...
或使用#pragma once,而#define方式更通用
鼓勵(lì)在 .cc 文件內(nèi)使用匿名命名空間或 static 聲明. 使用具名的命名空間時(shí), 其名稱(chēng)可基于項(xiàng)目名或相對(duì)路徑. 禁止使用 using 指示, 禁止使用內(nèi)聯(lián)命名空間(inline namespace)
一行盡量不要超過(guò)120個(gè)字符,一個(gè)函數(shù)盡量不要超過(guò)40行,同時(shí)一個(gè)文件盡量控制在500行內(nèi).
所有的引用形參如不做改動(dòng)一律加const,在任何可能的情況下都要使用 const或constexpr
new內(nèi)存的地方盡量使用智能指針,c++11 就盡量用std::unique_ptr替代std::auto_ptr
合理使用移動(dòng)語(yǔ)義,減少內(nèi)存拷貝,參考左值引用、右值引用、移動(dòng)語(yǔ)義、完美轉(zhuǎn)發(fā),你知道的不知道的都在這里
禁止使用 RTTI,盡量在編譯期間就確定參數(shù)類(lèi)型,不要搞運(yùn)行時(shí)識(shí)別typeid這種代碼
使用 C++ 的類(lèi)型轉(zhuǎn)換, 如 static_cast<>(). 不要使用 int y = (int)x 或 int y = int(x) 等轉(zhuǎn)換方式
明確使用前置++還是后置++的具體含義,如不考慮返回值,盡量使用效率高的前置++ (++i)
不要使用uint類(lèi)型,如果需要使用大整型可以考慮int64,否則類(lèi)型的隱式類(lèi)型轉(zhuǎn)換會(huì)帶來(lái)很多麻煩
如無(wú)特殊必要不要使用宏,可以考慮使用const或constexpr替代宏,宏的全局作用域很麻煩,如果非要用在馬上要使用時(shí)才進(jìn)行 #define, 使用后要立即 #undef
google文檔說(shuō)一定不要用宏來(lái)控制條件編譯(但是我自己還沒(méi)有查到不用宏如何控制條件編譯,或許就不要搞條件編譯)
盡可能用 sizeof(varname) 代替 sizeof(type).使用 sizeof(varname) 是因?yàn)楫?dāng)代碼中變量類(lèi)型改變時(shí)會(huì)自動(dòng)更新. 您或許會(huì)用 sizeof(type) 處理不涉及任何變量的代碼,比如處理來(lái)自外部或內(nèi)部的數(shù)據(jù)格式,這時(shí)用變量就不合適了
類(lèi)型名如果過(guò)長(zhǎng)的話(huà)可以考慮使用auto關(guān)鍵字
注釋統(tǒng)一使用 // ,不要通過(guò)注釋禁用代碼,擅用git,不要為易懂的代碼寫(xiě)注釋
寫(xiě)完代碼后記得format,VS Code(windows快捷鍵) shift + alt + F ,每個(gè)項(xiàng)目最好都有統(tǒng)一的.clang_format文件
使用C++的string和stream替代C語(yǔ)言風(fēng)格的char*,使用std::ostream和std::cout替代printf()、sprintf()等
盡量使用STL標(biāo)準(zhǔn)庫(kù)的容器而不是C語(yǔ)言風(fēng)格的數(shù)組,數(shù)組的越界訪(fǎng)問(wèn)之類(lèi)當(dāng)時(shí)是不會(huì)報(bào)錯(cuò)的,反而可能弄臟堆棧信息,導(dǎo)致奇奇怪怪難以排查的bug
可以更多的使用模板元編程,盡量多的使用constexpr等編譯器計(jì)算,編譯器是我們的好搭檔,個(gè)人認(rèn)為模板元編程以后會(huì)是C++的主流技術(shù)
可以考慮更多的使用異常處理方式,而不是C語(yǔ)言風(fēng)格的errno錯(cuò)誤碼等,這里可以參考你的c++團(tuán)隊(duì)還在禁用異常處理嗎?
附:本文不是技術(shù)文章,介紹較為主觀,可能和很多人想法有所沖突,各位可以結(jié)合自己的經(jīng)歷經(jīng)驗(yàn)酌情參考。
參考資料
《C++代碼整潔之道》
https://zh-google-styleguide.readthedocs.io/en/latest/google-cpp-styleguide/contents/
REVIEW
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺(tái)僅提供信息存儲(chǔ)服務(wù)。文章僅代表作者個(gè)人觀點(diǎn),不代表本平臺(tái)立場(chǎng),如有問(wèn)題,請(qǐng)聯(lián)系我們,謝謝!