提供的內容深入探討了C++編程中的一些關鍵概念,特別是如何編寫清晰、易維護的代碼,并展示了一些C++17的新特性。我將對這些內容做中文的解釋和總結。
1. 良好的代碼設計原則
什么是“良好的代碼”?
- 能工作:代碼實現了預期功能。
- 能在其他編譯器或平臺上運行:跨平臺的代碼很重要。
- 能預先回答常見問題:處理好內存管理、邊界情況等問題。
- 富有表現力:代碼應該易于理解,能清晰表達其目的。
- 透明且具有溝通性:代碼易于他人理解,代碼意圖清晰。
這些內容強調了寫清晰且可維護的代碼的重要性,不僅僅是代碼是否能正確工作。
2. Roger Orr 喜愛的代碼片段
通過代碼示例,可以對比如何通過一些小的改進讓代碼變得更加清晰和易于擴展。
原始版本:
class Holder
{
private:int number;
public:Holder(int i);Holder();void inc() { number++; }int getNumber() { return number; }std::string to_string();
};
- 問題:
getNumber
方法沒有標記為const
,to_string
方法也沒有標記為const
或者是override
。 - 缺乏清晰的意圖:沒有明確表明哪些方法會修改類的狀態。
改進版本:
class Holder
{
private:int number;
public:explicit Holder(int i); // 防止隱式轉換Holder();void inc() { number++; }int getNumber() const { return number; } // 表明不會修改對象狀態virtual std::string to_string() const; // 用const和virtual標記,確保可以重寫
};
- 改進點:
explicit
: 防止構造函數進行隱式類型轉換,從而避免一些難以發現的錯誤。const
:getNumber
和to_string
方法標記為const
,表示這些方法不會改變對象的狀態。virtual
:to_string
方法標記為virtual
,確保子類能夠重寫該方法。
3. C++ 中的對立概念
C++中許多構造都成對出現或者互為對立。我們來看看一些常見的例子。
操作符和括號:
- 操作符:
- 算術:
+
(加法)與*
(乘法) - 指針:
*
(解引用)與&
(取地址)
- 算術:
- 括號:
()
(函數調用){}
(代碼塊/作用域)[]
(數組下標)<>
(模板參數列表)
對立的關鍵字:
if
/else
: 條件執行。noexcept
/noexcept(false)
:noexcept
用于聲明一個函數不會拋出異常,noexcept(false)
用于聲明該函數可能拋出異常。
沒有對立的部分:
一些C++構造沒有直接的“對立”或“反義詞”,如:
break
、continue
、return
、foo(x)
、while
、for
、switch
等控制流語句沒有對立的概念。
4. C++ 17的新屬性(Attributes)
C++17引入了一些新屬性(attributes),比如[[fallthrough]]
、[[maybe_unused]]
、[[nodiscard]]
,它們有助于改善代碼質量和清晰度。
[[fallthrough]]
在 switch
語句中:
有時你希望故意跳過break
語句,使代碼從一個case
順利執行到下一個case
。
switch (i)
{
case 1:
case 2:msg += "case 1 or case 2. ";break;
case 3:msg += "case 3 or ";[[fallthrough]]; // 明確標記是故意的fallthrough
case 4:msg += "case 4.";
default:break;
}
- 沒有
[[fallthrough]]
,編譯器可能會警告你關于不小心遺漏break
的情況,而加上[[fallthrough]]
則表明這是故意的。
[[maybe_unused]]
:
用于那些可能未被使用的變量或函數,但不希望編譯器發出警告。
[[maybe_unused]] int j = FunctionWithSideEffects();
assert(j > 0);
- 即使
j
可能未使用,編譯器也不會對此發出警告。
[[nodiscard]]
:
此屬性幫助確保函數的返回值不會被忽略。
[[nodiscard]] int getNumber() { return 42; }
auto num = getNumber(); // 正常使用
getNumber(); // 編譯器會警告:返回值被丟棄
- **
[[nodiscard]]
**會在函數的返回值被忽略時觸發警告,提醒開發者該返回值可能很重要。
5. 其他重要概念
explicit
:
explicit
關鍵字用于防止構造函數進行隱式轉換,這樣可以避免一些潛在的錯誤。
const
和 mutable
:
const
:表示方法不會修改對象的狀態,常用于常成員函數或者常量變量。mutable
:即使在const
對象中,某些成員變量可以被修改,通常用于緩存等需要在不改變對象狀態的情況下修改的變量。
引用限定符(Ref-qualifiers):
引用限定符用于指定一個函數只能在特定的對象類型(左值或右值)上被調用。
void foo() & { /* 只能在左值上調用 */ }
void foo() && { /* 只能在右值上調用 */ }
總結
explicit
與 隱式轉換:explicit
防止隱式轉換,從而避免一些潛在的隱式轉換錯誤。const
和mutable
:const
確保方法不會修改對象狀態,而mutable
用于允許某些成員在const
對象中被修改。[[fallthrough]]
、[[maybe_unused]]
、[[nodiscard]]
:這些C++17屬性幫助清晰地表達代碼意圖,并讓編譯器能給出有用的警告。virtual
和override
:通過標記方法為virtual
,確保子類能夠重寫該方法;而override
則確保方法正確地重寫了基類方法。
這些特性和原則幫助我們寫出表達性強、可維護且易于理解的代碼。同時,C++17引入的屬性增強了編譯器的優化能力和錯誤檢查功能,使得代碼更健壯。
你提供的內容主要是關于如何通過清晰地表達意圖來編寫更易理解和維護的C++代碼。以下是對這些內容的中文解釋和總結:
1. 如何更清楚地表達意圖?
避免默認值(Avoid defaults)
- 不使用默認參數值:在函數或者類的構造函數中,最好明確指定每個參數,不使用隱式的默認值。這樣可以避免一些意外的行為,讓代碼更易理解。
在類或結構體中始終明確指定 public:
和 private:
- 即使是一個簡單的兩元素結構體(如
Point
),也應明確標注public:
和private:
區域。struct Point {int x, y; public:Point(int x, int y) : x(x), y(y) {} private:void privateMethod() {} };
- 意圖:明確告知其他開發者哪些成員是公開的,哪些是私有的。
在 void
函數中加上 return
- 即便是
void
函數,最好在函數末尾顯式添加return
,這表明你有意結束函數執行。void someFunction() {// Do somethingreturn; }
使用那些可選項(Use optional things)
- 標記虛函數的重寫:通過
override
明確標示函數是重寫了基類的方法。class Base {virtual void foo() {} }; class Derived : public Base {void foo() override {} // 明確表明這是對基類方法的重寫 };
noexcept
:如果你明確認為函數不會拋出異常,使用noexcept
進行標注,增加可讀性并避免誤解。void someFunction() noexcept {// No exceptions expected }
- 意義:雖然這些關鍵字(如
override
和noexcept
)不一定是必須的,但它們提供了重要的意圖信息,幫助讀者理解代碼的設計和限制。
2. 表達意圖的極限
在表達意圖時,有時我們需要在代碼中做出平衡。雖然有很多關鍵字可以使用,但有些關鍵字并不一定出現在C++中,我們也不一定需要它們。
不常見的關鍵字:
implicit
:C++ 沒有類似implicit
的關鍵字來表示隱式轉換。const(false)
:沒有這個關鍵字來表示“不可修改”。nonvirtual
:C++ 沒有類似nonvirtual
的關鍵字來禁止虛函數。ByVal
:C++ 中也沒有ByVal
關鍵字來表示按值傳遞。
我們該如何處理這些情況?- 可以通過清晰的命名和注釋來表達意圖。例如,“我知道自己在做什么,請不要修改此部分”。
3. 上下文的意義
缺少關鍵字意味著什么?
- 第一種情況:表示“我已經考慮過這個問題,因此不需要使用關鍵字”。
- 第二種情況:表示“我從未聽說過這個關鍵字,或者至少沒考慮過它是否應該在此使用”。
如何傳達給讀者:如果你在代碼庫中始終如一地使用某些關鍵字,那么讀者可以推測你已經考慮過它們的使用。例如,如果你在每個虛函數上都使用override
,那么讀者可以很清楚地理解你有意標明這個函數是重寫的。
4. 注釋的使用
- 注釋:注釋應僅用于那些可能讓讀者誤解的地方,而不是在每個函數旁邊都加上不必要的注釋。例如,
foo
方法看起來像是一個虛擬函數的重寫,但它可能只是一個簽名不同的函數。在這種情況下,你可以加上一些注釋來澄清。// 我知道這看起來像是 foo 的重寫,但實際上這是一個不同簽名的函數
- 注釋不是表達意圖的主要方式,而是用于澄清可能的誤解。
5. 可選的返回語句
在 void
函數中,盡管返回值是 void
類型,但最好明確地加上 return
語句,以表明函數的結束。
void Thimbule(int robbit)
{robbit++;if (robbit)return;robbit--;
}
void Sprial(int oob, int boo)
{oob++;while (true){if (++oob > boo)return;}
}
- 可選返回語句的好處:在一些復雜的條件分支中,顯式的
return
會使代碼更加清晰,避免不必要的復雜性。
6. 范圍 for
循環(Ranged For)
- 按值傳遞(
auto emp : department
):for (auto emp : department) {// ... }
- 適用于當你不需要修改元素時,復制元素的副本。
- 按引用傳遞(
auto& emp : department
):for (auto& emp : department) {// ... }
- 用于避免不必要的復制,直接操作元素的引用。
- 常量引用(
auto const & emp : department
):for (auto const & emp : department) {// ... }
- 如果不需要修改元素且希望避免復制,可以使用常量引用。
7. 參數傳遞
如何傳遞參數?
- 按值傳遞(
Order createOrder(Customer c, OrderItem oi);
)- 適用于參數較小或者需要復制的情況。
- 按引用傳遞(
Order createOrder(Customer& c, OrderItem oi);
)- 適用于對象較大,且希望避免復制的情況。
- 按常量引用傳遞(
Order createOrder(Customer const& c, OrderItem oi);
)- 如果不需要修改對象,并且希望避免復制,這是最佳選擇。
8. 省略參數名稱
- 在聲明時,參數名稱可以省略,編譯器并不關心。但人類開發者會關心,因此最好不要省略參數名稱。尤其是在定義函數時,如果某個參數未使用,可以在定義中省略其名稱:
int DetermineTotalTaxes(int, int, int);
- 但是,在定義時,如果某個參數不使用,最好加上注釋,說明這樣做的原因,避免其他人誤解。
int DetermineTotalTaxes(int ProvRate, int FedRate, int) {// do somethingreturn 42; }
總結
- 表達意圖的清晰性是編寫可維護代碼的關鍵。通過使用適當的關鍵字(如
override
、noexcept
等),標明函數行為,幫助其他開發者更容易理解代碼的設計。 - 明確代碼意圖:盡量避免默認值,確保參數傳遞方式清晰,函數的返回值明確,并使用注釋澄清潛在的誤解。
- 一致性:通過在整個代碼庫中一致使用關鍵字和模式,增強代碼的可讀性和可維護性。
這部分內容探討了如何通過代碼中隱含的設計選擇,傳達更多的意圖和信息,進而提高代碼的可讀性和可維護性。以下是對這些內容的中文解釋:
1. 其他選擇也能傳達大量信息
原始指針是否總是非擁有的指針?
bool sendEmails(Employee* pe)
和Message* sendEmails(Employee* pe)
- 這些代碼片段的設計中,是否使用了智能指針?
- 是否頻繁使用了
new
和delete
? - 這會涉及到“規則 3 或 5”(Rule of 3/5),即類如果有資源管理行為(如分配內存),它應該提供拷貝構造函數、賦值運算符和析構函數來管理這些資源。
- 代碼中是否有析構函數?
傳遞原始指針或智能指針的選擇,能夠傳達代碼的資源管理策略。如果使用了智能指針,代碼可以自動管理內存,減少資源泄漏的風險。如果是原始指針,則意味著可能需要手動管理內存,這就需要仔細考慮拷貝、賦值、析構等操作。
&
和 *
的意義?
&
表示引用,*
表示指針。- 傳遞地址或引用是否總是會修改數據?
- 傳遞引用(
T&
)和傳遞指針(T*
)都可能導致數據修改,但是否會修改取決于是否是const類型。 - 在一些編程習慣中,傳遞指針可以暗示轉移所有權,即調用者不再需要負責對象的生命周期,而是交給被調用函數。
- **“是否擁有”**并不是編譯器關心的問題,但它能表達出代碼設計中的意圖。通過這些選擇,你可以隱式地傳達某個對象是否由當前函數或對象負責管理其生命周期。
- 傳遞引用(
- 傳遞地址或引用是否總是會修改數據?
傳統的 for
循環是否總是在做一些奇怪的事?
- 為什么選擇這種循環?
- 這個問題的重點是:是否需要使用傳統的
for
循環?for
循環在某些情況下非常有用,但它可能會比使用范圍for
(for (auto& emp : department)
)更復雜且不易理解。選擇傳統的for
循環可能意味著你在“手動”處理某些特定的邏輯,而不是依賴于標準庫提供的算法。
- 這個問題的重點是:是否需要使用傳統的
- 為什么不使用范圍
for
循環?for
循環能對每個元素進行逐一操作,而 范圍for
循環(Ranged for)能夠更簡潔地遍歷容器。
- 是否有算法可以完成這項工作?
- C++ 標準庫中有許多算法,如
find
、count
、all_of
、sort
等,它們提供了比傳統循環更清晰、更簡潔的解決方案。
結論:使用標準算法而非傳統循環能顯得你對現有工具的理解更深入,表達了你對代碼簡潔性和可讀性的重視。
- C++ 標準庫中有許多算法,如
2. 初始化
- 構造函數沒有初始化成員變量時的含義
- 如果構造函數中沒有在
:
后進行成員變量的初始化,可能有以下幾種情況:- 成員變量有非靜態成員初始化器,即它們已經在類中指定了默認值。
- 在函數體內進行了初始化,可能是忘記在構造函數的初始化列表中進行初始化。
- 為什么會這樣?:
- 可能是忘記在構造函數中初始化某些成員。
- 在多個構造函數中,可能有一個構造函數忘記了初始化某個成員。
為什么將某個成員初始化為其默認值?
- 例如,
string s = "";
或vector<Employee> department(0);
。 - 這種行為表示可能在某些情況下不需要特別設置成員的初始值,或者是某個“默認”狀態。
- 如果構造函數中沒有在
3. 語言能否提供幫助?
- 我們是否應該添加關鍵字或屬性?你會使用它們嗎?
implicit
、const(false)
、nonvirtual
、ByVal
等關鍵字,在當前 C++ 標準中沒有,但它們可能能讓代碼更清晰。- 為什么不使用
fallthrough
和maybe_unused
?- 在 C++17 中引入了
[[fallthrough]]
和[[maybe_unused]]
屬性,這能幫助更清晰地表達意圖:[[fallthrough]]
表示在switch
語句中的 case 之間有意不加break
,表明是“故意跳過”。[[maybe_unused]]
用來標記那些可能未使用的變量,避免編譯器警告。
- 在 C++17 中引入了