Effective C++ 一些記錄和思考
Effective C++
- Iter 3 - 儘可能使用
const
一個反邏輯的 bitwise const
class Text { ... char& operator[](std::size_t pos) const { return text[pos]; } private: char *text; };
. 在 clang 3.8 上編譯失敗,編譯器已經修復這個反邏輯的問題。const 成員函式只能返回 const char& 型別的變數,這就保證了物件不能被修改。
當存在 const 成員函式和 non-const 成員函式的時候,可先實現 const 成員函式,non-const 成員函式通過呼叫 const 函式來實現,具體做法為
先將物件轉換為const型別(static_cast<const T&>()),呼叫const成員函式,再去除const屬性(const_cast<T&>())
// const const char& operator[](std::size_t pos) const { return text[pos]; } // non-const char& operator[](std::size_t pos) { return const_cast<char&>(static_cast<const char&>(*this)[pos]); }
. 這樣做的好處是 const 成員函式保證了資料的不變,減少程式碼量,缺點是轉換的效能缺失。
- Item 4 - 確定物件使用前已先被初始化
- 勿混淆賦值和初始化
- 物件的成員變數的初始化動作發生在進入建構函式
{/* body /*}
之前,使用member initialization list
初始化物件是一種比較好的做法 - 內建type(int char)預設為 0
- 初始化順序為其宣告次序
non-local static
編譯器對不同編譯單元的 non-local static 的編譯順序是隨機的- 解決的辦法是不直接訪問 non-local static 物件,而是使用一個函式包裝在其內部宣告為 local static 物件並且返回改物件引用
- static 為宣告在其作用域內的全域性變數,比如在函式內,只要該函式棧沒有被回收該變數便一直存在。在呼叫該函式時,該 local static 物件會在首次訪問時被初始化
- Item 5 6 - C++ 隱式實現和呼叫的函式
- 編譯器在沒有 預設建構函式,copy建構函式,賦值過載操作符 時會自動生成,且為 public
- 阻止呼叫 copy建構函式,賦值過載操作符函式
- C++11 之前,實現一個基類然後繼承
class UnCopyable { public: UnCopyable() {} ~UnCopyable() {} private: UnCopyable(const UnCopyable&); UnCopyable& operator=(const UnCopyable&); }; class Impl : private UnCopyable {};
- C++11 使用
delete
關鍵字
class Impl { public: Impl(const Impl&) = delete; Impl& operator=(const Impl&) = delete; };
- Item 7 - 為多型基類宣告
virtual
解構函式- C++ 11 中子類的可以用
override
來覆蓋基類的virtual
函式(解構函式可不用) - 由於virtual table 和virtual table pointer的存在,會使的class 的大小膨脹。
- 儘量不繼承non-virtual 解構函式的類,C++11後可以用
final
關鍵字修飾類而禁止被繼承 - 建構函式和解構函式執行方式相反,建構函式是從最頂層的基類開始執行
- C++ 11 中子類的可以用
- Item 8 - 別讓異常逃離解構函式
- Item 9 - 不在構造和解構函式內呼叫
virtual
函式- 在多型基類中建構函式呼叫虛擬函式,子類物件構造時執行的基類建構函式呼叫的基類的物件,而非子類物件,解構函式相同,這樣虛擬函式就變成普通的函數了
- Item 10 - 令
operator=
返回一個 reference to *this - Item 11 - 在
operator=
中處理“自我賦值”- 只看正確性,一般的做法是先保留一個副本,再進行賦值,最後刪除副本,這樣在賦值操作失敗的時候,不會丟失原來的資料
- Item 12 - 複製物件時勿忘其每一個部分
- 一個較容易忽略的地方,子類在copy構造的時候易忽略對基類物件變數進行copy,而這時預設呼叫了基類的default建構函式
- 不要在一個建構函式內呼叫另外一個建構函式,可行的做法是將共同的機能放進一個普通函式中,在兩個建構函式內呼叫
- Item 13 - 以物件管理資源
Resource Acquistion Is Initialization, RAII
auto_ptr
在標準中已經廢除,用unique_ptr
替代shared_ptr
互相引用的問題,可以用weak_ptr
解決
- Item 14 - 在資源管理類中注意copying行為
- Item 15 - 在資源管理類中提供對原始資源的訪問
- 在只能指標中提供原始指標即可
- Item 16 - 成對使用
new
和delete
- 儘量使用只能指標替代
new
- 陣列可用
std::vector
或者 C++11 中的std::array
代替
- 儘量使用只能指標替代
- Item 17 - 以獨立語句將
new
物件置入指標指標- 分離建立和使用的過程
- Item 18 - 讓介面易於使用,不易被誤用
- 類的設計通常應與內建型別的邏輯保持一致
- 函式的有資源相關的操作時可以考慮使用智慧指標來處理
- Item 19 - 設計
class
如同 type - Item 20 - 寧以 pass-by-reference-to-const 替換 pass-by-value
- 涉及到底層的處理,編譯器對待指標和自定義型別(class)的處理可能會不一樣
- 對於內建型別、STL的迭代器和函式物件,pass-by-value 更合適
- Item 21 - 必須返回物件時,勿返回reference
- clang 3.8 可以發現這個問題,併發出警告
warning: reference to stack memory associated with local variable 'a' returned [-Wreturn-stack-address]
- clang 3.8 可以發現這個問題,併發出警告
- Item 22 - 將成員變數宣告為
private
(能夠訪問private
的函式只有friend
函式和成員函式)- 將成員變數隱藏在函式介面後,可以為實現提供彈性
- Item 23 - 寧以 non-member、non-friend 替換 member 函式
- 將 non-member 函式與當前 class 宣告在同一個名稱空間內
- Item 24 - 若所有引數皆需型別轉換,請採用 non-member 函式
- 只有當引數位於引數列內,這個引數才是隱式型別轉換的合格者
- Item 25 - 考慮寫出一個不丟擲異常的 swap 函式
- 全特化版本,與
STL
保持一致性,這樣 Widget 物件就可以正常的呼叫 swap 了
`cpp class Widget { public: void swap(Widget& other) { using std::swap; swap(pImpl, other.pImpl); } private: WidgetImpl *pImpl; }; namespace std { // std template <> void swap<Widget> (Widget& a, Widget& b) { a.swap(b); } }
- 偏特化一個 function template 時,慣用手法是簡單的為它新增一個過載版本
cpp namespace WidgetFpp { // 這裡不是新增到 std 中了 class WidgetImpl {}; class Widget {}; template<typename T> void swap(Widget<T>& a, Widget<T>& b) { a.swap(b); } }
- 一般經驗
- 能使用 std::swap 就不去造輪子
- 有
pimpl
手法的,或類似的- 提供一個 public swap,置換該型別的兩個物件
- 在當前 class 或者 template 所在的名稱空間內提供一個 non-member swap,並呼叫上述成員函式
- 如果當前是 class 而不是 class template,為class特化
std::swap
,並呼叫 swap 成員函式
在呼叫 swap 前,使用
實現using std::swap
, 自動匹配合適的那個 swap 函式
- 全特化版本,與
- Item 26 - 儘可能延後變數定義是的出現時間
- 以“具明顯意義之初值”將變數初始化,還可以附帶說明變數的目的
- Item 27 - 儘量少做轉型動作
- 書中對 Window::OnResize() 的分析,這裡用過程式碼看結果,L18 這裡轉型後呼叫的OnResize()的*this是一個副本,L19 直接呼叫基類 OnResize能夠得到正確的結果
cpp 7 class Window { 8 public: 9 virtual void OnResize() { 10 a = 3; 11 } 12 int a; 13 }; 14 15 class SpWindow : public Window { 16 public: 17 void OnResize() override { // C++ 11 18 // static_cast<Window>(*this).OnResize(); // 這裡 a = 0 19 Window::OnResize(); // a = 3 20 } 21 };
- 謹慎使用
dynamic_cast
- 書中對 Window::OnResize() 的分析,這裡用過程式碼看結果,L18 這裡轉型後呼叫的OnResize()的*this是一個副本,L19 直接呼叫基類 OnResize能夠得到正確的結果
- Item 28 - 避免返回handles指向物件內部成分
- 書中的例子會造成 bitwise constness, 見 Item 3, 但是 clang 3.8 上這些程式碼已經不能編譯通過了
- Item 29 - 值得為“異常安全”花費精力
- 異常安全的要求
- 不洩露任何資源
- 不允許資料敗壞
- 資源洩漏用物件管理,如鎖
- 單個函式的安全保證,
copy-and-swap
- 多個函式“連帶效應”, 對每一個函式都實現安全保證
- 異常安全的要求
- Item 30 - 瞭解
inline
inline
和template
通常被定義於標頭檔案內。 是因為通常而言,inlining 和 template 是編譯期的行為,編譯器必須知道被呼叫函式的本體- 改變程式需要重新編譯,而不能像普通的函式直接連結即可
- Item 31 - 將檔案間的編譯關係降至最低
- Handle class 和 Interface class 解除介面和實現之間的耦合關係,降低檔案間的編譯依賴
Item 32 - 避免遮掩繼承而來的名稱
derived class 繼承了 base class 內的所有東西,實際上的執行方式是 derived class 作用域被巢狀在 base class 作用域內
繼承與面向物件設計
- Item 33 - 確認
public
繼承塑造出is-a
關係public
繼承適用於 base class 物件上的每件事情也能夠作用於 derived class 物件上- base class 和 derived class 中有相同的函式名稱時,base class 中的函式就被覆蓋了,
virtual
函式也是這樣,但是有執行時多型,但是普通函式當想繼承的時候就出問題了 - 在 derived class 加入
using base::func;
後相同引數和返回還是會覆蓋 base class 中的物件
- Item 34 - 區分介面繼承和實現繼承
- C++ 的隱式規則太多,需要繼承的東西乾脆顯示化,減少出錯
- Item 35 - 考慮
virtual
函式以及以外的選擇
// 定義虛擬函式後non-virtual interface, NVI
手法也是將真正需要的改變的部分分離出來放進一個函式內,而虛擬函式內先處理前置條件,再呼叫該函式
```cpp
7 class Game {
8 public:
9 int health() const {
10 std::cout << "Game::health()\n";
11 int ret = doHealth();
12 std::cout << "Game:: ret " << ret << '\n';
13 return ret;
14 }
15 private:
16 virtual int doHealth() const {
17 std::cout << "Game:: doHealth()\n";
18 return 1;
19 }
20 };
21
22 class LOL : public Game {
23 private:
24 virtual int doHealth() const {
25 std::cout << "LOL doHealth()\n";
26 return 2;
27 }
28 };
29
30 int main() {
31 LOL lol;
32 lol.health();
33 }
// 可以使用到 base class 預設的解構函式,參考 Item39,用 private 作為一種實現方式。
// LOL 未定義虛擬函式的時候,結果為
// Game::health()
// Game:: doHealth()
// Game:: ret 1
// Game::health()
// LOL doHealth()
// Game:: ret 2
```
strategy
策略使用函式指標替換虛擬函式,這樣每個物件都可以更靈活的有自己的特定處理函式,和執行時可以改變函式
cpp 7 class GameCharacter; 8 class HealthCalcFunc { 9 public: 10 virtual int calc(const GameCharacter&) const { 11 return 3; 12 } 13 }; 14 15 HealthCalcFunc defaultHealthFunc; 16 17 class GameCharacter { 18 public: 19 explicit GameCharacter(HealthCalcFunc* phcf = &defaultHealthFunc) : pHealthCalc(phcf) {} 20 int healthValue() const { 21 return pHealthCalc->calc(*this); 22 } 23 24 private: 25 HealthCalcFunc* pHealthCalc; 26 }; 27 28 int main() { 29 GameCharacter gc; 30 gc.healthValue(); 31 }
- Item 36 - 絕對不重新定義繼承而來的
non-virtual
函式- 繼承而來的
non-virtual
函式是靜態繫結,父類指標只能表現出父類物件的行為, 換而言之,這個在編譯期就確定好了
- 繼承而來的
- Item 37 - 絕不重新定義繼承而來的預設引數值
- 預設引數值都是靜態繫結的,應該覆蓋的是
virtual
函式,而它為動態繫結的
struct B { virtual void mf(int i = 1) { std::cout << "B::mf " << i << "\n"; } // 不改變 virtual 函式的時候,可以使用 NVI 手法,實現一個外圍函式,令子類呼叫它 }; struct D : public B { void mf() { std::cout << "D::mf\n"; } }; int main() { D d; B *pb = &d; pb->mf(); // B::mf }
- 預設引數值都是靜態繫結的,應該覆蓋的是
- Item 38 - 通過複合
(composition)
塑造或根據某物實現出has-a
- 在應用域,複合意味著
has-a
,在實現域複合意味著 is-implemented-in-terms-of
- 在應用域,複合意味著
- Item 39 - 謹慎使用
private
繼承private
繼承作為一種實現技術,意味著 is-implemented-in-terms-of, base class 為實現細節- 當兩個class不存在
is-a
關係時,其中一個需要訪問另一個的protected
成員,或者需要 重新定義 其一或者多個vitual
函式,可以考慮private
繼承
- Item 40 - 謹慎使用多重繼承
- 多重繼承易造成歧義(ambiguity)
- 乾脆直接禁用了
模板與泛型程式設計
- Item 41 - 瞭解隱式介面和編譯期多型
- Item 42 - 瞭解
typename
的雙重意義- 宣告
template
引數時,typename
和class
沒有區別 - 利用
typename
標示巢狀從屬型別名稱(形如 C::const_iterator),但不得在 base class lists 或者 member initialization list 以他作為 base class 標示
- 宣告
- Item 43 - 學習處理模板化基類內的名稱
- 模板的具現是在函式使用的時候
- 編譯器遇到 class template 繼承的類時,並不知道該類的定義是什麼
- 所以只能通過
this
指標指向 base class 物件,具現base class - 在該定義域內宣告需要訪問的 base class 函式
- 直接呼叫base class 中的程式碼
struct CompanyA { void sendClear(const std::string& msg) {} }; struct CompanyB { void sendClear(const std::string& msg) {} }; struct MsgInfo {}; template <typename Company> class MsgSend { public: void send(const MsgInfo& info) { std::string msg = "foo"; Company c; c.sendClear(msg); } }; template <typename Company> class LogMsg : public MsgSend<Company> { public: // using MsgSend<Company>::send; // slove - way 2: tell complier, send() is defined in base class void sendMsg(const MsgInfo& info) { // send(info); // base: cant pass complier // this->send(info); // slove - way 1: could pass complier // MsgSend<Company>::send(info); // slove - way 3: same as way 2 } }; int main() { MsgInfo mi; LogMsg<CompanyA> ma; ma.send(mi); }
- 所以只能通過
- Item 44 - 將與引數無關的程式碼抽離
template
- 模板只有在使用時被具現,不同型別的會被具現出不同的程式碼,引起程式碼膨脹,有點兒類似巨集的用法
- 或者引數使用相同的二進位制表述,如指標
- Item 45 - 運用成員函式模板接受所有相容型別
- 模板具現化後的 base class 和 derived class 為兩個獨立的類了
member function template
成員函式模板可接受所有相容型別引數,也就是泛化了建構函式- 為阻止編譯器預設生成copy建構函式和過載=,在已經有泛化版本的情況下也須自己定義這些函式
- Item 46 - 需要型別裝換時須為模板定義非成員函式
- 模板函式在進行實參型別推導的時候,不允許進行引數的隱式轉換(隱式轉換是發生在函式呼叫的時候),而型別推導的時候是先根據已經有的定義確定函式原型
- 為了轉換可以使用
friend
,感覺變複雜了
- Item 47 - 使用
traits classes
表現型別資訊- 整合過載技術,使得traits class 有可能在編譯期對型別執行if...else測試
- 使用 traits class
- 實現一組過載函式,或者函式模板,作為實現部分
- 建立一組驅動(控制)函式或者函式模板,呼叫以上過載函式
Item 48 - 認識模板超程式設計(編寫模板程式在編譯期執行的過程)
一些新的知識點
- pimpl, 以指標指向一個物件,內含真正資料
- 模板全特化、偏特化的一個部落格 https://blog.csdn.net/m_buddy/article/details/72973207
- copy and swap, 在副本上做修改,直到修改成功時再進行寫入,在陳碩的muduo書裡面講到的,用swap在臨界區外釋放資源