Effective C++讀書筆記

一直都沒有系統地看過一次Effective C++,最近好好地完整地讀了一遍,收穫頗豐,把讀書筆記分享給你們看看,與你們交流交流~java

裏面抄了書上很多的內容,也有一些我的的理解,可能有誤,但願你們能及時指出~ios

1. 讓本身習慣C++
條款01: 視C++爲一個語言聯邦,C、OO C++,Template C++, STL
 
條款02:儘可能以const, enum, inline替換#define
     1. 普一般量都用const,另外要注意的是隻有static const的整型的成員變量才能夠在類內聲明時賦初值。
     2. 不想被獲取到地址的常量用enum hack(對enum訪問地址是不合法的)像這樣:
class  Base  {
public  :
     enum   { Top  =   100  };
      ...
};
     3. 宏都用inline代替,糟糕宏中的典型
#define MAX(a, b) f((a) > (b) ? (a) : (b))  //當MAX(++a, b)時就出問題了

條款03:儘量用const
     1. *前const是 對象常量,*後const是 指針常量
     2. 返回const,如
class  R {...};
const   operator  *(  const  R  &  lhs  ,   const  R  &  rhs  );
     可避免
R a  , b  ,  c ;
(  a *  b  )   =  c  ;     //若是返回的不是const,編譯是能經過的
     這樣的狀況,讓這種錯誤在編譯的時候就被發現了。
     3. const與成員函數
          a) 兩個成員函數只是常量性的不一樣(即函數定義後有無const)是能夠重載的,形如:
class  TextBlock  {
public  :
     ...
      // operator[] for const對象
      const   char &  operator [](  std  :: size_t position  )   const  ;  
      // operator[] for non-const對象
      char &  operator [](  std  :: size_t position  );      
      ...
};
          b) const成員函數通常狀況下是不能改變成員的賦值的,當須要改變成員賦值時,須要成員變量 聲明前加mutable,這樣才能讓const成員函數改變。
          c) 當const與non-const成員函數若實現是等價時,令non-const成員函數調用const成員函數,方法就是像const_cast<T2>(static_cast<const T1&>(*this)...)這樣對this進行強制類型轉換成const。
 
條款04:肯定對象被使用前已先被初始化
     賦值操做不如初始化操做
     成員變量是const或者reference時必需要在初始化列表中初始化
     non-local static對象的初始化,主要指的是多文件的狀況,想某頭文件中的某變量在別的頭文件或CPP中也被使用。最直觀的方法是用extern。以下代碼
class  A  {
public :
     ...
     void  print  ()  const  ;
     ...
};
 
extern  A a ;
     但這裏的問題是我在別的文件調用a時,我不能保證a已經被初始化了(都怪糟糕的編譯器),因而就有如下方法
class  A  {
public :
     ...
     void  print  ()  const  ;
     ...
};
 
A &  a  ()  {
     static  A tmpa  ;
     return  tmpa  ;
}
     要調用時只須要用a()就能夠了,調用的是local變量的引用,並且保證有初始化。
 
2. 構造/析構/賦值運算
條款05:瞭解C++默默編寫並調用哪些函數
     類若用戶沒有定義會默認有copy構造函數、copy assignment操做符、析構函數,這種copy就是二進制的copy。而若是有引用或者是const成員變量時,以上的函數若須要只能本身定義,特別是copy assignment操做符,由於reference是不能指向不一樣對象的,而const也是不能被改變的。
 
條款06:若不想使用編譯器自動生成的函數,就該明確拒絕
     不想類擁有copy構造與copy assignment操做符,能夠把這些函數顯式地定義在private中。另外一個方案,就是定義一個uncopyable的類,這一個類就是把這些函數定義在private中,須要定義不能被copy的類時能夠直接繼承。
 
條款07:爲多態基類聲明virtual析構函數
     這樣作的緣由是,用父類指針指向子類時,若是沒有在父類定義的virtual析構,不能保證指向的子類能正確的被銷燬,可能調用的仍是父類的析構。
     另外,virtual函數的定義會增長類自己的大小,由於實現virtual是靠維護vptr(virtual table pointer)作到的,所以不是什麼狀況都用virtual。只在要實現多態的基類用virtual。
 
條款08:別讓異常逃離析構函數
     C++不喜歡析構函數吐出異常。緣由是當C++遇到兩個以上的異常同時存在時不是結束執行就是致使不明確行爲。而出現兩個以上的異常在容器成員要析構時要吐出異常的話可能就是一系列的異常,因而就出現了C++不容許的狀況了。(我的理解,其實感受仍是沒說清)
     這裏舉的例子是一個負責數據庫鏈接的類
class  DBConnection  {
public :
     ...
     static  DBConnection create  ();
    
     void  close  ();
};
     爲了保證用戶記得調用DBConnection對象中的close(),很天然的想法就是建立一個管理這個類的類,並在析構中調用close()。
class  DBConn {
public :
     ...
     ~ DBConn  ()  {
        db  . close ();
     }
private :
    DBConnection db  ;
};
     但這裏的問題是萬一close()吐出異常怎麼破?
     方法一:直接abort()
     ~ DBConn ()  {
          try  {  db . close ();}
          catch  (...)  {
            std  :: abort ();
          }
     }
     方法二:吞下異常,當什麼事兒也沒發生
     ~  DBConn ()  {
          try   {  db  . close  ();}
          catch   (...)  {
          }
      }
     方法三:從新設計,對可能出現的異常先做出反應
class  DBConn {
public :
     ...
     void  close  ()  {
        db  . close ();
        closed  =  true  ;
     }
     ~ DBConn  ()  {
          if  (!  closed )  {
              try  {  db . close ();}
              catch  (...)  {
                  ...
              }
          }
     }
private :
    DBConnection db  ;
     bool  closed  ;
};
     從以上可知,要儘可能把析構中可能出現的異常的部分轉移到別的成員函數中,把異常扼殺在別的成員函數中,而不是析構函數中。
 
條款09:毫不在構造和析構過程當中調用virtual函數
     這裏應該針對的是子類沒有定義構造函數,直接繼承父類構造函數的狀況,緣由是子類在繼承父類的構造函數後其調用的順序是先調用父類的,再調用子類的,而後這裏的問題是,若是是先調用父類的話,子類中的成員變量是被編譯器認爲是沒有定義的(包括virtual函數的定義),所以是調用不了的。一樣的析構函數在被調用時,是先把子類的成員變量空間都釋放了,而後再調用父類的,此時對於父類而言,子類中的定義都是不可知的(包括virtual函數的定義),所以也並無調用理想中的函數。
     直接在構造或者析構中放virtual是比較明顯的錯誤,可是當在virtual函數以外再定義一個非virtual的函數,這樣的錯誤就很隱蔽了,不可不防。
 
條款10:令operator=返回一個reference to *this。這是個約定,認了吧。
 
條款11:在operator=中處理「自我賦值」
     問題代碼是這樣的
class  Bitmap  {};
class  Widget  {
     ...
    Widget &  Widget  :: operator =( const  Widget &  rhs );
     ...
private :
    Bitmap *  pb  ;
};
 
Widget &  Widget  :: operator =( const  Widget &  rhs )  {
     delete  pb  ;
    pb  =  new  Bitmap (* rhs . pb  );
     return  * this ;
}
     當rhs是自身時,就出問題了。pb被提早刪掉了。
     方法一:證同測試
Widget &  Widget  :: operator =( const  Widget &  rhs )  {
     if  (this == & rhs) return * this;
     delete  pb  ;
    pb  =  new  Bitmap (* rhs . pb  );
     return  * this ;
}
     這裏就涉及到另外一個問題,「異常安全」,若是pb在new的時候出異常了,pb指向的數據就沒了,這時候數據就不完整了,異常沒法處理。
     方法二:調整順序
Widget &  Widget  :: operator =( const  Widget &  rhs )  {
    Bitmap *  pOrig  =  pb ;
    pb  =  new  Bitmap (* rhs . pb  );
     delete  pOrig  ;
     return  * this ;
}
     這樣可能不如方法一效率來得高,但至少安全。
     方法三:copy and swap 
Widget &  Widget  :: operator =( const  Widget &  rhs )  {
    Widget temp  ( rhs );
    swap ( temp  );
     return  * this ;
}
 
條款12:複製對象時勿忘其每個成分
     這裏容易出問題的是子類繼承父類時,若是從新定義了構造函數,而後又沒有考慮到父類的構造函數(由於父類構造函數可能會存在一些私有變量的初始化),這樣的後果是,對象會調用父類的無參構造函數,若是無參構造函數是你想要的那還能接受,可是若是無參構造參數裏面出了問題(好比說忘了給某私有變量賦值,那樣就黑了)。
     另外,copy賦值與copy構造若是代碼相同,不該該互相調用,應該藉助別的成員函數(如init())來完成。
 
3. 資源管理
條款13:以對象管理資源
     給指針動態分配空間,容易由於過程當中的某一步return或continue而錯過了自覺得會delete的代碼。而經過對象來管理這樣分配的空間會更合適,由於對象能夠在退出代碼塊時經過析構來進行delete。因而就推薦使用RAII對象(Resource Acquisition Is Initialization)。
     經常使用的有std::auto_ptr和std::tr1::shared_ptr,前者保證使用對象的只有一個,所以複製會形成被複制指針變爲NULL,保證對象只會引用一次,後者則是帶計數的智能指針,比較經常使用。但二者都有問題,實現的是delete而不是delete []。實現這個能夠用Boost裏的boost::scoped_array與boost::shared_array。
 
條款14:在資源管理類中當心copying行爲
     這裏主要是針對RAII對象,由於RAII對象是資源管理的脊柱(書是這麼寫的。)。設計好本身的RAII對象的COPYING行爲,通常是禁止複製或者是引用計數。還有一些就是複製底部資源以及轉移底部資源的擁有權(如auto_ptr)。
 
條款15:在資源管理類中提供對原始資源的訪問
     RAII對象把資源保護得好好的,可是有些函數卻須要直接用到RAII對象所保護的對象,如auto_ptr裏的指針是A* a,可是我函數f(A* a)要接收的參數類型是A*,那樣是不可能把auto_ptr傳遞給函數f的,這時候就須要一個獲取原始資源的接口了。這種訪問可能經由顯式轉換(即經過get()成員函數)或者是隱式轉換(即定義operator A*() const{return a;})。
     顯式轉換比較安全,由於它將「非故意的類型轉換」的可能性最小化了。但使用不太方便,由於看着太長太煩了。
     隱式轉換比較方便,但問題代碼以下:
class  my_ptr {
private :
    A *  pa  ;
     ...
public :
     ...
     operator  A  *()  const  {
          return  pa ;
     }
     ...
};
 
A *  getA  ();
my_ptr ptr1 ( getA  ());
...
A *  p_tmp  =  ptr1  ;
     這個操做的問題是,當ptr1被銷燬後,p_tmp就會懸空(dangle),程序會產生core dump。所以不太安全。
 
條款16:成對使用new和delete時要採起相同形式
     這裏主要強調的是new時用的[],delete別忘了[]。沒有的固然就不用加了。由於數組的new是帶數組長度的。
 
條款17:以獨立語句將已new的對象置入智能指針
     問題代碼以下:
class  A {};
int  func ();
void  process ( std :: tr1  :: shared_ptr < A >  pa  ,  int  );
     當以下調用時
process (std :: tr1:: shared_ptr <A >( new A ), func());
     這裏的問題是不知道編譯器的處理順序(這個問題只能怪編譯器,特別是各類不一樣標準的C++編譯器。),理想順序應該是先new A,而後把它傳給shared_ptr,而後再執行func(),最後再賦給process。或者是先執行func(),而後再new A,而後blabla。這樣都沒有問題。
     但,若是,賦給shared_ptr不是緊接着new A後,而是new A後,先執行func(),再傳new A的值給shared_ptr,那就可能會出問題,由於不保證func()不會出錯。
     改進代碼:
std :: tr1  :: shared_ptr < A >  pa  ( new  A  );
process ( pa  ,  func ());
 
4. 設計與聲明
條款18:讓接口容易被正確使用,不易被誤用
     一方面是促進正確使用,這裏提到的主要是接口的一致性,以及與內置類型的行爲兼容。
     另外一方面就是防止誤用。提到的幾個方法
     a) 創建新類型:
     先看
class  Date  {
public :
    Date ( int  month ,  int  day ,  int  year );
     ...
};
     這裏要正確使用很容易,要誤用也很容易,一個不當心把month與day的順序搞錯了,就出問題了。因而就有個方案是創建新類型Month, Day, Year。對應的類就應該這麼寫
class  Date  {
public :
    Date ( const  Month &  month  ,  const  Day &  day  ,  const  Year &  year  );
     ...
};
     這樣定義變量時就應該是相似
Date d ( Month  ( 1 ),  Day ( 2  ),  Year (  2013 ));
     明瞭而又不容易犯錯,並且Month、Day、Year還可慢慢寫,不斷完善。
     b) 限制類型上的操做。
     這裏的一個典型就是返回const,這樣能夠避免出現if(a*b=c)這種錯誤。
     c) 束縛對象值。
     這裏說的例子仍是日期,月份只有12個,別的都是錯的,爲了防止用戶出錯,咱們能夠限制它的值,因而Month類能夠這麼寫:
class  Month  {
public :
     static  Month Jan  ()  {  return  Month (  1 );}
     static  Month Feb  ()  {  return  Month (  2 );}
     ...
     static  Month Dec  ()  {  return  Month (  12 );}
      ...
private :
     explicit  Month  ( int  m );
     ...
};
     d) 消除客戶的資源管理責任
     這裏做者強烈推薦使用Boost的tr1::shared_ptr,雖然笨重,但能有效避免錯誤的發生。
 
條款19:設計class猶如設計type
     書中列的一系列注意問題,沒細講,都列出來吧
     a) 新type的對象應該如何被建立和銷燬
     b) 對象的初始化和對象的賦值該有什麼差異?
     c) 新type的對象若是被passed by value,會怎樣?
     d) 什麼是新type的「合法值」
     e) 你的新type須要配合某個繼承圖系嗎?
     f) 你的新type須要什麼樣的轉換?
     g) 什麼樣的操做符和函數對此新type而言是合理的?
     h) 什麼樣的標準函數應該駁回?
     i) 誰該取用新type的成員
     j) 什麼是新type的「未聲明接口」?
     k) 你的新type有多麼通常化?
     l) 你真的須要一個新type嗎?
 
條款20:寧以pass-by-reference-to-const替換pass-by-value
     可是若是傳遞的是基礎類型、STL的迭代器和函數對象,仍是建議pass-by-value
 
條款21:必須返回對象時,別妄想返回其reference
     就是搞清楚返回的東西是在stack仍是在heap,就算是返回static也是有可能出錯的,問題代碼是這樣的:
const  Rational  &   operator  *   ( const  Rational  &  lhs  ,   const  Rational rhs  )
{
     static  Rational result  ;
    result  =   ...;
      return  result  ;
}
 
bool   operator  ==   ( const  Rational  &  lhs  ,   const  Rational  &  rhs  );
Rational a  ,  b  ,  c ,  d  ;
if   ((  *  b  )   ==   (  *  d  ))   {     // 這裏必定會是true的!
     ...
}   else  {
      ...
}
     由於reference引用是同一個static變量result。
 
條款22:將成員變量聲明爲private
     首先是代碼的一致性(調用public成員時不用考慮是成員仍是函數)。
     其次封裝性,都寫成函數進行訪問能夠提供之後修改訪問方法的可能性,而不影響使用方法。另外,public影響的是全部使用者,而protected影響的是全部繼承者,都影響巨大,因此都不建議聲明成員變量。
 
條款23:寧以non-member、non-friend替換member函數
     書中展開討論的是這個狀況,當你有一個類這麼寫的:
class  A  {
public :
     ...
     void  func1  ();
     void  func2  ();
     void  func3  ();
     ...
};
     而後由於有個常用的操做須要順序的使用三個成員函數,因此就想寫一個便利的函數。這裏有個選擇,是寫成member函數,仍是non-member non-friend函數。也就是
class  A  {
public :
     ...
     void  func1  ();
     void  func2  ();
     void  func3  ();
    
     void funcAll ();
     ...
};
     與
void  funcAllA ( A &  a  )  {
    a . func1  ();
    a . func2  ();
    a . func3  ();
}
     之間的選擇。做者的意思是後者好。
     而這個做者也有一套關於封裝性的解釋,做者經過計算可以訪問對象內數據的函數數量,大概計算封裝性。原則就是越多函數能訪問,封裝性越低。
     其實就我我的理解,這裏特地強調non-member、non-friend其實要強調的就是這種函數是不能訪問類內的private區的。
     那麼這條款的邏輯應該是能夠這麼認爲的,當要寫的函數並不須要直接調用private區的變量時,儘可能寫成non-member、non-friend函數。
     固然C++是不會阻止你們把函數都寫成member函數的,java/c#使用者不用擔憂。只是針對以上狀況,C++的寫法通常是寫在namespace裏面(其實我以爲是否是能夠寫成static的member函數呢?)
     做者順帶談了下namespace與class的區別
     namespace是能夠分好幾個文件寫的,不受約束,並且可擴充,固然它不是一個可能實例化的存在。
     class的聲明必須得一塊兒寫,要擴充也只能繼承,但問題是,不是全部的類都設計用於繼承的。不過class是一個能夠實例化的,就是數據是有本身的私有空間的,可能帶着周圍跑的。
 
條款24:若全部參數皆需類型轉換,請爲此採用non-member函數
     這裏提出的例子是對Rational的operator*的實現應該是member函數仍是non-member。
     member函數時
result  =  oneHalf  *  2  ;     //OK
result  =  2  *  oneHalf ;     // 錯誤
     這裏暗含一個條件就是構造函數是容許隱式類型轉換(不帶explicit),不容許的話兩個就過不了。而若是用non-member就一點問題也沒有了。
     這個operator*的特色就是,兩個變量其實都有類型轉換的須要,若是是寫成member函數,那麼左操做符就不能進行類型轉換了。而non-member函數就能知足這一需求了。
 
條款25:考慮寫出一個不拋出異常的swap函數
     首先要明白,swap函數用處很大,在以前的條款11中就用於處理自我賦值可能性上,而在條款29(日後看吧)將會說到與異常安全性編程相關的用法。總之很重要,同時很複雜。
     std中的swap是基於copy構造與copy賦值的,典型實現以下
namespace  std  {
     template < typename  T >
     void  swap  ( T &  a ,  T &  b  )  {
        T temp  ( a );
        a  =  b ;
        b  =  temp ;
     }
}
     但當本身定義的類,有更高效的swap方法時,如成員變量中的數據包含指針,copy賦值或copy構造時進行的操做是對指針指向的內容進行徹底的拷貝(很合理),可是放時swap裏面時就要進行這樣屢次的指針指向內容的拷貝,再進行交換,而事實上,更好的方法是直接進行指針的交換就能夠,不須要經過copy賦值與構造。(常適用於pimpl手法實現的class,pimpl,pointer to implementation)
     所以這裏就引進了特化的方法。這裏要訪問到類內數據(private),就得是member或者是friend了,而對於swap這麼一個特殊的函數(可用於異常安全性編程,本人猜測),則傾向於先定義一個member的swap,而後再定義一個non-member的swap。STL內也是這麼實現的,代碼以下
class  A  {
public :
     ...
     void  swap  ( A &  other )  {
          using  std ::  swap ;
        swap  ( pImpl ,  other . pImpl  );
     }
     ...
private :
    AImpl *  pImpl  ;     //實現類
};
 
namespace  std  {
     template <>         //全特化版本
     void  swap  < A >(  A &  l ,  A  &  r )  {
        l  . swap (  r );
     }
}
     以上是針對A是class的狀況,而若是A是class template時就不同了,對應的swap函數就是偏特化的,大概形式以下
namespace  std  {
     template < typename  T >           //偏特化版本
     void  swap  <  A <  T >  >( A  < T >&  l ,  A < T  >&  r )  {
        l  . swap (  r );
     }
}
     但這樣是錯的,C++容許對class template進行偏特化(即class定義前約束爲template<typename T>),而不容許對function template進行偏特化。解決方法是寫一個swap的重載版本,以下
namespace  std  {
     template < typename  T >
     void  swap  ( A <  T >&  l ,  A  < T >&  r )  {
        l  . swap (  r );
     }
}
     到這裏,問題應該說是算解決了,但std是個很特殊的命名空間,裏面的東西你能夠用,能夠特化,但寫這個std的人們是不想咱們去改裏面的東西,甚至是重載也不行(雖然是能夠編譯經過)。做爲乖孩子,咱們只有在本身的小空間裏知足本身的小要求了,也就是把這個重載寫在本身的命名空間裏面。
namespace  MyWorld  {
     template < typename  T >
     void  swap  ( A <  T >&  l ,  A  < T >&  r )  {
        l  . swap (  r );
     }
}
     到最後,就是要注意最終使用時,編譯器會調用哪個的問題了,容易不清楚的是如下狀況
template < typename  T >
void  doSomething ( T &  a  ,  T &  b )  {
     using  std  :: swap ;
     ...
    swap ( a  ,  b );
     ...
}
     這裏的關鍵在於using std::swap。
     試想若是沒有的話,可能就只能找到你本身的小空間裏的swap函數了,然而T自己是不受約束的,你本身的命名空間內的swap是不夠用的。
     再想一想,若是特定約束爲std::swap(a, b),則問題就是隻能找到std內對應的swap版本,卻找不到你本身定義的更高效的實現版本。
     總的來講就是,使用前swap時,先using std::swap,而後使用時swap不帶命名空間約束。
     另外,關於不拋出異常的swap問題只是針對member函數而言的,在條款29會講。
     此條款較長,須要總結下:
     1. 當std::swap對自定義類型效率不高時,就提供一個swap成員函數,並確保不拋出異常。
     2. 提供與swap的member函數相對應的non-member函數版本,對於class提供全特化版本std::swap,對於class template提供非std空間的重載。
     3. 使用前swap時,先using std::swap,而後使用時swap不帶命名空間約束。
 
5. 實現
條款26:儘量延後變量定義式的出現時間
     主要是延後到要用時才定義,延後到你願意賦初值時才定義。
     另外有個問題就是應該把變量定義在循環體內仍是體外,這得看狀況。
     copy賦值開銷低於構造+析構開銷,且效率要求高,則建議定義在循環體外,不然定義在循環體內(主要考慮便於管理)。
 
條款27:儘可能少作轉型動做
      C style轉型:
     1. (T)expression
     2. T(expresstion)
 
      C++ style轉型:
     1. const_cast,把const轉到non-const
     2. dynamic_cast,轉成子類,可用於判斷是否有歸屬關係,但特別慢
     3. reinterpret_cast,低級的轉型,好比pointer to int轉型爲int
     4. static_cast,強迫隱式轉換。把non-const轉成各位。
 
      注意:
     1. 轉型不會改變待轉型對象的值,只是產生一個轉型後的副本。
     2. 儘可能避免使用dynamic_cast,使用前必定要三思。
     3. 轉型動做盡可能隱藏起來,不要讓使用者進行轉型。
     4. 使用C++ style轉型,由於分得更細,且容易辨認(特別是要查找代碼時)。
 
條款28:避免返回handles指向對象內部成分
     handles指的是reference、pointer、iterator之類的,就是可能有懸空可能存在的東西。由於
     1. 返回handles的話就會有handles所指向對象比handles自己提早銷燬的可能性。
     2. 並且返回handles會下降封裝性。
 
條款29:爲「異常安全」而努力是值得的
     異常安全兩條件,就是在出現異常時:
     1. 不泄漏任何資源
     2. 不容許數據敗壞
     異常安全函數要提供如下三個保證之一:
     1. 基本保證:出現異常仍能保持在一個 有效的狀態。
     2. 強烈保證:出現異常程序狀態 不改變
     3. 不拋保證: 保證不拋出異常,這與帶着空白的異常明細的函數不同,帶着空白異常明細的函數是指一旦拋出異常,將是嚴重錯誤。
int  doSomething ()  throw ();     // 空白異常明細
     其中:
     1. 不拋保證是最好的,但明顯很差實現。
     2. 強烈保證通常經過copy and swap都能實現(這就是爲何swap函數要保證不出異常,方法在條款11中提到過),但
          a) 有時候須要考慮到有沒有這麼實現的意義,由於這麼實現是有效率上的犧牲的。
          b) 另外,不是全部函數都能實現的,特別是有嵌套的保證需求的時候。好比,要實現強烈保證,函數內調用的函數也一樣須要強烈保證,而就算保證了調用的函數都能作到強烈保證,其調用帶來的狀態改變也有多是不能復原的。
     3. 「異常安全保證」服從木桶原理,決定異常安全的關鍵在於最薄弱的「異常安全保證」
 
條款30:透徹瞭解inlining的裏裏外外
      inline是這樣的:
     1. inline函數的調用,是對函數本體的調用,是函數的展開,使用不當會形成代碼膨脹。
     2. 大多數C++程序的inline函數都放在頭文件,inlining發生在編譯期。
     3. inline函數只表明「函數本體」,並無「函數實質」,是沒有函數地址的。
      要注意到:
     1. 構造函數與析構函數每每不適合inline。由於這兩個函數都包含了不少隱式的調用,而這些調用付出的代價是值得考慮的。可能會有代碼膨脹的狀況。
     2. inline函數沒法隨着程序庫升級而升級。由於大多數都發生在編譯期,升級意味着從新編譯。
     3. 大部分調試器是不能在inline函數設斷點的。由於inline函數沒有地址。
      所以
     1. 大多數inlining限制在小型、被頻繁調用的函數身上。
     2. 另外,對function templates的inline也要慎重,保證其全部實現的函數都應該inlined後再加inline。
 
6. 繼承與面向對象設計
條款31:將文件間的編譯依存關係降至最低
     這個問題產生是源於但願編譯時影響的範圍儘可能小,編譯效率更高,維護成本更低,這一需求。
     實現這個目標首先第一個想到的就是,聲明與定義的分離,用戶的使用只依賴聲明,而不依賴定義(也就是具體實現)。
     但C++的Class的定義式卻不只僅只有接口,還有實現細目(這裏指實現接口須要的私有成員)。而有時候咱們須要修改的一般是接口的實現方法,而這一修改可能須要添加私有變量,但這個私有變量對用戶是不該該可見的。但這一修改卻放在了定義式的頭文件中,從而形成了,使用這一頭文件的全部代碼的從新編譯。
     因而就有了 pimpl(pointer to implementation)的方法。用pimpl把實現細節隱藏起來,在頭文件中只須要一個聲明就能夠,而這個poniter則做爲private成員變量供調用。
     這裏會有個有意思的地方,爲何用的是指針,而不是具體對象呢?這就要問編譯器了,由於編譯器在定義變量時是須要預先知道變量的空間大小的,而若是隻給一個聲明而沒有定義的話是不知道大小的,而指針的大小是固定的,因此能夠定義指針(即便只提供了一個聲明)。
     這樣把實現細節隱藏了,那麼實現方法的改變就不會引發別的部分代碼的從新編譯了。並且頭文件中只提供了impl類的聲明,而基本的實現都不會讓用戶看見,也增長了封裝性。
     結構應該以下:
class  AImpl ;
class  A  {
public :
     ...
 
private :
    std :: tr1  :: shared_ptr < AImpl >  pImpl  ;
};
     這一種類也叫 handle class
     另外一種實現方法就是用帶factory函數的 interface class。就是把接口都寫成純虛的,實現都在子類中,經過factory函數或者是virtual構造函數來產生實例。
     聲明文件時這麼寫
class  A  {
     ...
     static  std  :: tr1 ::  shared_ptr < A  >
            create  (...);
     ...
};
     定義實現的文件這麼寫
class  RealA :  public  A  {
public :
     ...
private :
     ...
};
 
std :: tr1  :: shared_ptr < A >  A  :: create (...)  {
     return  std  :: tr1 ::  shared_ptr < A  >( new  RealA  (...));
}
     以上說的爲了去耦合而使用的方法不可避免地會帶上一些性能上的犧牲,但做者建議是發展過程當中使用以上方法,當以上方法在速度與/或大小上的影響比耦合更大時,再寫成具體對象來替換以上方法。
 
條款32:肯定你的public繼承塑模出is-a關係
     public繼承意味着is-a關係。肯定關係再繼承。
 
條款33:避免遮掩繼承而來的名稱
     這個問題來源是變量的有效區域引發的,而引入到繼承,就是子類與基類同名函數的關係問題了。
     1. 當子類不重載基類的函數的時候,基類的public函數是可以被繼承的。
     2. 當子類重載基類的函數的時候,基類全部同名函數均不會被繼承,即被遮掩了。
     既然是繼承,那就是is-a關係了,沒有基類的被重載函數通常狀況是不合適的。
     解決方法一:使用using,繼承全部版本的重載
class  Base  {
private :
     int  x ;
public :
     virtual  void  mf1 ()  ;
     void  mf1 ( int  );
     void  mf3 ();
     void  mf3 ( double  );
};
 
class  Derived :  public  Base  {
public :
     using  Base  :: mf1 ;
     using  Base  :: mf3 ;
     virtual  void  mf1 ();
     void  mf3 ();
     ...
};
     解決方法二:轉交函數(forwarding function),只繼承個別版本。有時候咱們只須要繼承重載函數中的個別版本,如以上的mf1的無參版本.
class  Derived :  public  Base  {
public :
     virtual  void  mf1 ()
     {  Base  :: mf1 ();  }
     ...
};
     關於繼承結合templates的狀況,將在條款43說起。
 
條款34:區分接口繼承和實現繼承
     這裏我的認爲能夠說是pure virtual、impure virtual、non-virtual成員函數在繼承中的表現的討論。
     pure virtual,只能繼承接口。若被繼承,則必須提供實現方案,基類也能夠實現默認方案(函數名同名),只是不會被繼承,要調用必須得inline調用(相似Base::func()這樣的形式)。
     impure virtual,繼承接口與缺省實現。基類提供默認實現方案,可被子類繼承,也可被重寫,繼承時要搞清楚是否須要繼承缺省實現。
     non-virtual,繼承接口與強制性實現,不能被重寫,能夠被重載。
     總的來講,就是接口是必定會被繼承的(至少接口名),實現方法怎麼繼承就看實際。

條款35:考慮virtual函數之外的其餘選擇
     這條款談了兩種設計模式,鼓勵咱們多思考的。
     以遊戲中的人物設計繼承體系爲例子,不一樣的人物有不一樣的計算健康指數的方法,就叫healthValue函數吧,很天然的就會想到設計一個基類,把healthValue函數設計爲virtual的用於繼承。
     此條款提供了兩種不一樣的思路
      1. Template Method模式,由 Non-Virtual Interface手法實現。
     具體到以上的例子就是大概以下:
class  GameCharacter  {
public :
     int  healthValue  ()  const  {
          ...
          int  retVal  =  doHealthValue ();
          ...
          return  retVal ;
     }
private :
     virtual  int  doHealthValue  ()  const  {
          ...
     }
};
     其實最早提的把healthValue設計爲virtual的方法也是Template Method模式,而這裏就是保留healthValue爲public,而實現爲private virtual(固然這裏是能夠protected的),這樣的好處在於其中的先後「...」(省略號),這部分能夠進行一些相似檢查、調整的操做,保證doHealthValue()在一個適當的場景下調用。並且子類也能夠繼承實現private virtual成員函數。
      2. Strategy模式,這一模式令實現方法是個變量,就算是同一個對象在不一樣的時段也能夠有不一樣的實現方法。但這裏都有個約束,就是對私有成員變量的訪問限制。
          a) Function Pointers實現,此種實現手法的約束是隻能是函數,並且形式受函數的簽名(參數數量,參數類型,返回類型)的約束。
          b) tr1::function實現,擺脫了a)的約束,支持隱式類型轉換,還支持函數對象或者是成員函數(經過std::tr1::bind實現)
          c) 古典實現,其實就是對象實體是一類,而實現方法是另外一類。
 
條款36:毫不從新定義繼承而來的non-virtual函數
     繼承non-virtual函數的後果是,最終函數的實現效果不禁聲明時的類型決定,而是由使用時用的指針或者引用類型決定。簡單些用代碼表達以下:
class  A  {
public :
     void  f (){
        cout  <<  "A"  <<  endl ;
     }
};
 
class  B :  public  A  {
public :
     void  f ()  {
        cout  <<  "B"  <<  endl ;
     }
 
};
 
int  main ()  {
    B t ;
    B *  pb  =  &  t ;
    A *  pa  =  &  t ;
    pa -> f  ();          //調用A的f()
    pb -> f  ();          //調用B的f()
     return  0 ;
}
     這個實際上是不符合non-virtual成員函數的不變性特色的。也不能體現出B的特異性(由於t被調用f時的效果不必定來自B)
     所以,毫不從新定義。
     其實這一條款的另外一層含義就是隻從新定義virtual成員函數
 
條款37:毫不從新定義繼承而來的缺省參數值
     先說兩個名詞,動態綁定(又稱前期綁定,early binding)與靜態綁定(又稱後期綁定,late binding)。
     另外,對應的,靜態類型就是在聲明時所採用的類型,動態類型就是目前所指對象的類型。
     virtual函數就是動態綁定的,而non-virtual函數就是靜態綁定的。
     而這條款討論的是更細的一層,「繼承帶有缺省參數值的virtual函數」。(由於條款36說過了,毫不繼承non-virtual函數)
     這裏的問題是C++編譯器對缺省參數是靜態綁定的(出於效率考慮),virtual函數倒是動態綁定的。由於這樣的設定就會可能會致使調用的不一致(當用到多態時),看如下代碼:
class  A  {
public :
     virtual  void  f ( char  c  =  'A' ){
        cout  <<  "this is A: "  ;
        cout  <<  c  <<  endl ;
     }
};
 
class  B :  public  A  {
public :
     virtual  void  f ( char  c  =  'B' )  {
        cout  <<  "this is B: "  ;
        cout  <<  c  <<  endl ;
     }
 
};
 
int  main ()  {
    A *  p  =  new  B ;
    p -> f  ();         //調用B的f(),缺省參數來自A,結果是
                      //this is B: A
     return  0 ;
}
     由於p的靜態類型是A*,因此缺省是來自A,而動態類型是B,因此f()調用來自B。
     解決方法就是選擇virtual函數的替代設計方案,如NVI(non-virtual interface)。
 
條款38:經過複合(composition)塑模出has-a或is-implemented-in-terms-of
     主要是介紹除以前public繼承的is-a關係外的has-a與is-implemented-in-terms-of關係。
     has-a對應的是應用域,如World中的persons, cars等等,這些一般是被訪問查詢而存在。
     is-implemented-in-terms-of對應的是實現域,如對象中的buffers, mutexes, search trees等等,這些一般是爲實現某些功能而存在的東西。
 
條款39:明智而審慎地使用private繼承
     首先private繼承意味的是is-implemented-in-terms-of
     其次特殊狀況才用private繼承作到is-implemented-in-terms-of的關係,通常都用複合(composition,條款38)實現。
     緣由這裏提了兩個:
     1. private繼承不能阻止在virtual函數在再一次被繼承後的再一次被重寫。如Base爲要private繼承的基類,而Base有virtual函數f(),private繼承後爲Derived,當Derived被繼承時,f()仍是能夠重寫的。
     2. private繼承可能會增長編譯依存關係。由於通常能夠經過只在class內包含一個僅僅是聲明而沒有實現的類型的指針,實現對用戶是不可見的方式(能夠是在別的cpp文件中public繼承)去替代private繼承。這就涉及到條款31提到的編譯依存性最小化的問題了。
     特殊狀況就是須要訪問基類的protected成員時又或者是須要從新定義繼承而來的virtual函數時。
     還有一種狀況就是須要對象尺寸最小化時。當一個類裏面沒有non-static的數據時,C++編譯器認爲對象都應該有非零大小,所以,當用包含的方式(當爲對象中的成員變量)時,沒有non-static的數據仍然會被分配空間(至少char的大小,雖然沒有意義),而若是是private繼承就不會增長空間開銷的。固然這種基類就是通常只有一些typedef或者non-virtual的函數,沒有任何可能帶來空間花銷的成員。
 
條款40:明智而審慎地使用多重繼承
     使用多重繼承就要考慮歧義的問題(成員變量或者成員函數的重名)。
     最簡單的狀況的解決方案是顯式的調用(諸如item.Base::f()的形式)。
     複雜一點的,就可能會出現「 鑽石型多重繼承」,以File爲例:
class  File  {...}
class  InputFile :  public  File  {...}
class  OutputFile :  public  File  {...}
class  IOFile :  public  InputFile  ,  public  OutputFile  {...}
     這裏的問題是,當File有個filename時,InputFile與OutputFile都是有的,那麼IOFile繼承後就會複製兩次,就有兩個filename,這在邏輯上是不合適的。解決方案就是用 virtual繼承
class  File  {...}
class  InputFile :  virtual  public  File  {...}
class  OutputFile :  virtual  public  File  {...}
class  IOFile :  public  InputFile  ,  public  OutputFile  {...}
     這樣InputFile與OutputFile共享的數據就會在IOFile中只保留一份了。
     可是virtual繼承並不經常使用,由於:
     1. virtual繼承會增長空間與時間的成本。
     2. virtual繼承會很是複雜(編寫成本),由於不管是間接仍是直接地繼承到的virtual base class都必須承擔這些bases的初始化工做,不管是多少層的繼承都是。針對這一特性,可讓class實現相似java的final功能,這就不是這一條款涉及的內容了,這裏只貼代碼跟說明吧:
template < typename  T >  class  MakeFinally  {
private :       //構造函數與析造函數都在private,只有友員能夠訪問
    MakeFinally (){};
     ~ MakeFinally  (){};
     friend  T ;
};
 
// MyClass是MakeFinally<MyClass>的友員,能實現初始化
class  MyClass :  public  virtual  MakeFinally  < MyClass >  {};
 
// D不是MakeFinally<MyClass>的友員,不能訪問private,而D繼承的
// 是virtual base class,必須得訪問MakeFinally<MyClass>中的構造
// 所以,不能經過,
class  D :  public  MyClass {};
 
int  main ()  {
    MyClass var1 ;
    D var2 ;        // 到這裏就會出錯,註釋掉不會報錯,由於定義爲空,
                  // 被編譯器忽略了
}
     可參考wiki中的說明: http://zh.wikipedia.org/wiki/%E8%99%9A%E7%BB%A7%E6%89%BF
     對於virtual bases的建議就是
     1. 非必要不要使用virtual bases。
     2. 實在要用,就儘可能避免在virtual bases裏面放數據。
     最後總結就是,能用單一繼承儘可能使用單一繼承,而多繼承在審慎考慮事後也要大膽使用,如以前提到的is-a與is-implemented-in-terms-of兩個關係分別與兩個base class相關時,只要審慎考慮過了再使用就能夠了。
 
7. 模板與泛型編程
條款41:瞭解隱式接口和編譯期多態
     以幾行代碼爲例:
template < typename  T >
void  doProcessing ( T &  w  ){
     if  ( w .  size ()  >  10  &&  w  !=  something  )  {
        T temp  ( w );
        w  . normalize ();
        temp  . swap (  w );
     }
}
     隱式接口,就是例子中類型T的變量w使用到的全部相關的函數。就是要求使用時調用的類型T必須具有的接口。
     編譯期多態,就是經過不一樣的template參數(T)致使不一樣的調用結果,而這些發如今編譯期。
     須要注意templates與classes的區別,classes的接口是顯式的,多態是經過virtual函數實現的,發生在運行期。
 
條款42:瞭解typename的雙重意義
     在聲明template參數時,class與typename是能夠互換的。
     在調用template嵌套的從屬類型名稱時,就只能是typename,但不能在base class lists(基類列表)或member initialization list(成員初值表)內使用。簡單的示例代碼以下:
template < typename  C >
void  print (  const  C &  c )  {
     // 沒有typename是通不過編譯的
     typename  C ::const_iterator iter(c.begin ());
     ...
}
 
條款43:學習處理模板化基類內的名稱
     (其實我以爲這一條款翻譯爲「學習訪問模板化基類內的名稱」(Know how to access names in templatized base classes)更合適一些。)
     C++編譯時,若是繼承的是模板化的基類,那麼像普通的基類繼承那樣直接調用基類的函數是不合法的。本條款說到的緣由是,模板化的基類是能夠被特化的(能夠參考條款33),而特化後的基類是能夠不具有某一函數的,而這一函數也許就是你繼承時須要調用的。把書中的代碼敲一下看看吧:
class  CompanyA  {
public :
     ...
     // 發送明文
     void  sendCleartext  ( const  std  :: string msg );
     // 發送密文
     void  sendEncrypted  ( const  std  :: string msg );
     ...
};
 
class  CompanyB  {
public :
     ...
     void  sendCleartext  ( const  std  :: string msg );
     void  sendEncrypted  ( const  std  :: string msg );
     ...
};
 
template < typename  Company >
class  MsgSender  {
public :
     ...
     void  sendClear  ( const  std  :: string msg )  {
        Company c  ;
        c  . sendCleartext ( msg );
     }
     void  sendSecret  ( const  std  :: string msg )
     {...}
};
     到這裏都沒有問題,當要繼承MsgSender的模板化基類時就出問題了
template < typename  Company >
class  DerivedMsgSender :  public  MsgSender  < Company >  {
public :
     ...
     void  sendClearMsg  ( const  std  :: string &  msg )  {
        sendClear (msg);   //這樣是編譯不過的!
     }
};
     由於MsgSender是能被特化的,而特化的版本是容許不存在某些接口的。如咱們特化一個CompanyZ的版本以下:
class  CompanyZ  {...};
 
template <>
class  MsgSender < CompanyZ >  {
public :
     ...
     // 只能傳密文,不能傳明文
     void  sendSecret  ( const  std  :: string &  msg )  
     {...}
     ...
};
     這樣是合法的,沒有了sendClear那麼在DerivedMsgSender中就不能用了,因此在C++的編譯中,就不容許這樣的調用。解決方法有三:
     1. 函數前加this->
template  < typename  Company  >
class  DerivedMsgSender  :   public  MsgSender  <  Company  >   {
public  :
      ...
      void  sendClearMsg  (  const  std  ::  string &  msg  )   {
        this->sendClear  ( msg);
      }
};
     2. 使用using
template < typename  Company >
class  DerivedMsgSender :  public  MsgSender  < Company >  {
public :
     using  MsgSender <Company>::sendClear;
     ...
     void  sendClearMsg  ( const  std  :: string &  msg )  {
        sendClear  ( msg );
     }
};
     3. 顯式調用base class
template < typename  Company >
class  DerivedMsgSender :  public  MsgSender  < Company >  {
public :
     ...
     void  sendClearMsg  ( const  std  :: string &  msg )  {
        MsgSender <Company>::sendClear(msg );
     }
};
 
條款44:將與參數無關的代碼抽離templates
     (其實這條款沒有太多的實際代碼參考,將懂不懂吧,直接把最後的總結抄了下來)
     這條款要提醒的是注意templates可能帶來的代碼膨脹。
     非類型模板參數形成的代碼膨脹(如template<typename T, int n>中的n),每每可 消除,作法是以 函數參數或者 class成員變量替換template參數。
     類型參數形成的代碼膨脹,每每可 下降,作法是讓帶有 徹底相同的二進制表述的具現類型 共享實現碼
 
條款45:運用成員函數模板接受全部兼容類型
     這條款講的是學會使用成員函數模板(member function templates),用於兼容可兼容的類型。問題來源是:
class  Top {...};
class  Middle :  public  Top  {...};
class  Bottom :  public  Mid  {...};
 
Top *  pt1  =  new  Middle ;
Top *  pt2  =  new  Bottom ;
const  Top *  pct2  =  pt1  ;
     這樣的操做對於指針是很天然的,也是很方便的,只是直接用的指針實在太不安全了,應該讓智能指針也能有這樣的能力。相似如下的效果:
SmartPtr < Top  >  pt1  =  SmartPtr < Middle  >( new  Middle  );
SmartPtr < Top  >  pt2  =  SmartPtr < Bottom  >( new  Bottom  );
SmartPtr < const  Top >  pct2  =  pt1 ;
     其實認真觀察以上的操做,以上都是copy構造(聲明時使用=調用的是copy構造)。因而就有了如下的解決方案。
template < typename  T >
class  SmartPtr  {
public :
     template < typename  U >       // 成員函數模板
    SmartPtr ( const  SmartPtr < U  >&  other )
          : heldPtr ( other . get  ())  {...}
    T *  get  ()  const  {  return  heldPtr  :}
 
private :
    T *  heldPtr  ;
};
     以上就是成員函數模板,就是泛化了的成員函數。
     這裏須要注意到,泛化的copy構造其實在U==T時就與正常的copy構造在實質上是同樣的。
     可是事實上,若是你沒有定義正常的copy構造(沒有泛化的),編譯器依然會默默地生成正常的copy構造。
     因此,若是想全部的copy構造都在本身的掌控,仍是要進行正常的定義。
     一樣的,對於編譯器自動提供的copy assignment也是有相同的注意點。
     看看tr1中的shared_ptr的定義摘要就明白了。
template < class  T >
class  shared_ptr  {
public :
     // copy 構造
    shared_ptr ( shared_ptr  const &  r  );
     //  泛化  copy 構造
     template < class  Y >                         
    shared_ptr ( shared_ptr  < Y >  const  &  r );
     // copy assignment
    shared_ptr &  operator =( shared_ptr  const &  r  );  
     //  泛化  copy assignment
     template < class  Y >                             
    shared_ptr &  operator =( shared_ptr  < Y >  const  &  r );
};
     
條款46:須要類型轉換時請爲模板定義非成員函數
     (其實這條款的翻譯我以爲也有問題,原文是Define non-member functions inside templates when type conversions are desired,我的以爲應該這麼翻譯:當非成員函數模板須要類型轉換時把函數定義在模板類中。具體解釋看下文吧)
     以operator*函數的模板化爲例(有理數的模板化類Rational<T>)。
     首先,由條款24可知,應該把operator*寫成 non-member function
template < typename  T >
class  Rational  {
public :
    Rational ( const  T &  num  =  0 ;
              const  T &  den  =  1 );
     const  T num  ()  const  ;
     const  T den  ()  const  ;
     ...
};
 
template < typename  T >
const  Rational < T >  operator *  ( const  Rational  < T >&  lhs ,
                              const  Rational < T >&  rhs  )
{...}
 
Rational < int  >  oneHalf ( 1 ,  2 );
Rational < int  >  result  =  oneHalf  *  2 ;    //編譯不過
     編譯不過的緣由是,operator*右側的參數(2)轉化不到Rational<int>上,參數(2)要轉化成Rational<int>上,先要知道T=int,這 兩層推導已經超出了編譯器的能力範圍了。
     因而,問題就變成了, 如何讓operator*能把T=int告訴編譯器,而且仍是一個non-member function(條款24)。
     這裏咱們注意到oneHalf要推出T=int是隻須要一步,而當operator*是在template class內部時,template的參數類型是不須要推導的(在聲明時已經知道了)。利用這一點把函數放進template class內就省去了template function的類型(T)推導過程了。
     但寫進class內瞭如何作到non-member function(仍是條款24)呢?解決方法就是 前面加個friend。代碼以下:
template < typename  T >
const  Rational < T >  doMultiply  ( const  Rational  &  lhs ,
                              const  Rational &  rhs );
template < typename  T >
class  Rational  {
public :
     ...
     friend  const  Rational  < T >  operator *  ( const  Rational < T >&  lhs  ,
                                      const  Rational < T >&  rhs  )
     {
          return  doMultiply ( lhs ,  rhs  );
     }
};
     這寫法很新鮮吧,記住就好。
     另外,用doMultiply函數去封裝是由於寫進class內意味着 inline,這樣是可能帶來 代碼膨脹的,爲了不這種狀況出現,通常都會在外面封裝一個實現函數(這裏這麼用實際上是爲了提醒你們使用時要注意inline的問題而已,就這個例子來講,代碼展開了也只是一行,封裝意義不大)。
     說到這裏應該明白我不一樣意此條款的翻譯的緣由吧。
 
條款47:請使用 traits classes表現類型信息
     書中覺得迭代器提供的advance函數爲例展開說明。
     先說迭代器的5個種類:
     1. input,如istream
     2. output,如ostream
     3. forward,std沒有實現的,意思就是單向鏈表
     4. bidirectional,如list
     5. random access,如vector、deque、指針
     因而就有了這麼一堆tag的定義
struct  input_iterator_tag  {};
struct  output_iterator_tag  {};
struct  forward_iterator_tag :  public  input_iterator_tag  {};
struct  bidirectional_iterator_tag :  public  forward_iterator_tag  {};
struct  random_access_iterator_tag :  public  bidirectional_iterator_tag  {};
     到這裏的時候大概會有些疑問:
     1. 爲何不是使用相似enum或者是const的文件去定義這些tag,而是這麼鄭重地定義成struct?
     2. 以上爲何會有繼承關係?或者更進一步問,爲何須要繼承關係?
     3. 爲何是struct?
     先說說第3個問題的我的理解,由於這隻涉及有類型信息,沒有不可被用戶所知的私有信息,所以struct合適。
     至於第一、第2個問題先不具體回答,但首先咱們明確的一點是這些classes是爲類型區分服務的。
     而後,咱們明確這個advance函數的設計目標:
     1. 利用不一樣迭代器的類型的優點(如random access的迭代器)
     2. 能兼容內置的迭代器類型(如指針)
     這裏就須要一個可以獲得類型信息的方法,也就是咱們要提到的traits。
     由以上分析可知,traits class內包含信息的方法是不可取的(內置類型作不到),所以就必然用templates的方法實現併爲傳統類型提供特化版本(不太明白的話,往下看就明白了)。固然通常也是struct的:
template  < typename  IterT  >
struct  iterator_traits  {
      typedef   typename  IterT ::  iterator_category iterator_category  ;
      ...
};
 
template < typename  IterT >    //對指針類型的特化,認真學習下
struct  iterator_traits < IterT *>  {
     typedef  random_access_iterator_tag iterator_category  ;
     ...
};
     而後還得迭代器的配合。
template <...>
class  deque  {
public :
     class  iterator  {
     public :
          typedef  random_access_iterator_tag iterator_category ;
          ...
     };
     ...
};
 
template <...>
class  list  {
public :
     class  iterator  {
     public :
          typedef  bidirectional_iterator_tag iterator_category ;
          ...
     };
     ...
};
     接着就有了advance函數的初始樣子
template  < typename  IterT  ,   typename  DistT >
void  advance  ( IterT  &  iter  ,  DistT d  )   {
      if  ( typeid (typename std:: iterator_traits <IterT >:: iterator_category )
         == typeid ( std:: random_access_iterator_tag ))
      ...
}
     實際上,這個advance函數實際上是有缺點的(條款48深刻解釋)。而事實上 用if來進行類型上的判斷也不是最好的方案,由於if判斷髮生在運行期。這裏提供的方案是利用函數重載的特性,由於重載函數的選取就已經包括了函數參數類型的判斷而且發生在編譯期實現方案以下
template < typename  IterT ,  typename  DistT >
void  doAdvance ( IterT &  iter  ,  DistT d ,
        std ::random_access_iterator_tag )  {
    iter  +=  d  ;
}
 
template < typename  IterT ,  typename  DistT >
void  doAdvance ( IterT &  iter  ,  DistT d ,
        std ::bidirectional_iterator_tag )  {
     if  ( d  >=  0  )  {  while  ( d  --)  ++  iter }
     else  { while  ( d ++)  -- iter ;}
}
 
template < typename  IterT ,  typename  DistT >
void  doAdvance ( IterT &  iter  ,  DistT d ,
        std ::input_iterator_tag )  {
     if  ( d  <  0  )  {
         throw  std ::  out_of_range ( "Negative distance"  );
     }
     while  ( d --)  ++ iter ;
}
 
template < typename  IterT ,  typename  DistT >
void  advance ( IterT &  iter  ,  DistT d )  {
    doAdvance (
        iter  ,  d ,
         typename std:: iterator_traits<IterT >::iterator_category
     );
     // 書中版本爲
     // doAdvance(
     //  iter, d,
      //  typename std::iterator_traits<IterT>::iterator_category()
     // );
     // 我的以爲有誤。也未查證英文原版
}
     到此,就能夠解答咱們開始的兩個問題了。
     1. 由於若是用enum或者const這種方式,相應的類型判斷就應該用if,而if判斷只能在運行期,不是最好方案,而用class的形式就能夠藉助重載函數的選擇過程當中進行類型判斷的這一特性了,而且這一切都發生在編譯期。
     2. 這個繼承關係無疑是正確的,都是is-a關係。除了明確關係外,這樣作實際上是有好處的,細心的同窗可能會發現這裏沒有實現std::forward_iterator_tag版本的重載函數,但事實上只須要std::input_iterator_tag的重載函數就足夠了,由於二者是is-a關係。
      總結:
     關於traits class的設計與實現:
     1. 確認相關類型信息,如上例所須要的category。
     2. 爲該信息選擇一個名稱,如上例的iterator_category。
     3. 讓相應的類型具有相同名稱的信息,如上例提到的typedef random_access_tag iterator_category。
     4. 爲須要兼容的類型提供特化版本,如上例對指針的特化。
     關於traits class的使用:
     1. 創建一組重載函數或函數模板(如doAdvance),彼此間差別只在於各自的traits參數。
     2. 創建一個控制函數或函數模板(如advance),調用上述函數,並傳遞traits提供的參數。
     
     最後提一下,std除了提供iterator_traits外,還提供了char_traits和numeric_limits等這樣保存類型相關信息的traits。而TR1也補充了很多,如is_fundamental<T>(判斷T是否爲內置類型),is_array<T>,is_base_of<T1,T2>等50個以上的traits classes。
 
條款48:認識template元編程(Be aware of template metaprogramming)
     條款47的traits技術其實就是TMP的一個應用,先解釋下條款47留下來的那個問題吧。
template  < typename  IterT  ,   typename  DistT >
void  advance  ( IterT  &  iter  ,  DistT d  )   {
      if  ( typeid (typename std:: iterator_traits <IterT >:: iterator_category )
         == typeid ( std:: random_access_iterator_tag ))
      ...
}
     以一個具現的例子爲例:
     代碼是這樣的:
std :: list  < int >::  iterator iter ;
...
advance ( iter  ,  10  );
     具現的代碼是這樣的:
void  advance ( std :: list  < int >::  iterator &  iter  ,  int  d )  {
     if  ( typeid ( typename  std ::  iterator_traits <  std  :: list <  int >:: iterator  >:: iterator_category )
          ==  typeid  ( std ::  random_access_iterator_tag ))
     {...}
     else
     {...}
}
     這個代碼在運行結果上是沒有問題的,但缺點就是,這個具現出來的代碼裏,存在 廢話。由於當參數類型( std  :: list  <  int >::  iterator)肯定之後,if裏面的判斷實際上是固定的,就是說,這個函數,的if或者是else裏面那部分代碼其實是永遠都不會被調用的。這是一種浪費。用條款47中的方案也就是TMP的方案就沒有這個問題了。
     解釋完了這個問題,咱們繼續領略TMP的魅力吧。
     TMP的循環都是經過遞歸完成的。
     直接上代碼:計算階乘
template < unsigned  n >
struct  Factorial  {
     enum  {  value  =  n  *  Factorial < n  - 1 >::  value  };
};
 
template <>
struct  Factorial < 0 >  {
     enum  { value  =  1  };
};
 
int  main ()  {
    std :: cout  <<  Factorial < 5 >:: value  ;     // 打印:120
    std :: cout  <<  Factorial < 10 >:: value  ;    // 打印:3628800
}
     我感受挺酷的。
     而後書中就列了幾個TMP的應用例子,沒有代碼實現(失望。得本身去找找看了),我就抄一下吧:
     1. 確保量度單位正確。關鍵字:編譯期錯誤偵測。
     2. 優化矩陣運算。關鍵字:expression templates。
     3. 能夠生成客戶定製的設計模式(Strategy, Observer, Visitor等等)實現品。關鍵字:policy-based design之TMP-based技術。
     總結:
     TMP很酷,不過很不直觀,並且資料還不多,雖不能成爲主流,但不可缺乏。
 
8. 定製new和delete
條款49:瞭解new_handler的行爲
     new_handler就是當new不能知足需求(如申請不到內存)時調用的函數。
     先看new_handler在<new>中的聲明:
namespace  std  {
     typedef  void  (*  new_handler )();
    new_handler set_new_handler  ( new_handler p )  throw ();
}
     new_handler是個typedef,是個函數指針,指向的是無參數無返回的函數。
     set_new_handler是用於指定new不能知足要求時該被調用的函數,其返回值是個指針,指向set_new_handler調用前正在執行的(立刻就要被調換的)那個new_handler函數。
     一個簡單的使用例子:
#include <iostream>
#include <new>       // set_new_handler
#include <cstdlib>   // abort
using  namespace  std ;
 
void  outOfMem ()  {
    cerr  <<  "out of mem"  <<  endl ;
    abort ();
}
int  main ()  {
    set_new_handler ( outOfMem  );
     int *  data  =  new  int  [ 10000000000000L ];
     return  0 ;
}
     設計良好的new_handler必須作如下事情(直接抄的,實現方法沒說):
     1. 讓更多內存可被使用。使operator new內的下一次內存分配動做可能成功。
     2. 安裝另外一個new_handler。若目前的new_handler沒法取得更多可用內存,而知道存在別的new_handler有此能力,就能夠安裝那個new_handler以替換本身。
     3. 卸除new_handler,就是把null指針傳給set_new_handler。一量沒有安裝任何的new_handler,operator new會在內存分配不成功時拋異常。
     4. 拋出bad_alloc(或者派生自bad_alloc)的異常。
     5. 不返回,一般調用abort或exit。
     這裏沒涉及new_handler的例子,日後說。而針對一個可能的要求——爲特定的class提供特定的new_handlers——提供了一個參考方案。
     先看方案1:
     Widget的聲明
class  Widget  {
public :
     static  std  :: new_handler set_new_handler ( std :: new_handler p  )  throw  ();
     static  void *  operator  new ( size_t size )  throw  (  bad_alloc );
private :
     static  std  :: new_handler currentHandler ;
};
     這裏用的都是static,爲何呢?
     首先咱們須要明確的就是咱們須要currentHandler存當前的new_handler,而這個是全部Widget對象共享的(不是獨有的),因此須要static。static成員變量必須得在外內被定義(const的整型能夠在成員內部定義),以下:
std :: new_handler Widget  :: currentHandler  =  0 ;
     而Widget::set_new_handler函數是在尚未new以前被外部調用的,因此寫成static也能夠理解(寫成外部函數就修改不了private變量currentHandler)。代碼以下:
std :: new_handler Widget  :: set_new_handler ( std :: new_handler p  )  throw  ()  {
    std :: new_handler oldHandler  =  currentHandler ;
    currentHandler  =  p  ;
     return  oldHandler  ;
}
     輸入輸出跟全局的set_new_handler的效果是同樣的,不過要注意到這過程並無調用全局的set_new_handler,只是作一個簡單的狀態存儲而已。
     最後就是重頭戲new了,這個函數爲何也是static呢?這是要被全局調用的,並且若是不是static,對象還不存在,又如何調用new呢?因此static也能夠理解,但可能一些事多的同窗(如我)可能會發現,在實際操做時,咱們不加static的聲明,其實效果也是同樣的!爲何呢?實際上是這樣的,在C++的標準裏面說到:
Any allocation function for a class T is a static member (even if not explicitly declared static).
     也就是說不管寫不寫static,new都必須保證是能在全局調用的。
     疑問解決完了,就學習下實現吧:
class  NewHandlerHolder  {
public :
     explicit  NewHandlerHolder  ( std ::  new_handler nh ): handler  ( nh ){}
     ~ NewHandlerHolder  ()  {  std :: set_new_handler  ( handler );}
public :
    std :: new_handler handler  ;
 
     // 阻止copying,見條款14
    NewHandlerHolder  ( const  NewHandlerHolder  &  nh ){}
    NewHandlerHolder  &  operator  =( const  NewHandlerHolder  &  nh ){}
};
 
void *  Widget  :: operator  new ( std  :: size_t size )  throw ( std :: bad_alloc  )  {
    NewHandlerHolder h  ( std ::  set_new_handler ( currentHandler  ));
     return  :: operator  new ( size  );
}
     這裏的精華我以爲是NewHandlerHolder的使用,利用了臨時對象在棧中的特性,就lock對象同樣,在出棧時自動調用析構函數,還原以前的狀態。
     如下即是大概的調用過程了。
void  outOfMem ();
Widget :: set_new_handler  ( outOfMem );
Widget *  pw1  =  new  Widget ;
std :: string  *  ps  =  new  std :: string  ;
Widget :: set_new_handler  ( 0 );
Widget *  pw2  =  new  Widget ;
     對Widget這個類來講,支持設置本身的new_handler的功能算是實現好了,很明顯這樣的代碼是能夠複用的,怎麼複用呢?這段代碼有個核心問題就是須要一樣的class(不是對象)共享相同的currentHandler,很天然就會想到使用base classes的templates。
     因而看看升級版:
template < typename  T >
class  NewHandlerSupport  {
public :
     static  std  :: new_handler set_new_handler ( std :: new_handler p  )  throw  ();
     static  voie  *  operator  new ( std ::  size_t size )  throw ( std  :: bad_alloc );
private :
     static  std  :: new_handler currentHandler ;
};
 
template < typename  T >
std :: new_handler
NewHandlerSupport < T  >:: set_new_handler ( std :: new_handler p  )  throw  ()  {
    std :: new_handler oldHandler  =  currentHandler ;
    currentHandler  =  p  ;
     return  oldHandler  ;
}
 
template < typename  T >
void *  NewHandlerSupport  < T >::  operator  new  ( std ::  size_t size )
throw ( std  :: bad_alloc )
{
    NewHandlerHolder h  ( std ::  set_new_handler ( currentHandler  ));
     return  :: operator  new ( size  );
}
 
template < typename  T >
std :: new_handler NewHandlerSupport  < T >::  currentHandler  =  0 ;
     要爲Widget添加set_new_handler支持能力只須要
class  Widget :  public  NewHandlerSupport  < Widget >  {
     ...        // 這樣就不須要再聲明set_new_handler和operator new
};
     這種class T: public NewHandlerSupport<T>的形式你們稱之爲CRTP(curiously recurring template pattern),這種技術使得全部的不一樣T類型的共享域不互相影響,對於這裏就是使得不一樣的T類型都有本身的currentHandler,互不影響。
     到這裏順帶提一下nothrow,就是在內存分配失敗時返回null,使用以下:
class  Widget  {...}
Widget *  pw1  =  new  Widget ;                  //若是分配失敗拋出bad_alloc
if  (  pw1  ==  0 )  ...                          //這個測試必定失敗
Widget *  pw2  =  new  ( std :: nothrow  )  Widget ;  //若是分配失敗返回0
if  (  pw2  ==  0 )  ...                          //這個測試可能成功
     用了nothrow爲何說是可能成功呢?這裏的成功是指測試能達到想要的效果,這裏的意思就是分配失敗了,但pw2可能並不等於0。由於nothrow只做用於給Widget分配內存時起做用,而當Widget進行本身的構造函數時所調用的東西(好比說進行一個可能會失敗的new操做)就不是nothrow所管的事情的。這裏的建議是忘記nothrow吧,它是爲了照顧老使用者而產生的東西。
 
條款50:瞭解new和delete的合理替換時機
     這條款主要說了一些要重寫new和delete的場合。列一下:
     1. 檢測運用錯誤
     2. 收集動態內存使用統計信息
     3. 增長分配和歸還的速度
     4. 下降缺省內存管理器帶來的空間額外開銷
     5. 彌補缺省分配器中的非最佳齊位(alignment)
     6. 將相關對象成簇集中
     7. 爲了獲取非傳統行爲
     我的總結:
     1. 更省的空間
     2. 更省的時間
     3. 更全的信息
     4. 爲了作到某些神奇的行爲
 
條款51:編寫new和delete時需固守常規
     明顯,這條款是講規則的。
     1. operator new能處理 0 byte的請求。即便要求0 byte,也得返回一個合法指針。最簡單的莫過於相似於這樣的if(size == 0)size=1;這樣的處理方法。
     2. operator new應該內含一個 無窮循環,如while(true){do something}之類的,其中的do something就是嘗試分配內存,若是它沒法知足內存需求,就該調用new_handler。
     3. operator new能處理 「比正確大小更大的(錯誤)申請」。其實這個問題的來源是子類是能夠繼承父類的operator new的,而若是沒有注意處理的話,子類能夠調用的是父類的operator new,這裏一個簡單的解決方案就是加一個判斷if (size != sizeof(Base)) { 調用標準的new } 大概這樣的形式。另外提醒下sizeof的返回不可能爲0的(條款39),所以當size爲0(0 byte的請求)時,就必定會交給大括號內的內容,這裏就是標準的new去處理了。
     4. operator delete應該在收到 null指針時不作任何事,保證刪除null指針永遠安全。
     5. operator delete一樣的也要能處理 「比正確大小更大的(錯誤)申請」。簡單的方案也如3提到的那樣加一個判斷。順便也提一下,當base class不提供virtual析構時,在operator delete時可能就會傳遞一個錯誤的size,致使delete失敗,由於base class就別忘了提供virtual析構。
 
條款52:寫了placement new也要寫placement delete
     這一條款主要講定製非正常operator new(也就是這裏的placement new)時要注意的東西。
     先認識下正常的operator new。
Widget *  pw  =  new  Widget ;
     這裏麪包含兩個過程,一個是用operator new分配內存,一個是Widget的default構造函數。
     當分配到了內存,可是Widget的構造函數出錯時,那麼就須要作到 分配的內存取消掉,並恢復原樣。在這裏,這個任務就交給了正常operator new 對應的operator delete(注意是 對應的)。到這裏咱們須要認識下這些正常的東西的函數簽名式:
// 正常的operator new
void *  operator  new  ( std ::  size_t )  throw ( std  :: bad_alloc );
// 正常的global做用域的operator delete
void  operator  delete ( void *  rawMemory )  throw ();
// 正常的class做用域的operator delete
void  operator  delete ( void *  rawMemory ,  std  :: size_t size )  throw ();
     而後咱們再認識下非正常的operator new,也就是咱們要討論的placement new。
     先說placement new最先也是頗有用的一個版本:
void *  operator  new  ( std ::  size_t size ,  void *  pMemory  )  throw  ();
     這一版本已經歸入了C++標準程序庫了,在<new>中,負責在vector未使用的空間上建立對象。而這一目的也致使了placement new這一名稱的出現:在特色位置上new。這也是placement new多數所指的版本:惟一額外參數是個void*的new。
     而這裏討論的主要是那個比較小衆的版本:帶任意額外參數的new。我就繼續抄書中的代碼了(作過整合的)。
class  Widget  {
     ...
     // 帶輸出日誌的new
     static  void *  operator  new ( std :: size_t size  ,  std ::  ostream &  logStream  )
             throw ( std  :: bad_alloc );
     // 正常的class做用域的operator delete
     static  void  operator  delete ( void  *  pMemory ,  std :: size_t size  )
             throw ();
     // 與帶輸出日誌的new對應的delete
     static  void  operator  delete ( void  *  pMemory ,  std :: ostream  &  logStream )
             throw ();
     ...
};
     這個operator new的調用就應該是這樣的:
Widget *  pw  =  new  ( std :: cerr  )  Widget // 是否是有點像條款49中nothrow的調用?
     而在這樣調用operator new產生的對象,日後使用operator delete以下
delete  pw ;
     調用的就是正常的operator delete了,由於placement delete只會 伴隨的placement new出現異常時纔會調用。
     這裏咱們特別強調 對應關係啊,由於若是調用了placement new,出錯時候若是沒有對應版本的delete的話,程序是不知道如何delete的!並且這些都是發生在運行期的!
     另外,還有一個問題,就是函數名被掩蓋的問題。由於全部(不一樣版本)的operator new重寫都會掩蓋global版本和繼承而得的operator new!就像剛纔寫的Widget,沒有寫正常的new版本,而後
Widget *  pw  =  new  Widget ;    // 錯誤
Widget  *  pw  =   new  ( std  :: cerr  )  Widget  ;   // 正確
     一樣的子類繼承父類後,重寫了new,那麼父類的new也是會被掩蓋的。
     在解決這個問題以前,咱們先了解下global域中的operator new:
void *  operator  new  ( std ::  size_t )  throw ( std  :: bad_alloc );    // normal new
void *  operator  new  ( std ::  size_t ,  void *)  throw ();    // placement new
void *  operator  new  ( std ::  size_t ,  const  std ::  nothrow_t &)  throw ();
                                                  // nothrow new
     這裏的忠告是,若是不是爲了阻止class使用以上的operator new,請確保它們在你所生成的任何定製型operator new以外還能用。還要提供對應的operator delete。
     書中提供了這一問題的一個簡單解決方案,創建一個base class,內含全部正常形式的new和delete,以下
class  StandardNewDeleteForms  {
public :
     // normal new/delete
     static  void *  operator  new ( std :: size_t size  )  throw  ( std ::  bad_alloc )
     { return  :: operator  new ( size );}
     static  void  operator  delete ( void  *  pMemory )  throw ()
     {:: operator  delete ( pMemory );}
     // placement new/delete
     static  void *  operator  new ( std :: size_t size  ,  void  *  ptr )  throw ()
     { return  :: operator  new ( size ,  ptr  );}
     static  void  operator  delete ( void  *  pMemory ,  void *  ptr )  throw ()
     {:: operator  delete ( pMemory ,  ptr  );}
     // nothrow new/delete
     static  void *  operator  new ( std :: size_t size  ,  const  std :: nothrow_t  &  nt )
             throw ()
     { return  :: new ( size ,  nt );}
     static  void  operator  delete ( void  *  pMemory ,  const  std  :: nothrow_t &)
             throw ()
     {:: operator  delete ( pMemory );}
};
     而後利用繼承和using用上這些函數:
class  Widget :  public  StandardNewDeleteForms  {
public :
     using  StandardNewDeleteForms  :: operator  new ;
     using  StandardNewDeleteForms  :: operator  delete ;
 
     static  void *  operator  new ( std :: size_t size  ,  std ::  ostream &  logStream  )
             throw ( std  :: bad_alloc );
     static  void  operator  delete ( void  *  pMemory ,  std :: ostream  &  logStream )
             throw ();
     ...
};
     總結起來就是:
     1. 寫了placement new就得寫對應的placement delete。
     2. 別 無心識(非故意)地讓placement new 與 placement delete 掩蓋了它們的正常版本。
 
9. 雜項討論
這一部分以提醒介紹爲主,沒有太多的代碼。做者說很重要。不過我是看過就算了。把各條款的總結抄一下吧。
條款53:不要輕忽編譯器的警告
     1. 嚴肅對待編譯器發出的警告信息,努力爭取無任何警告。
     2. 不要過分依賴編譯器的報警能力,由於編譯器是有可能變的。
條款54:讓本身熟悉包括TR1在內的標準程序庫
     1. C++標準程序庫的主要機能由STL、iostreams、locales組成。幷包含C99標準程序庫。
     2. TR1添加了智能指針、通常化函數指針、hash-based容器、正則表達式以及另外10個組件的支持。
     3. TR1自身只是一份規範。一個好的實物來源是Boost
條款55:讓本身熟悉Boost
     1. Boost是一個社羣,網址http://boost.org
     2. Boost提供許多TR1組件實現品,以及其餘許多程序庫。
相關文章
相關標籤/搜索