1. 程式人生 > >Effective C++ 一些記錄和思考

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關鍵字修飾類而禁止被繼承
    • 建構函式和解構函式執行方式相反,建構函式是從最頂層的基類開始執行
  • 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 - 成對使用newdelete
    • 儘量使用只能指標替代 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]
  • 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 手法的,或類似的
        1. 提供一個 public swap,置換該型別的兩個物件
        2. 在當前 class 或者 template 所在的名稱空間內提供一個 non-member swap,並呼叫上述成員函式
        3. 如果當前是 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
  • Item 28 - 避免返回handles指向物件內部成分
    • 書中的例子會造成 bitwise constness, 見 Item 3, 但是 clang 3.8 上這些程式碼已經不能編譯通過了
  • Item 29 - 值得為“異常安全”花費精力
    • 異常安全的要求
      • 不洩露任何資源
      • 不允許資料敗壞
    • 資源洩漏用物件管理,如鎖
    • 單個函式的安全保證,copy-and-swap
    • 多個函式“連帶效應”, 對每一個函式都實現安全保證
  • Item 30 - 瞭解 inline
    • inlinetemplate 通常被定義於標頭檔案內。 是因為通常而言,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 引數時,typenameclass 沒有區別
    • 利用 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在臨界區外釋放資源