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组件实现品,以及其余许多程序库。
相关文章
相关标签/搜索