《Effective Modern C++》第3章 Moving to Modern C++
一、區分圓括號 ()
與大括號 {}
(Item?7)
C++11 引入統一初始化(brace?initialization),即使用 {}
來初始化對象,與傳統的 ()
存在細微差別:
-
避免窄化轉換(narrowing)
int x1(3.5); // x1 == 3(隱式截斷) int x2{3.5}; // 編譯錯誤,防止窄化
-
列表初始化優先級高于單參數構造
struct A { A(int); A(std::initializer_list<int>); }; A a1(1); // 調用 A(int) A a2{1}; // 調用 A(std::initializer_list<int>)
-
內置數組與聚合類型
std::vector<int> v1(5, 10); // 五個元素,每個值為 10 std::vector<int> v2{5, 10}; // 兩個元素:5, 10
建議
- 對基本類型和聚合類型優先使用
{}
,以獲得更嚴格的類型檢查和一致的語法; - 對于只接受單一特定參數的構造,明確使用
()
避免調用錯誤的初始化列表構造函數。
二、優先使用 nullptr
而非 0
或 NULL
(Item?8)
-
問題
NULL
在不同平臺下定義可能為0
或(void*)0
,帶來類型模糊;- 使用整型
0
傳給重載函數時,編譯器難以區分指針重載與整數重載。
-
解決
void f(int); void f(char*);f(0); // 調用 f(int) f(nullptr); // 調用 f(char*)
建議
- 在所有指針上下文中使用
nullptr
,保證類型安全和重載解析明確。
三、使用別名聲明(using
)替代 typedef
(Item?9)
-
typedef
限制- 語法晦澀,無法用于模板別名;
- 不易與模板參數一起閱讀。
-
using
別名typedef std::map<std::string, std::vector<int>> MapType; // 改為 using MapType = std::map<std::string, std::vector<int>>;// 模板別名 template<typename K, typename V> using MapOf = std::map<K, V>;
建議
- 在新代碼中一律采用
using
,既清晰又可與模板別名和別名模板配合使用。
四、優先使用作用域枚舉(enum class
)(Item?10)
-
傳統枚舉問題
- 枚舉常量位于所在命名空間,易與其他符號沖突;
- 默認可隱式轉換為整型,丟失類型安全。
-
作用域枚舉優勢
enum Color { Red, Green, Blue }; // Red 與全局沖突 enum class Shape { Circle, Square }; // Shape::Circle,無沖突int i = Shape::Circle; // 錯誤,不能隱式轉換
-
指定底層類型
enum class ErrorCode : uint8_t { OK = 0, Fail = 1 };
建議
- 新枚舉定義一律使用
enum class
; - 如需與整型交互,可顯式
static_cast
。
五、用已刪除函數(= delete
)替代私有未定義函數(Item?11)
-
舊習慣
class NonCopyable { private:NonCopyable(const NonCopyable&);NonCopyable& operator=(const NonCopyable&); };
僅在不定義函數時會在鏈接期報錯,且誤報位置不直觀。
-
現代做法
class NonCopyable { public:NonCopyable(const NonCopyable&) = delete;NonCopyable& operator=(const NonCopyable&) = delete; };
建議
- 對于不希望調用的函數,使用
= delete
,讓編譯器在編譯期明確報錯并指出源位置。
六、重寫虛函數時聲明 override
(Item?12)
-
風險
- 虛函數簽名微小變動會導致意外重載而非重寫,潛藏運行期錯誤。
-
加上
override
struct Base { virtual void f(int); }; struct Derived : Base {void f(int) override; // 正確重寫void f(double) override; // 編譯錯誤,函數簽名不匹配 };
建議
- 所有重寫基類虛函數的派生類函數都顯式標注
override
。
七、優先使用 const_iterator
而非 iterator
(Item?13)
-
背景
在不需要修改容器元素時,應使用只讀迭代器以保證不被意外改變。 -
示例
std::vector<int> v = {/*...*/}; for (auto it = v.cbegin(); it != v.cend(); ++it) {// it 為 const_iterator,無法通過 *it 進行寫操作 }
建議
- 在遍歷容器且不打算修改元素時,始終使用
cbegin()
/cend()
或手動指定const_iterator
。
八、聲明不會拋出異常的函數為 noexcept
(Item?14)
-
好處
- 編譯器可據此做更激進的優化;
- 在容器擴容時,若元素移動構造標記為
noexcept
,可避免回退到拷貝構造。
-
示例
void swap(Buffer& b1, Buffer& b2) noexcept {using std::swap;swap(b1.data, b2.data); }
建議
- 默認將不會拋出異常的函數標注
noexcept
; - 使用
noexcept(expr)
形式當拋出與否依賴于表達式。
九、盡可能使用 constexpr
(Item?15)
-
作用
- 在編譯期間求值,提高性能;
- 構造常量對象、用作編譯期上下文。
-
示例
constexpr int factorial(int n) {return n <= 1 ? 1 : (n * factorial(n - 1)); }static_assert(factorial(5) == 120, "錯誤");
建議
- 對所有能在編譯期求值的函數或構造函數加上
constexpr
; - 在 C++14 及以后,
constexpr
函數可包含循環和更多語句。
十、使常量成員函數線程安全(Item?16)
-
問題
const
成員函數默認是線程安全的嗎?不是。const
只是保證不修改成員表面狀態,但底層可能修改緩存等。 -
做法
- 對內部緩存、延遲初始化等涉及可變狀態的數據成員,使用
mutable
和適當的同步機制(如std::mutex
); - 或者在
const
函數中不使用可變共享狀態。
- 對內部緩存、延遲初始化等涉及可變狀態的數據成員,使用
示例
class Data {
public:int get() const {std::lock_guard<std::mutex> lg(m_);return cachedValue_;}
private:mutable std::mutex m_;int cachedValue_;
};
十一、理解特殊成員函數的生成規則(Item?17)
C++ 會在未顯式聲明時自動生成默認構造、拷貝/移動構造、拷貝/移動賦值、析構函數,規則復雜:
-
拷貝構造函數
- 如果顯式聲明了移動構造或拷貝賦值,拷貝構造會被阻塞(C++11);
-
移動構造函數
- 如果顯式聲明了拷貝構造、拷貝賦值或析構,移動構造會被阻塞;
-
析構函數
- 顯式定義后,依然會生成,但會影響其他特殊成員函數生成。
建議
- 對于需要自定義移動或拷貝行為的類,最好同時聲明并定義所有相關特殊成員函數(Rule of Five);
- 如無需移動,應顯式
= delete
移動構造與移動賦值; - 利用
= default
保留自動生成版本,并在聲明處表達意圖。
通過對以上十一個細則的深入理解與實踐,你將全面掌握現代 C++ 編程中的常見陷阱與最佳實踐,為編寫高性能、類型安全、可維護的代碼奠定堅實基礎。