C++最佳實踐 | 2. 程式碼風格
本系列是開源書C++ Best Practises的中文版,全書從工具、程式碼風格、安全性、可維護性、可移植性、多執行緒、效能、正確性等角度全面介紹了現代C++專案的最佳實踐。本文是該系列的第二篇。
程式碼風格
程式碼風格最重要的是一致性,其次是遵循C++程式設計師習慣的閱讀風格。
C++允許任意長度的識別符號名稱,因此在命名時沒必要非要保持簡潔,建議使用描述性名稱,並在風格上保持一致。
CamelCase
(駝峰命名法)snake_case
(蛇形命名法)
這兩種是很常見的命名規範,snake_case
的優點是,在需要的時候可以適配拼寫檢查器。
建立程式碼風格指南
無論建立什麼樣的程式碼風格指南,一定要實現指定期望風格的.clang-format
檔案。雖然這對命名沒有幫助,但對於開源專案來說,保持一致的風格尤為重要。
許多IDE、編輯器都支援內建的clang-format,或者可以很方便的通過載入項安裝。
- VSCode: Microsoft C/C++ extension for VS Code
- CLion: ClangFormat as alternative formatter
- VisualStudio: ClangFormat
- Resharper++: Using Clang-Format
- Vim
- Format your C family code
- vim-autoformat
- XCode: ClangFormat-Xcode
通用C++命名約定
- 類以大寫字母開頭:
MyClass
。 - 函式和變數以小寫字母開頭:
myMethod
。 - 常量全部大寫:
const double PI=3.14159265358979323
。
C++標準庫(以及其他著名C++庫,如Boost)使用以下指導原則:
- 巨集使用大寫和下劃線:
INT_MAX
。 - 模板引數名使用駝峰命名法:
InputIterator
。 - 所有其他名稱都使用蛇形命名法:
unordered_map
。
區分私有物件資料
使用m_
字首命名私有資料,以區別於公共資料,m_
代表“member(成員)”資料。
區分函式引數
最重要的是保持程式碼庫的一致性,這是一種有助於保持一致性的方式。
使用t_
字首命名函式引數,t_
可以被認為是“the”,但其可以表示任意含義,關鍵是要將函式引數與作用域內的其他變數區分開來,同時遵循一致的命名策略。
可以為團隊選擇任何字首或字尾,下面是一個例子,提出了一個有爭議的建議,相關討論見issue #11。
```cpp struct Size { int width; int height;
Size(int t_width, int t_height) : width(t_width), height(t_height) {} };
// This version might make sense for thread safety or something, // but more to the point, sometimes we need to hide data, sometimes we don't. class PrivateSize { public: int width() const { return m_width; } int height() const { return m_height; } PrivateSize(int t_width, int t_height) : m_width(t_width), m_height(t_height) {}
private: int m_width; int m_height; }; ```
不要用下劃線(_)作為名字的開頭
_ 開頭的名字有可能與編譯器或標準庫的保留名發生衝突: What are the rules about using an underscore in a C++ identifier?
良好程式碼風格示例
```cpp class MyClass { public: MyClass(int t_data) : m_data(t_data) { }
int getData() const { return m_data; }
private: int m_data; }; ```
使Out-of-Source-Directory構建
確保構建生成的檔案存放在與原始檔夾分離的輸出資料夾中。
使用nullptr
C++11引入了nullptr
表示空指標,應該用來代替0
或NULL
來指示空指標。
註釋
註釋塊應該使用//
,而不是/* */
,使用//
可以更容易的在除錯時註釋掉程式碼塊。
cpp
// this function does something
int myFunc()
{
}
要在除錯期間註釋掉這個函式塊,可以這樣做:
cpp
/*
// this function does something
int myFunc()
{
}
*/
如果函式頭註釋使用/* */
,這麼做就會有衝突。
永遠不要在標頭檔案中使用using namespace
這會導致正在using
的名稱空間被強行拉入到包含標頭檔案的所有檔案的名稱空間中,從而造成名稱空間汙染,並可能在導致名稱衝突。在實現檔案中using
名稱空間就足夠了。
Include保護符
標頭檔案必須包含名稱清晰的include保護符,從而避免同一標頭檔案被多次include的問題,並防止與其他專案的標頭檔案發生衝突。
```cpp
ifndef MYPROJECT_MYCLASS_HPP
define MYPROJECT_MYCLASS_HPP
namespace MyProject { class MyClass { }; }
endif
```
此外還可以考慮使用#pragma once
指令,這是許多編譯器的準標準,內容簡短,意圖明確。
程式碼塊必須包含{}
省略{}
可能會導致程式碼語義錯誤。
```cpp // Bad Idea // This compiles and does what you want, but can lead to confusing // errors if modification are made in the future and close attention // is not paid. for (int i = 0; i < 15; ++i) std::cout << i << std::endl;
// Bad Idea // The cout is not part of the loop in this case even though it appears to be. int sum = 0; for (int i = 0; i < 15; ++i) ++sum; std::cout << i << std::endl;
// Good Idea // It's clear which statements are part of the loop (or if block, or whatever). int sum = 0; for (int i = 0; i < 15; ++i) { ++sum; std::cout << i << std::endl; } ```
保持每行程式碼長度合理
```cpp // Bad Idea // hard to follow if (x && y && myFunctionThatReturnsBool() && caseNumber3 && (15 > 12 || 2 < 3)) { }
// Good Idea // Logical grouping, easier to read if (x && y && myFunctionThatReturnsBool() && caseNumber3 && (15 > 12 || 2 < 3)) { } ```
許多專案和編碼標準都對此制定了軟規則,即每行字元應該少於80或100個,這樣的程式碼通常更容易閱讀,此外還可以把兩個檔案並排顯示在一個螢幕上,不用小字型也能看到全部程式碼。
使用""
表示include本地檔案
...<>
表示include系統檔案。
```cpp // Bad Idea. Requires extra -I directives to the compiler // and goes against standards.
include
include
// Worse Idea // Requires potentially even more specific -I directives and // makes code more difficult to package and distribute.
include
include
// Good Idea // Requires no extra params and notifies the user that the file // is a local file.
include
include "MyHeader.hpp"
```
初始化成員變數
...使用成員初始化列表。
對於POD型別,初始化列表的效能與手動初始化相同,但對於其他型別,有明顯的效能提升,見下文。
```cpp // Bad Idea class MyClass { public: MyClass(int t_value) { m_value = t_value; }
private: int m_value; };
// Bad Idea // This leads to an additional constructor call for m_myOtherClass // before the assignment. class MyClass { public: MyClass(MyOtherClass t_myOtherClass) { m_myOtherClass = t_myOtherClass; }
private: MyOtherClass m_myOtherClass; };
// Good Idea // There is no performance gain here but the code is cleaner. class MyClass { public: MyClass(int t_value) : m_value(t_value) { }
private: int m_value; };
// Good Idea // The default constructor for m_myOtherClass is never called here, so // there is a performance gain if MyOtherClass is not is_trivially_default_constructible. class MyClass { public: MyClass(MyOtherClass t_myOtherClass) : m_myOtherClass(t_myOtherClass) { }
private: MyOtherClass m_myOtherClass; }; ```
在C++11中,可以為每個成員初始化預設值(使用=
或使用{}
)。
使用=
設定預設值
cpp
// ... //
private:
int m_value = 0; // allowed
unsigned m_value_2 = -1; // narrowing from signed to unsigned allowed
// ... //
這樣可以確保不會出現建構函式“忘記”初始化成員物件的情況。
用大括號初始化預設值
用大括號初始化不允許在編譯時截斷資料長度。
```cpp // Best Idea
// ... // private: int m_value{ 0 }; // allowed unsigned m_value_2 { -1 }; // narrowing from signed to unsigned not allowed, leads to a compile time error // ... // ```
除非有明確的理由,否則優先使用{}
初始化,而不是=
。
忘記初始化成員會導致未定義行為錯誤,而這些錯誤通常很難發現。
如果成員變數在初始化後不會更改,則將其標記為const
。
```cpp class MyClass { public: MyClass(int t_value) : m_value{t_value} { }
private: const int m_value{0}; }; ```
由於不能給const成員變數賦值,拷貝賦值操作可能對這樣的類沒有意義。
總是使用名稱空間
幾乎沒有理由需要全域性名稱空間中宣告識別符號。相反,函式和類應該存在於適當命名的名稱空間中,或者存在於名稱空間裡的類中。放在全域性名稱空間中的識別符號有可能與來自其他庫(主要是沒有名稱空間的C庫)的識別符號發生衝突。
為標準庫特性使用正確的整數型別
標準庫通常使用std::size_t
來處理與尺寸相關的內容,size_t
的大小由實現定義。
一般來說,使用auto
可以避免大部分問題。
請確保使用正確的整數型別,並與C++標準庫保持一致,否則有可能在當前使用的平臺上不會發出警告,但如果切換到其他平臺,可能會發出警告。
注意,在對無符號數執行某些操作時,可能會導致整數下溢。例如:
cpp
std::vector<int> v1{2,3,4,5,6,7,8,9};
std::vector<int> v2{9,8,7,6,5,4,3,2,1};
const auto s1 = v1.size();
const auto s2 = v2.size();
const auto diff = s1 - s2; // diff underflows to a very large number
使用.hpp
和.cpp
作為副檔名
歸根結底,這是個人喜好問題,但是.hpp和.cpp已被各種編輯器和工具廣泛認可。因此,這是一個務實的選擇。具體來說,Visual Studio只自動識別.cpp和.cxx為C++檔案,而Vim不一定會把.cc識別為C++檔案。
某個特別大的專案(OpenStudio)使用.hpp和.cpp表示使用者生成的檔案,而使用.hxx和.cxx表示工具生成的檔案。兩者都能被很好的識別,並且區分開來有很大的幫助。
不要混用tab和空格
某些編輯器喜歡在預設情況下使用tab和空格的混合縮排,這使得沒有使用完全相同的tab縮排設定的人很難閱讀程式碼。請配置好編輯器,確保不會發生這種情況。
不要將有副作用的程式碼放在assert()中
cpp
assert(registerSomeThing()); // make sure that registerSomeThing() returns true
上述程式碼在debug模式下構建時可以成功執行,但在進行release構建時會被編譯器刪除,從而造成debug和release構建的行為不一致,原因在於assert()
是一個巨集,它在release模式下展開為空。
不要害怕模板
模板可以幫助我們堅持DRY原則。由於巨集有不遵守名稱空間等問題,因此能用模板的地方就不要用巨集。
明智的使用操作符過載
運算子過載是為了支援表達性語法。比如讓兩個大數相加看起來像a + b
,而不是a.add(b)
。另一個常見的例子是std::string
,通常使用string1 + string2
連線兩個字串。
但是,使用過多或錯誤的操作符過載很容易寫出可讀性不強的表示式。在過載操作符時,要遵循stackoverflow文章中描述的三條基本規則。
具體來說,記住以下幾點:
- 處理資源時必須過載
operator=()
,參見下面Rule of Zero章節。 - 對於所有其他操作符,通常只有在需要在上下文中使用時才過載。典型的場景是用+連線事物,負號可以被認為是“真”或“假”的表示式,等等。
- 一定要注意操作符優先順序,儘量避免不直觀的結構。
- 除非實現數字型別或遵循特定域中可識別的語法,否則不要過載~或%這樣的外部操作符。
- 永遠不要過載
operator,()
(逗號操作符)。 - 處理流時使用非成員函式
operator>>()
和operator<<()
。例如,可以過載operator<<(std::ostream &, MyClass const &)
,從而允許將類“寫入”到一個流中,例如std::cout
或std::fstream
或std::stringstream
,後者通常用於建立值的字串表示。 - 這篇文章描述了更多需要過載的常見操作符: What are the basic rules and idioms for operator overloading?。
更多關於自定義操作符實現細節的技巧可以參考: C++ Operator Overloading Guidelines。
避免隱式轉換
單引數建構函式
可以在編譯時應用單引數建構函式在型別之間自動轉換,比如像std::string(const char *)
,這樣的轉換很方便,但通常應該避免,因為可能會增加額外的執行時開銷。
相反,可以將單引數建構函式標記為explicit
,從而要求顯式呼叫。
轉換操作符
與單引數建構函式類似,編譯器可以呼叫轉換操作符,同樣也會引入額外開銷,也應該被標記為explicit
。
```cpp //bad idea struct S { operator int() { return 2; } };
//good idea struct S { explicit operator int() { return 2; } }; ```
考慮Rule of Zero
Rule of Zero規定,除非所構造的類具有某種新的所有權形式,否則不提供編譯器可以提供的任何函式(拷貝建構函式、拷貝賦值操作符、移動建構函式、移動賦值操作符、解構函式)。
目標是讓編譯器提供在新增更多成員變數時自動維護的最佳版本。
這篇文章介紹了這一原則的背景,並解釋了幾乎可以覆蓋所有情況的實現技術: C++'s Rule of Zero。
你好,我是俞凡,在Motorola做過研發,現在在Mavenir做技術工作,對通訊、網路、後端架構、雲原生、DevOps、CICD、區塊鏈、AI等技術始終保持著濃厚的興趣,平時喜歡閱讀、思考,相信持續學習、終身成長,歡迎一起交流學習。 \ 微信公眾號:DeepNoMind
- Ikigai: 享受生命的意義
- 5分鐘搞懂BFF
- 無程式碼的未來
- Rush vs C 深度比較
- 10分鐘開發Kubernetes Operator
- SDN系統方法 | 3. 基本架構
- 從 IaC 到 IaD
- C 最佳實踐 | 2. 程式碼風格
- Git進階系列 | 5. Rebase vs Merge
- Git 進階系列 | 4. 合併衝突
- Git進階系列 | 6. 互動式Rebase
- Git進階系列 | 3. 基於Pull Request實現更好的協作
- Git進階系列 | 1. 建立完美的提交
- GitOps指南
- 軟體架構的23個基本原則
- 面向快速反應的工程團隊--QRF團隊模型
- 自動化的藝術
- GitOps的12個痛點
- 微服務分散式事務處理
- 架構師成長路線圖