Effective C++总结


Effective C++


文章目录


  • 构造函数前加关键字 explicit 可以阻止它被用来执行隐式类型转换(implicit type conversions),但它们仍可被用来进行显示类型转换(explicit type conversions)。
    class C {
        public:
           explicit C(int x);  //不是 default 构造函数(可被调用而不带任何实参者,要不没有参数,要不每个参数都有隐含值)
    }
    
  • copy 构造函数被用来“以同型对象初始化自我对象”, copy assignment 操作符被用来“从另一个同型对象中拷贝其值到自我对象”:
    class Widget {
      public:
         Widget();                             //default 构造函数
         Widget(const Widget& rhs);            //copy 构造函数
         Widget& operator=(const Widget& rhs); //copy assignment 操作符
         ...
    };
    Widget w1;           //调用 default 构造函数
    Widget w2(w1);       //调用 copy 构造函数
    w1=w2;               //调用 copy assignment 操作符
    
    当看到赋值符号时要小心,因为“=”语法也可用来调用 copy 构造函数:
    Widget w3 = w2;   //调用 copy 构造函数
    
    如果一个新对象被定义(例如上句中的 w3),一定会有个构造函数被调用,不可能调用赋值操作。如果没有新对象被定义(例如前述的 w1=w2 语句),就不会有构造函数被调用,那么当然是赋值操作被调用。

条款 01:视 C++ 为一个语言联邦

  • C++ 是一个多重泛型编程语言,同时支持过程形式、面向对象形式、函数形式、泛型形式和元编程形式。

条款 02:尽量以 const,enum,inline 替换 #define (宁可以编译器替换预处理器)

  • 问题在于 #define 不被视为语言的一部分:
    #define ASPECT_PATIO 1.653
    
    记号名称 ASPECT_PATIO 也许从未被编译器看见;也许在编译器开始处理源码之前它就被预处理器移走了。于是记号名称 ASPECT_PATIO 有可能没有进入 记号表(symbol table) 内。于是当你运用此常量获得一个编译错误信息时,可能会带来困惑,因为这个信息也许会提到 1.635 而不是 ASPECT_PATIO 。如果 ASPECT_PATIO 被定义在一个非你所写的头文件内,你肯定对 1.635 以及它来在何处毫无概念。解决之道是以一个常量替换上述宏(#define):
    const double AspectRatio = 1.635;
    
    最为一个语言常量,AspectRatio 当然会被编译器看到,就会进入记号表。
  • 以常量替换 #define ,有两种特殊情况。第一种是定义常量指针。由于常量定义式通常被放在头文件内(以便被不同的源码含入),因此有必要将指针(而不只是指针所指之物)声明为 const。例如若要在头文件中定义一个常量的 char*-based 字符串,你必须写 const 两次:
    const char* const authoeName = "Scott Meyers";
    
    string 对象通常比 char*-based 合宜。所以上述的 authoeName 往往定义为这样更好:
    const std::string authoeName(Scott Meyers");
    
    第二个需要注意的是 class 专属常量。为了将常量的作用域限制于 class 内,你必须让它成为 class 的一员,而为确保此常量之多只有一份实体,你必须让它成为一个 static 成员:
    class GamePlayer{
      private:
         static const int NumTurns = 5; //常量声明式
         int scores[NumTurns];          //使用该常量
    };
    
    你所看到的是 NumTurns 的声明式而非定义式。通常 C++ 要求你对你所使用的任何东西提供一个定义式,但如果它是个 class 专属常量而又是 static 且为整数类型(integral type,例如 ints,chars,bools),则需要特殊处理。只要不取它们的地址,你可以声明它们而无需提供定义式。但是如果你取某个 class 专属常量的地址,或纵使你不取地址而你的编译器却(不争取地)坚持要看到一个定义式,你就必须另外提供定义式如下:
    const int GamePlayer::NumTurns;  //NumTurns 的定义式;它以在声明时获得初值,因此定义时可以不再设初值。
    
  • 如果你不想让别人获得一个 pointer 或 reference 指向你的某个整数常量, enum 可以帮助你实现这个约束。Enums 和 #defines 一样绝不会导致非必要的内存分配。(取一个 const 的地址是合法的,但取一个 enum 或 #define 的地址是非法的)
  • 另一个常见的 #define 的误用情况是以它实现宏,宏看起来像函数,但不会招致函数调用带来的额外开销。必须记住为宏中的所有实参加上小括号。 可以用 template inline 函数 获得宏带来的效率以及一般函数的所有可预料行为和类型安全性:
    template<typename T>
    inline void callWithMax(const T& a,const T& b){
      f(a > b ? a : b);
    }
    //这个 template 产生出一整群函数,每个函数都接受两个同型对象,并以其中较大者调用 f 。
    

条款 03:尽可能使用 const

  • char greeting[] = "Hello";
    char *p = greeting;                  //non-const pointer,non-const data
    const char* p = greeting;            //non-const pointer,const data
    char* const p =greeting;             //const pointer,non-const data
    const char* const = greeting;        //const poniter,const data
    
    如果关键字 const 出现在星号左边,表示指物是常量;如果出现在星号右边,表示指针自身是常量;如果出现在星号左右两边,表示指物和指针两者都是常量。
    如果指物是常量,有人将关键字 const 写在类型之前,有人把它写在类型之后、星号之前。两种写法含义相同:
    const Widget* pw;   //两者都是一个指针指向一个常量的 Widget 对象
    Widget const * pw;
    
  • 迭代器的作用就像个 T* 指针。声明迭代器为 const 就像声明指针为 const 一样(即声明一个 T* const 指针),表示这个迭代器不得指向不同的东西,但它所指之物是可以变动的。如果你希望迭代器所指的东西不可被改动(即希望 STL 模拟一个 const T* 指针),你需要的是 const_iterator;
  • const 最具威力的用法是面对函数声明时的应用。在一个函数声明式内,const 可以和函数返回值、各参数、函数自身(如果是成员函数)产生关联。
    • 令函数返回一个常量值,往往可以降低客户错误而造成的意外,而又不至于放弃安全性和高效性。
      class Rational {...};
      const Rational operator* (const Rational& lhs,const Rational& rhs);
      //误用的例子
      Rational a,b,c;
      ...
      (a * b) = c; //在 a * b 的成果上调用 operator=
      
      将 operator* 的回传值声明为 const 可以预防这个“没意思的赋值动作”,这就是该这么做的原因。
    • const 成员函数: 将 const 作用于成员函数的目的,是为了确认该成员函数可作用于 const 对象身上。两个函数如果只是常量性不同,可以被重载。
      • bitwise const :成员函数只有在不更改对象之任何成员变量(static 除外)时才可以说是 const 。也就是说它不能更改对象内的任何一个 bit 。这种论点的好处是很容易侦测违反点:编译器只需寻找成员变量的赋值动作即可。bitwise constness 正是 C++ 对常量性的定义,因此 const 成员函数不可以更改对象内任何 non-static 成员变量。(一个更改了“指针所指之物”的成员函数虽然不能算是 const ,但如果只有指针(而非其所指物)隶属于对象,那么称此函数为 bitwise const 不会引发编译异议。)
      • logical constness :一个 const 成员函数可以修改它所处理对象内的某些 bits ,但只有在客户端侦测不出的情况下才得如此。
      • mutable 释放掉 non-static 成员变量的 bitwise constness 约束。
    • 编译器强制实施 bitwise constness ,但你编写程序时应该使用“概念上的常量性”。
    • 当 const 和 non-const 成员函数有着实质等价的实现时,令 non-const 版本调用 const 版本可避免代码重复。

条款 04:确定对象被使用前已先被初始化

  • 永远在使用对象之前先将它初始化。对于无任何成员的内置类型,你必须手工完成此事。例如:
    int x = 0;                              //对 int 进行手工初始化
    const char* test = "A C-style string";  //对指针进行手工初始化
    double d;
    std::cin >> d;                          //以读取 input stream 的方式初始化
    
    置于内置类型以外的任何其他东西,初始化责任落在构造函数上。规则很简单:确保每一个构造函数都将对象的每一个成员初始化。
    • 别混淆了赋值和初始化
      ABEntry::ABEntry(const std::string& name,const std::string& address,const std::list<PhoneNumber>& phones){
        theName = name;                 //这些都是赋值而非初始化
        theAddress = address;
        thePhone = phones;
        numTimesConsulted = 0;
      }
      
      这会导致 ABEntry 对象带有你期望的值,但不是最佳做法。C++ 规定,对象的成员变量的初始化动作发生在进入构造函数本体之前。在 ABEntry 构造函数内,theName,theAddress 和 thePhones 都不是被初始化,而是被赋值。初始化的发生时间更早,发生于这些成员的 default 构造函数被自动调用之时(比进入 ABEntry 构造函数本体的时间更早)。但这对 numTimesConsulted 不为真,因为它属于内置类型,不保证一定在你所看到的那个赋值动作的时间点之前获得初值。
      ABEntry 构造函数的一个较佳写法是,使用所谓的 member initialization list (成员初值列)替换赋值动作:
      ABEntry::ABEntry(const std::string& name,const std::string& address,
      const std::list<PhoneNumber>& phones)
      :theName(name),                 //现在这些都是初始化
      theAddress(address),
      thePhones(phones),
      numTimesConsulted(0)
      { }                             //现在构造本体不必有任何赋值动作
      
      这个构造函数相比于上一个效率更高。基于赋值的那个版本首先调用 default 构造函数为 theName,theAddress 和 thePhones 设初值,然后立刻对它们赋予新值。default 构造函数的一切作为因此浪费了。成员初始化列表的做法避免了这一问题,因为初值列中针对各个成员而设的实参,被拿去作为各成员变量之构造函数的实参。(规定总是在成员初始化列中列出所有成员变量,以免还得记得那些成员变量可以无需初值) 本例中 theName 以 name 为初值进行 copy 构造,theAddress 以 address 为初值进行 copy 构造,thePhones 以 phones 为初值进行 copy 构造。**对于大多数类型而言,比起先调用 default 构造函数然后调用 copy assignment 操作符,单只调用一次 copy 构造函数是比较高效的,有时甚至高效得多。对于内置类型,其初始化和赋值成本相同,但为了一致性最好也通过成员初始化列表。同样道理,甚至当你想要 default 构造一个成员变量,你都可以使用成员初始化列,只要指定无物作为初始化实参即可。
      ABEntry::ABEntry()
          :theName(),                     //调用 theName 的 default 构造函数
          theAddress(),                   //调用 theAddress 的 default 构造函数
          thePhones(),                    //调用 thePhones 的 default 构造函数
          numTimesConsulted(0)            //记得将 numTimesConsulted 显式初始化为 0
      { }
      
  • C++ 有着十分固定的“成员初始化次序”:base classes 更早于其 derived classes 被初始化,而 class 的成员变量总是以其声明的次序被初始化(即使它们在成员初始化列中以不同的次序出现,也不会有任何影响(建议最好以其声明的次序为次序安排初始化列))。
  • 函数内的 static 对象称为 local static 对象,其他 static 对象称为 non-local static 对象。程序结束时 static 对象会被自动销毁,也就是它们的析构函数会在 main() 结束时被自动调用。
  • 所谓编译单元是指产出单一目标文件的那些源码。基本上它是单一源码文件加上其所含入的头文件。C++ 对于定义于不同编译单元的 non-local static 对象的初始化顺序并无明确定义。为免除“跨编译单元之初始化次序”的问题,请以 local static 对象替换 non-local static 对象

条款 05:了解 C++ 默默编写并调用哪些函数

  • 编译器可以暗自为 class 创建 default构造函数、copy 构造函数、copy assignment 操作符,以及析构函数。唯有当这些函数需要(被调用),它们才会被编译器创建出来。
  • default 构造函数和析构函数实质是编译器调用 base classes 和 non-static 成员变量的构造函数和析构函数。注意,编译器所产出的析构函数是个 non-virtual ,除非这个 class 的 base class 自身声明有 virtual 析构函数。至于 copy 构造函数和 copy assignment 操作符,编译器创建的版本只是单纯地将来源对象的每一个 non-static 成员变量拷贝到目标对象。
  • 如果你打算在一个 “内含 reference 成员” 的 class 内支持赋值操作,你必须自己定义 copy assignment 操作符。面对 “内含 const 成员” 的 classes,编译器的反应也一样。更改 const 成员是不合法的,所以编译器不知道如何在它自身生成的赋值函数内面对它们。
  • 如果某个 base classes 将 copy assignment 操作符声明为 private , 编译器将拒绝为其 derived classes 生成一个 copy assignment 操作符。毕竟编译器为 derived classes 所生成的 copy assignment 操作符想象中可以处理 base class 成分,但它们显然无法调用 derived classes 无权调用的成员函数。

条款 06:若不想使用编译器自动生成的函数,应该明确拒绝

  • 通常如果你不希望 class 支持某一个特定机能,只要不声明对应函数就是了。但这个策略对 copy 构造函数和 copy assignment 操作符却不起作用,因为如果你不声明它们,而某些人尝试调用它们,编译器会为你声明它们。
  • **所有编译器产出的函数都是 public 的。为阻止这些函数被创建出来,你得自行声明它们,并将其声明为 private。**藉由明确声明一个成员函数,阻止了编译器暗自创建其专属版本;而令这些函数为 private ,得以成功阻止人们调用它。一般而言,这样的做法并不绝对安全,因为 member 函数和 friend 函数还是可以调用你的 private 函数。最好的做法是 “将成员函数声明为 private 而且故意不实现它们”。
    class HomeForSale{
      public:
         ...
      private:
         ...
         HomeForSale(const HomeForSale&);           //只有声明
         HomeForSale& operator=(const HomeForSale&);
    };
    //有了上述定义,当客户企图拷贝 HomeForSale 对象,编译器会阻挠他。如果你不慎在 member 函数或 friend 函数中那么做,连接器会发出错误。
    
    将连接器错误转移至编译期是可能,只要在一个专门为了阻止 copying 动作而设计的 base class 内将 copy 构造函数和 copy assignment 操作符声明为 private 就可以办到。
    class Uncopyable{
      protected:
         Uncopyable(){}                          //允许 derived 对象构造和析构
         ~Uncopyable(){}
      private:
         Uncopyable(const Uncopyable&);          //但阻止 copying
         Uncopyable& operator=(const Uncopyable&);
    };
    //为阻止 HomeForSale 对象被拷贝,我们唯一需要做的就是继承 Uncopyable:
    class HomeForSale:private Uncopyable{        //class 不在声明 copy 构造函数和 copy assignment 操作符
         ...
    };
    

条款07:为多态基类声明 virtual 析构函数

  • C++ 明白指出,当 derived class 对象经由一个 base class 指针被删除,而该 base class 带着一个 non-virtual 析构函数,其结果未有定义————实际执行时通常发生的是对象的 derived 成分没有被销毁。消除这个问题的方法很简单:给 base class 一个 virtual 析构函数。 任何 class 只要带有 virtual 函数几乎确定应该也有一个 virtual 析构函数。如果 class 不含 virtual 函数,通常表示它并不意图被用作一个 base class。当 class 不企图被当作 base class,令其析构函数为 virtual 往往是个馊主意。
  • 欲实现出 virtual 函数,对象必须携带某些信息,主要用来在运行期决定哪一个 virtual 函数该被调用。这份信息通常是由一个所谓的 vptr (virtual table pointer) 指针指出。 vptr 指向一个由函数指针构成的数组,称为 vtbl (virtual table);每一个带有 virtual 函数的 class 都有一个相应的 vtbl。当对象调用某一 virtual 函数,实际被调用的函数取决于该对象的 vptr 所指的那个 vtbl————编译器从中寻找适当的函数指针。无端将所有的 classes 的析构函数声明为 virtual ,就像从未声明它们为 virtual 一样,都是错误的。只有当 class 内含有至少一个 virtual 函数,才为它声明 virtual 析构函数。
  • 一般而言纯虚函数的函数体是缺省的,但是也可以给出纯虚函数的函数体(此时纯虚函数仍然为纯虚函数,对应的类仍然为抽象类,还是不能实例化对象)调用纯虚函数的方法为:抽象类类名::纯虚函数名(实参表)。
  • 析构函数的运作方式是,最深层派生的那个 class 其析构函数最先被调用,然后是每一个 base class 的析构函数被调用。
  • “给 base classes 一个 virtual 析构函数”,这个规则只适用于 polymorphic (带多态性质的)base classes 身上。这种 base classes 的设计目的是为了用来“通过 base class 接口处理 derived class 对象”。并非所有 base classes 的设计目的都是为了多态用途。例如标准 string 和 STL 容器都不被设计作为 base classes 使用,更别提多态了。

条款 08:别让异常逃离析构函数

  • 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。
    1. 如果 close 抛出异常就结束程序。通常通过调用 abort 完成:
    DBConn::~DBConn(){
      try{db.close();}
      catch (...){
        //制作运转记录,记下对 close 调用的失败
        std::abort();
      }
    }
    2. 吞下因调用 close 而发生的异常:
    DBConn::~DBConn(){
      try{db.close();}
      catch (...){
        //制作运转记录,记下对 close 调用的失败
      }
    }
    
  • 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么 class 应该提供一个普通函数(而非在析构函数内)执行该操作。
    class DBConn{
      public:
         ...
         void close(){         //供客户使用的新函数
          db.close();
          closed=true;
         }
      ~DBConn(){
         if(!closed){
          try{
            db.close();
          }
          catch(...){
            制作运转记录,记下对 close 的调用失败
            ...
          }
         }
      }
      private:
         DBConnection db;
         bool closed;
    };
    

条款 09:绝不在构造和析构过程中调用 virtual 函数

条款 10:令 operator= 返回一个 reference to *this

  • 关于赋值,可以把它们写成连锁形式:
    int x, y, z;
    x = y = z = 15;           //赋值连锁形式
    
    赋值采用右结合律,所以上述连锁赋值被解析为:
    x = (y= (z = 15));
    
  • 为了实现“连锁赋值”,赋值操作符必须返回一个 reference 指向操作符左侧的实参:
    class Widget{
      public:
         ...
         Widget& operator=(const Widget& rhs){  //返回类型是一个 reference ,指向当前对象
          ...
          return *this;
         }
         ...
    };
    //这个协议适用于所有的赋值相关的运算:
    class Widget{
      public:
         ...
         Widget& operator+=(const Widget& rhs){  //这个协议适用于 +=,-=,*=,等等
          ...
          return *this;
         }
         Widget& operator=(int rhs){             //此函数也适用,即使此一操作符的参数类型不符协定
          ...
          return *this;
         }
         ...
    };
    
  • 题外话
    函数返回值是引用的情况:
    1. 函数返回值为引用时,若返回栈变量(局部变量),不能为其它引用的初始值(函数返回后就不存在了),不能做为左值。
    2. 函数返回值为引用时,若返回的是静态变量或是全局变量,可以成为其他引用的初始值,既可以作为左指,也可以作为右值。
    
    this 的说明
    1. this 是 C++ 中的一个关键字,也是一个 const 指针(它的值是不能被修改的),指向对象自己,通过它可以访问自己的所有成员。
    2. this 指针是所有成员函数的隐含参数,因此成员函数内部,它可以用来指向调用对象。
    3. 友元函数没有 this 指针,因为友元不是类的成员。只有成员函数才有 this 指针。
    4. 只有当前对象被创建后 this 才有意义,因此不能在 static 成员函数中使用。
    

条款 11:在 operator= 中处理“自我赋值”

  • 确保当对象自我赋值时 operator= 有良好的行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及 copy-and-swap (见如下代码)。
    class Widget{
      ...
      void swap(Widget& rhs);        //交换 *this 和 rhs 的数据
      ...
    };
    Widget& Widget::operator=(const Widget& rhs){
      Widget temp(rhs);              //为 rhs 数据制作一份复件
      swap(temp);                    //将 *this 数据和上述复件的数据交换
      returb *this;
    }
    
  • 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。

条款 12:复制对象时勿忘其每一个成分

  • Copying 函数应该确保复制“对象内的所有成员变量”及“所有 base class 成分”。
    任何时候只要承担起“为 derived class 撰写 copying 函数”的重大责任,必须很小心地也复制其 base class 成分。那些成分往往是 private ,所以你无法直接访问它们,应该让 derived class 的 copying 函数调用相应的 base class 函数:
    PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
       :Customer(rhs),                    //调用 base class 的 copy 构造函数
       priority(rhs.priority){
        logCall("PriorityCustomer copy constructor...");
       }
    PriorityCustomer&
    PriorityCustomer::operator=(const PriorityCustomer& rhs){
      logCall("PriorityCustomer copy assignment operator...");
      Customer::operator=(rhs);           //对 base class 成分进行赋值动作
      priority = rhs.priority;
      return *this;
    }
    //1. 复制所有 local 成员变量 2. 调用所有 base classes 内的适当的 copying 函数
    
  • 不要尝试以某个 copying 函数实现另一个 copying 函数。应该将共同机能放进第三个函数中,并由两个 copying 函数共同调用。

条款 13:以对象管理资源

  • 为防止资源泄露,请使用 RAII(资源取得时机便是初始化时机,资源在构造期间获得,在析构期间释放) 对象,它们在构造函数中获得资源并在析构函数中释放资源。
  • 两个常被用的 RAII classes 分别是 tr1::shared_ptr 和 auto_ptr。前者通常是较佳选择,因为其 copy 行为比较直观。若选择 auto_ptr 复制动作会使它(被复制物)指向 null。

条款 14:在资源管理类中小心 copying 行为

  • 复制 RAII 对象必须一并复制它所管理的资源,所以资源的 copying 行为决定 RAII 对象的 copying 行为。
  • 普遍而常见的 RAII class copying 行为是:抑制 copying、施行引用计数法。

条款 15:在资源管理类中提供对原始资源的访问

  • APIs 往往要求访问原始资源(raw resources),所以每一个 RAII class 应该提供一个“取得其所管理之资源”的办法。
    tr1::shared_ptr 和 auto_ptr 都提供一个 get 成员函数,用来执行显示转换,也就是它会返回智能指针内部的原始指针(的复件)。就像所有只能指针一样,tr1::shared_ptr 和 auto_ptr 也重载了指针操作符(operator-> 和 operator*),它们允许隐式转换至底部原始指针:
    class Investment{                            //investment 继承体系的根类
      public:
        bool isTaxFree() const;
        ...
    };
    Investment* createInvestment();              //factory 函数
    std::tr1::shared_ptr<Investment>             //令 tr1::shared_ptr 管理一笔资源
      pi1(createInvestment());
    bool taxablel = !(pi1->isTaxFree());         //经由 operator-> 访问资源
    ...
    std::auto_ptr<Investment> pi2(createInvestment());
                                                 //令 auto_ptr 管理一笔资源
    bool taxable2 = !((*pi2).isTaxFree());       //经由 operator* 访问资源
    ...
    
  • 对原始资源的访问可能经由显示转换或隐式转换。一般而言显示转换比较安全,但隐式转换对客户比较方便。
    operator 有两种用法:
    1. 操作符重载(operator overloading),格式如下:
      return_type T operator operand(parameter list)
      
    2. 隐式类型转换(opeartor casting),格式如下:
      operator cast_to_type_T()         //其中 T 是一个类型
      // operator int()     将类型隐式转换为 int 型
      
  • 可以用单个实参来调用的构造函数定义了从形参类型到该类型的一个转换。阻止这项功能可以使用 explicit 关键字,它只能用在构造函数上,并且只需在函数的声明时标注即可,在类函数的定义时不需要标注该关键字。通常除非有明显理由要定义隐式转换,否则,单形参构造函数设置为 explicit 可以避免错误,并且当转换有用时,用户可以显式地构造对象。

条款 16:成对使用 new 和 delete 时要采取相同的形式

  • 如果在 new 表达式中使用 [],必须在相应的 delete 表达式中使用 []。如果在 new 表达式中不使用 [],一定不要在相应的 delete 表达式中使用 []。

条款 17:以独立语句将 newed 对象置入智能指针

  • 以独立语句将 newed 对象置入智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄露。
    processWidget(std::tr1::shared_ptr<Widget>(new Widget),priority());  //不要这样做
    //请这样做
    std::tr1::shared_ptr<Widget> pw(new Widget); //在单独的语句内以智能指针存储 newed 所得对象
    processWidget(pw, priority());  //这个调用绝不至于造成泄露
    

条款 18:让接口容易被正确使用,不易被误用

  • “促进正确使用”的办法包括接口一致性,以及与内置类型的行为兼容。
  • “防止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。
  • tr1::shared_ptr 支持定制型删除器。这可防范 DLL 问题,可被用来自动解除互斥锁等等。

条款 19:设计 class 犹如设计 type

条款 20:宁以 pass-by-reference-to-const 替换 pass-by-value

  • 尽量以 pass-by-reference-to-const 替换 pass-by-value。前者通常比较高效,并可避免切割问题。
    bool validateStudent(const Student& s);  //这种传递方式效率高得多:没有任何构造函数或析构函数被调用
    
    以 by reference 方式传递参数也可以避免 slicing (对象切割)问题。当一个 derived class 对象以 by value 方式传递并被视为一个 base class 对象, base class 的 copy 构造函数会被调用,而“造成此对象的行为像个 derived class 对象”的那些特化性质全被切割掉了,仅仅留下一个 base class 对象。
    references 往往以指针实现出来,因此 pass by reference 通常意味着真正传递的是指针。因此如果对象属于内置类型(例如 int ),pass by value 往往比 pass by reference 的效率高。
  • 以上规则并不适用于内置类型,以及 STL 的迭代器和函数对象。对它们而言, pass-by-value 往往比较适当。

条款 21:必须返回对象时,别妄想返回其 reference

  • 绝不要返回 pointer 或 reference 指向一个 local stack 对象,或返回 reference 指向一个 heap-allocated 对象,或返回 pointer 或 reference 指向一个 local static 对象而有可能同时需要多个这样的对象。

条款 22:将成员变量声明为 private

  • 切记将成员变量声明为 private 。这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供 class 作者以充分的弹性。
  • protected 并不比 public 更具封装性。
    一旦将一个成员变量声明为 public 或 protected 而客户开始使用它,就很难改变那个成员变量所涉及的一切。因为如果这些成员变量要改变,就会有不可预知的大量代码受到破坏。

条款 23:宁以 non-member、non-friend 替换 member 函数

  • 宁以 non-member、non-friend 替换 member 函数。这样做可以增加封装性、包裹弹性和机能扩充性。
  • 考虑对象内的数据。愈少的代码可以看到数据(也就是访问它),愈多的数据可被封装,而我们也就愈能自由地改变对象数据,例如改变成员变量的数量、类型等等。如何测量“有多少代码可以看到某一块数据”呢?我们计算能够访问该数据的函数数量,作为一种粗糙的量测。愈多函数可访问它,数据的封装性就愈低。
  • friends 函数对 class private 成员的访问权力和 member 函数相同,因此两者对封装性的冲击力道也相同。
  • namespace 和 classes 不同,前者可以跨越多个源码文件而后者不能。将所有便利函数放在多个头文件内但隶属于同一个命名空间,意味着用户可以轻松扩充这一组便利函数。他们做的就是添加更多的 non-member non-friend 函数到此命名空间内。

条款 24:如所有参数皆需类型转换,请为此采用 non-member 函数

  • 如果你需要为某个函数的所有参数(包括被 this 指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个 non-member。
    //考虑以下例子
    class Rational{
      public:
        Rational(int numberator=0,int denominator=1);  //构造函数刻意不为 explicit;允许  int-to-Rational 隐式类型转换
        int numberator() const;   //分子(numberator)和分母(denominator)的访问函数
        ...
        const Rational operator*(const Rational& rhs) const;   //将 operator* 写成成员函数的写法
      private:
        ...
    };
    Rational oneEight(1,8);
    Rational oneHalf(1,2);
    Rational result = oneEight * oneHalf;  //很好
    result = result * oneEight;            //很好
    result = oneHalf * 2;                  //很好
    result = 2 * oneHalf;                  //糟糕!!!!!!!
    以对应的函数形式重写上述两个式子,便会发现问题:
    result = oneHalf.operator*(2);         //很好      做了隐式类型转换
    result = 2.operator*(oneHalf);         //糟糕!!!!!!!
    解决办法是让 operator* 成为一个 non-member 函数,允许编译器在每一个实参身上执行隐式类型转换:
    class Rational{
      ...
    };
    const Rational operator*(const Rational& lhs,const Rational  rhs){  //现在成为了一个 non-member 函数
      return Rational(lhs.numberator() * rhs.numberator(), lhs.denominator() * rhs.denominator());
    }
    Rational oneFourth(1,4);
    Rational result;
    result = oneFourth * 2;         //没问题
    result = 2 * oneFourth;         //没问题
    
  • 无论如何你可以避免 friend 函数就该避免

条款 25:考虑写出一个不抛异常的 swap 函数

  • 当 std::swap 对你的类型效率不高时,提供一个 swap 成员函数,并确定这个函数不抛出异常。
  • 如果你提供一个 member swap,也该提供一个 non-member swap 用来调用前者。对于 classes (而非 templates),也请特化 std::swap。
  • 调用 swap 时应针对 std::swap 使用 using 声明式,然后调用 swap 并且不带任何“命名空间资格修饰”。
  • 为“用户定义类型”进行 std templates 全特化是好的,但千万不要尝试在 std 内加入某些对 std 而言全新的东西。
  • 实例:
    namespace std{
      template<typename T>
      void swap(T& a, T& b){     //std::swap 的缺省实现
        T temp(a);               //只要类型 T 支持 copying (通过 copy 构造函数和 copyassignment 操作符完成),缺省的 swap 实现代码就会帮你置换类型为 T 的对象,你不需要为此另外再做任何工作。
        a = b;
        b = temp;
      }
    }
    --------------------------------------------------------------------
    //pimpl 手法(pointer to implementation)
    class WidgetImpl{             //针对 Widget 数据而设计的 class
      public:
        ...
      private:
        int a,b,c;                //可能有很多数据,意味着复制时间很长
        std::vector<double> v;   
    };
    class Widget{
      public:
        Widget(const Widget& rhs);
        Widget& operator=(const Widget& rhs){  //复制 Widget 时,令它复制其 WidgetImpl 对象
          ...
          *pImpl = *(rhs.pImpl);
          ...
        }
        ...
      private:
        WidgetImpl* pImpl;       //指针,所指对象内含 Widget 数据
    };                           //一旦要置换两个 Widget 对象值,我们唯一需要做的就是置换其 pImpl 指针,但缺省的 swap 算法不知道这一点。它不止复制三个 Widgets,还复制三个 WIdgetImpl 对象。非常缺乏效率
    
  • 总结:
    • 首先,如果 swap 的缺省实现码对你的 class 或 class template 提供可接受的效率,你不需要额外做任何事。任何尝试置换(swap)那种对象的人都会取得缺省版本,而那将有良好的运作。
    • 其次,如果 swap 缺省实现版本的效率不足(那几乎总是意味着你的 class 或 template 使用了某种 pimpl(pointer to implementation 指针指向实现,这样其实只用交换指针即可) 手法),试着做以下事情:
      1. 提供一个 public swap 成员函数,让它高效地置换你的类型的两个对象值。这个函数绝不该抛出异常。
        class Widget{
          public:
            ...
            void swap(Widget& other){
              using std::swap;                //这个声明指代可以以下代码使用 std 内的 swap
              swap(pImpl, other.pImpl);       //如要置换 Widgets 就置换其 pImpl 指针
            }
            ...
        };
        
      2. 在你的 class 或 template 所在的命名空间内提供一个 non-member swap,并令它调用上述 swap 成员函数。
        namespace WidgetStuff{
          ...                                //模板化的 WidgetImpl 等等
          template<typename T>               //同前,内含 swap 成员函数
          class Widget{ ... };
        
          ...
          template<typename T>               //non-member swap 函数,这里并不属于 std 命名空间
          void swap(Widget<T>& a, Widget<T>& b){
            a.swap(b);
          }        
        }
        
      3. 如果你正编写一个 class (而非 class template),为你的 class 特化 std::swap。并令它调用你的 swap 成员函数。
        • 通常我们不能够(不被允许)改变 std 命名空间内的任何东西,但可以(被允许)为标准 templates (如 swap)制造特化版本,使它专属于我们自己的 classes (例如 Widget)。
        namespace std{
          template<>                         //全特化
          void swap<Widget>(Widget& a, Widget& b){
            a.swap(b);                       //若要置换 Widgets,调用其 swap 成员函数(也就是上面的那个成员函数)
          }
        }
        
    • 最后,如果你调用 swap,请确定包含一个 using 声明式,以便让 std::swap 在你的函数内曝光可见,然后不加任何 namespace 修饰符,赤裸地调用 swap。
    • 一旦编译器看到对 swap 的调用,它们便查找适当的 swap 并调用它。C++ 的名称查找法则(name lookup rules)确保将找到 global 作用域或 T 所在之命名空间的任何 T 专属的 swap。如果 T 是 Widget 并位于命名空间 WidgetStuff 内,编译器会使用“实参取决之查找规则”(argument-dependent lookup)找出 WidgetStuff 内的 swap 。如果没有 T 专属之 swap 存在,编译器会使用 std 内的 swap ,这得感谢 using 声明式让 std::swap 在函数内曝光。然而即便如此编译器还是比较喜欢 std::swap 的 T 专属特化版,而非一般化的那个 template ,所以如果你已针对 T 将 std::swap 特化,特化版会被编译器挑中。也就是说编译器会根据实参类型按照如下顺序找到合适的 swap 调用:
      global 或命名空间内的专属 swap -> std 内的专属特化版 -> std 内的一般化版
      

条款 26:尽可能延后变量定义式的出现时间

  • 不只应该延后变量的定义,直到非得使用该变量的前一刻为止,甚至应该尝试延后这份定义知道能够给它初值实参为止。如果这样,不仅能够避免构造(和析构)非必要对象,还可以避免无意义的 default 构造行为。

条款 27:尽量少做转型动作

  • C 风格的转型动作:
    (T)expression         //将 expression 转型为 T
    
    函数风格的转型动作:
    T(expression)         //将 expression 转型为 T
    
  • C++ 提供四种新式转型:
    1. const_cast 通常用来将对象的常量性转除(cast away the constness)。它也是唯一有此能力的 C+±style 转型操作符。
      const_cast<T>(expression)
      
    2. dynamic_cast 主要用来执行“安全向下转型”(safe downcasting),也就是用来决定某对象是否归属继承体系中的某个类型。它是唯一无法由旧式语法执行的动作,也是唯一可能耗费重大运行成本的转型动作。
      dynamic_cast<T>(expression)
      
    3. reinterpret_cast 意图执行低级转型,实际动作(及结果)可能取决于编译器,这也就表示它不可移植。例如将一个 pointer to int 转型为一个 int 。这一类转型在低级代码以外很少见。
      reinterpret_cast<T>(expression)
      
    4. static_cast 用来强迫隐式转换(implicit conversions),例如将 non-const 对象转为 const 对象,或将 int 转为 double 等等。它也可以用来执行上述多种转换的反向转化,例如将 void* 指针转换为 typed 指针,将 pointer-to-base 转为 pointer-to-derived。但它无法将 const 转为 non-const————这只有 const_cast 才办得到。
      static_cast<T>(expression)
      
  • 如果可以,尽量避免转型,特别是在注重效率的代码中避免使用 dynamic_casts(执行速度相当慢,很普遍的实现版本是基于“class 名称之字符串比较”)。如果有个设计需要转型动作,试着发展无需转型的替代设计。(“使用类型安全容器”或“将 virtual 函数往继承体系上方移动”
  • 如果转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调用该函数,而不需将转型放进他们自己的代码。
  • 宁可使用 C+±style (新式)转型,不要使用旧式转型。前者很容易辨别出来,而且也比较有着分门别类的职掌。

条款 28:避免返回 handles 指向对象内部成分

  • 避免返回 handles (包括 references、指针、迭代器)指向对象内部。遵守这个条款可以增加封装性,帮助 const 成员函数的行为像个 const ,并将发生“虚吊号码牌”(dangling handles)的可能性降到最低。
  • 绝不该令成员函数返回一个指针指向“访问级别较低”的成员函数。如果那么做,后者实际访问级别就会调高如同前者(访问级别较高者),因为客户可以取得一个指针指向那个“访问级别较低”的函数,然后通过那个指针调用它。
  • 如果 public-member 函数返回一个 private-data (或 private-function)内部成员的 reference(或 pointer、迭代器),那将导致这个私有成员可以被通过返回的引用或指针进行修改,实质上私有成员也就变成了 public 的了。

条款 29:为“异常安全”而努力是值得的

  • 异常安全函数即使发生异常也不会泄漏资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型。
  • “强烈保证”往往能够以 copy-and-swap 实现出来,但“强烈保证”并非对所有函数都可实现或具备现实意义。
  • 函数提供的“异常安全保证”通常最高只等于其所调之各个函数的“异常安全保证”中的最弱者。

条款 30:透彻了解 inlining 的里里外外

  • 隐喻的 inline 申请是将函数定义于 class 定义式内。
  • inline 函数通常一定被置于头文件内,因为大多数构建环境在编译过程中 inlining ,而为了将一个“函数调用”替换为“被调函数本体”,编译器必须知道那个函数长什么样子。
  • templates 通常也被置于头文件内,因为它一旦被使用,编译器为了将它具现化,需要知道它长什么样子。
  • 一个表面上看似 inline 的函数是否真的 inline ,取决于你的构建环境(building environments),主要取决于编译器。
  • inline 函数无法随着程序库的升级而升级。 如果 f 是程序库中的一个 inline 函数,客户将“f 函数本体”编进其程序中,一旦程序库设计者决定改变 f ,所有用到 f 的客户端程都必须重新编译。然而如果 f 是 non-inline 函数,一旦它有任何修改,客户端只需重新连接就好,远比重新编译的负担少得多。将大多数 inlining 限制在小型、被频繁调用的函数身上。
  • 大部分调试器对 inline 函数都束手无策,毕竟很难在一个不存在的函数内设置断点。
  • 不要只因为 function templates 出现在头文件,就将它们声明为 inline 。

条款 31:将文件间的编译依存关系降至最低

  • 支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是 Handle classes 和 Interface classes。
  • 程序库头文件应该以“完全且仅有声明式”的形式存在。 这种做法不论是否涉及 templates 都适用。
  • C++ 提供关键字 export,允许将 template 声明式和 template 定义式分割于不同的文件内。

条款 32:确定你的 public 继承塑模出 is-a 关系

  • “public 继承”意味 is-a。适用于 base classes 身上的每一件事情一定也适用于 derived classes 身上,因为每一个 derived class 对象也都是一个 base class 对象。

条款 33:避免遮掩继承而来的名称

  • derived classes 内的名称会遮掩 base classes 内的名称(C++ 的名称遮掩规则,名称相同的不管变量的类型是否相同或函数的返回值以及参数列表是否相同,内层作用域的成分会遮掩外层作用域内名称相同的成分)。在 public 继承下从来没有人希望如此。
  • 为了让被掩盖的名称再见天日,可使用 using 声明式或转交函数(forwarding functions)。
    //使用 using 声明式:
    class Base{
      private:
        int x;
      public:
        virtual void mf1() = 0;
        virtual void mf1(int);
        virtual void mf2();
        void mf3();
        void mf3(double);
        ...
    };
    class Derived: public Base{
      public:
        using Base::mf1;            //让 Base class 内名为 mf1 和 mf3 的所有东西在 Derived 作用域内都可见(并且 public)
        using Base::mf3;
        virtual void mf1();
        void mf3();
        void mf4();
        ...
    };
    //转交函数
    class Base{
      private:
        int x;
      public:
        virtual void mf1() = 0;
        virtual void mf1(int);
        virtual void mf2();
        void mf3();
        void mf3(double);
        ...
    };
    class Derived: public Base{
      public:
        virtual void mf1(){             //转交函数暗自成为 inline
          Base::mf1();
        }
        ...
    };
    inline 转交函数的另一个用途是为那些不支持 using 声明式的老旧编译器另辟一条新路,将继承而得的名称汇入 derived class 作用于内。
    
  • derived class 作用域被嵌套在了 base class 作用域内。

条款 34:区分接口继承和实现继承

  • 接口继承和实现继承不同。在 public 继承之下,derived classes 总是继承 base clas 的接口。
  • pure virtual 函数只具体指定接口继承。
    我们可以为 pure virtual 提供定义。但调用它的唯一途径是“调用时明确指出其 class 名称”:
    class Shape{
      public:
        virtual void draw() const = 0;
        ...
    };
    class Rectangle: public Shape{...};
    void Shape::draw(){                //pure virtual 函数实现
      缺省行为,画出图形
    }
    void Rectangle::draw(){
      Rectangle 的特有行为
    }
    Shape* rect = new Rectangle();
    rect->draw();                      //调用 Rectangle 的特有行为
    rect->Base::draw();                //调用缺省实现
    
  • 简朴的(非纯)impure virtual 函数具体指定接口继承及缺省实现继承。
  • non-virtual 函数具体指定接口继承以及强制实现继承。如果成员函数是 non-virtual 函数,意味是它并不打算在 derived classes 中有不同的行为。实际上一个 non-virtual 函数表现的不变性凌驾其特异性,因为它表示不论 derived class 变得多么特异化,它的行为都不可以改变。就其自身而言。

条款 35:考虑 virtual 函数以外的其它选择

  • virtual 函数的替代方案包括 NVI手法(non-virtual interface)及 Strategy 设计模式的多种形式。NVI 手法自身是一个特殊形式的 Template Methmod 设计模式。
  • 将机能从成员函数移到 class 外部函数,带来的一个缺点是,非成员函数无法访问 class 的 non-public 成员。
  • tr1::function 对象的行为就像一般函数指针。这样的对象可接纳“与给定之目标签名式兼容”的所有可调用物。

条款 36:绝不重新定义继承而来的 non-virtual 函数

  • non-virtual 函数都是静态绑定的。如果 pB 被声明为一个 pointer-to-B,通过 pB 调用的 non-virtual 函数永远是 B 所定义的版本,即使 pB 指向一个类型为“B 派生之 class”的对象(而且这个对象有自己的该 non-virtual 函数实现)。
  • virtual 函数是动态绑定的,所以没有这个问题。不管是用父类指针还是派生类指针指向的派生类,最终调用的函数是它本身类型对应的函数。

条款 37:绝不重新定义继承而来的缺省参数值

  • 对象的所谓静态类型,就是它在程序中被声明时所采用的类型。
  • 对象的所谓动态类型则是指“目前所指对象的类型”。
  • Virtual 函数系动态绑定而来,意思是调用一个 virtual 函数时,究竟调用哪一份函数实现代码,取决于发出调用的那个对象的动态类型。
  • virtual 函数是动态绑定,而缺省参数值确实静态绑定的。意思是你可能会在“调用一个定义于 derived class 内的 virtual 函数”的同时,却使用 base class 为它所指定的缺省参数值。
  • 绝不重新定义继承而来的缺省参数值,因为缺省参数值都是静态绑定的,而 virtual 函数——你唯一应该覆写的东西——确实动态绑定的。

条款 38:通过复合塑模出 has-a 或“根据某物实现出”

  • 复合(composition)的意义和 public 继承完全不同。
  • 在应用域(application domain),复合意味着 has-a (有一个)。在实现域(implementation domain),复合意味着 is-implemented-in-terms-of(根据某物实现出)。

条款 39:明智而审慎地使用 private 继承

  • private 继承意味着 is-implemented-in-terms-of(根据某物实现出)。只有实现部分被继承,接口部分被略去。它通常比复合(composition)的级别低。但是当 derived class 需要访问 protected base class 的成员,或需要重新定义继承而来的 virtual 函数时,这么设计是合理的。
  • 和复合(composition)不同,private 继承可以造成 empty base 最优化。这对致力于“对象尺寸最小化”的程序库开发者而言,可能很重要。
    class Empty{ };                  //没有数据,所以其对象应该不使用任何内存
    class HoldAnInt{                 //应该只需要一个 int 空间
      private:
        int x;                         
        Empty e;                     //应该不需要内存
    };
    然而,sizeof(HoldAnInt) > sizeof(int);面对“大小为零之独立(非附属)对象”,通常 C++ 官方勒令默默安插一个 char 到空对象内。然而对齐需求可能造成编译器为类似 HoldAnInt 这样的 class 加上一些衬垫,所以有可能 HoldAnInt 对象不止一个 char 大小。
    如果你继承 Empty ,而不是内含一个那种类型的对象:
    class HoldAnInt: private Empty{
      private:
        int x;
    };
    几乎可以确定,sizeof(HoldAnInt) = sizeof(int)。这是所谓的 EBO (empty base optimization;空白基类最优化)。需要注意的是,EBO 一般只在单一继承(而非多重继承)才行,统治 C++ 对象布局的那些规则通常表示 EBO 无法被施行于“拥有多个 base”的 derived classes 身上。
    

条款 40:明智而审慎地使用多重继承

  • 多重继承比单一继承复杂。它可能导致新的歧义,以及对 virtual 继承的需要。
  • virtual 继承会增加大小、速度、初始化(及赋值)复杂度等等成本。如果 virtual base clases 不带任何数据,将是最具实用价值的情况。
  • 多重继承的确有正当用途。其中一个情节涉及“public 继承某个 Interface class”和“private 继承某个协助实现的 class”的两相结合。
  • 对于 virtual base classes 的忠告:第一,非必要不使用 virtual bases 。平常请使用 non-virtual 继承。第二,如果你必须使用 virtual base classes ,尽可能避免其中放置数据。

条款 41:了解隐式接口和编译期多态

  • classes 和 templates 都支持接口和多态。
  • 对 classes 而言接口是显式的,以函数签名为中心。多态则是通过 virtual 函数发生于运行期。
  • 对 templates 而言接口是隐式的,奠基于有效表达式。多态则是通过 template 具现化和函数重载解析发生于编译器。
  • 编译期多态:“以不同的 template 参数具现化 function templates ”会导致调用不同的函数。——————哪一个重载函数被调用(发生在编译器)
  • 运行期多态:将于运行期根据 object 的动态类型决定究竟调用哪一个函数。——————哪一个 virtual 函数该被绑定(发生在运行期)
  • 通常显式接口由函数的签名式(也就是函数名称、参数类型、返回类型)构成。隐式接口并不基于函数签名式,而是由有效表达式组成。
    template<typename T>
    void doProcessing(T& w){
      if(w.size() > 10 && w != someNastyWidget){
        ...
      }
    }
    

条款 42:了解 typename 的双重意义

  • 当声明 template 类型参数时,class 和 typename 的意义完全相同。所以声明 template 参数时,前缀关键字 class 和 typename 可互换。
    template<class T> class Widget;         //使用 class
    template<typename T> class Widget;      //使用 typename
    
  • template 内出现的名称如果相依于某个 template 参数,称之为从属名称(dependent names)。如果从属名称在 class 内呈嵌套状,我们称之为嵌套从属名称(nested dependent name)。嵌套从属名称有可能导致解析困难:
    template<typename C>
    void print2nd(const C& container){
      C::const_iterator *x;            //??????有歧义
      ...
    }
    
    看起来好像在声明 x 为一个 local 变量,它是个指针,指向一个 C::const_iterator 。但是 C::const_iterator 可能是个类型,也可能是个变量名称。
    C++ 有个规则可以解析此一歧义状态:如果解析器在 template 中遭遇一个嵌套从属名称,它便假设这名称不是个类型,除非你告诉它是。只要紧临它之前放置关键字 typename :
    template<typename C>
    void print2nd(const C& container){
      if(container.size() >= 2){
        typename C::const_iterator iter(container.begin());
        ...
      }
    }
    
    一般性规则很简单:任何时候当你要在 template 中指涉一个嵌套从属类型名称,就必须在紧临它的前一个位置放上关键字 typename 。
    typename 只被用来验明嵌套从属类型名称;其他名称不该有它的存在:
    template<typename C>
    void f(const C& container,               //不允许使用 typename
            typename C::iterator iter);      //一定要使用 typename
    
  • 请使用关键字 typename 标识嵌套从属类型名称;但不得在 base class lists (基类列)或 member initialization list (成员初始列)内以它作为 base clss 修饰符
    template<typename C>
    class Derived: public Base<T>::Nested{  //在 base class list 中不允许 typename
      public:
        explicit Derived(int x)
        :Base<T>::Nested(x){                //在 member initialization list 中不允许 typename
          typename Base<T>::Nested temp;    //既不在 base class list 也不在 member initialization list 中,作为一个 base class 修饰符需加上 typename
          ...
        }
        ...
    };
    

条款 43:学习处理模板化基类内的名称

  • 可在 derived class templates 内通过 “this->”指涉 base class templates 内的成员名称,或藉由一个明白写出的 “base class 资格修饰符”完成。

条款 44:将与参数无关的代码抽离 templates

  • templates 生成多个 classes 和多个函数,所以任何 template 代码都不该与某个造成膨胀的 template 参数产生相依关系。
  • 因非类型模板参数而造成的代码膨胀,往往可消除,做法是以函数参数或 class 成员变量替换 templates 参数。
  • 因类型模板参数而造成的代码膨胀,往往可降低,做法是让带有完全相同二进制表述的具现类型共享实现码。

条款 45:运用成员函数模板接受所有兼容类型

  • 所谓智能指针是“行为像指针”的对象,并提供指针没有的机能。STL 容器的迭代器几乎总是智能指针。
  • 请使用 member function templates (成员函数模板)生成“可接受所有兼容类型”的函数。
  • 如果你声明 member templates 用于“泛化 copy 构造”或“泛化 assignment 操作”,你还是需要声明正常的 copy 构造函数和 copy assignment 操作符。

条款 46:需要类型转换时请为模板定义非成员函数

  • 当我们编写一个 class template ,而它所提供之“于此 template 相关的”函数支持“所有参数之隐式类型转换”时,请将那些函数定义为“class template”内部的 friend 函数。

条款 47:请使用 traits classes 表现类型信息

  • traits classes 使得“类型相关信息”在编译期可用。它们以 templates 和 “templates 特化”完成实现。
  • 整合重载技术后,traits classes 有可能在编译期对类型执行 if…else 测试。

条款 48:认识 template 元编程

  • Template metaprogramming(TMP,模板元编程)可将工作由运行期移往编译器,因而得以实现早期错误侦测和更高的执行效率。
  • TMP 可被用来生成“基于政策选择组合”(base on combinations of policy choices)的客户定制代码,也可用来避免生成对某些特殊类型并不合适的代码。

条款 49:了解 new-handler 的行为

  • set_new_handler 允许客户指定一个函数,在内存分配无法获得满足时被调用。
  • Nothrow new 是一个颇为局限的工具,因为它只适用于内存分配;后继的构造函数调用还是可能抛出异常。

条款 50:了解 new 和 delete 的合理替换时机

  • 有许多理由需要写个自定的 new 和 delete ,包括改善效能、对 heap 运用错误进行调试、收集 heap 使用信息。

条款 51:编写 new 和 delete 时需固守常规

  • operator new 应该内含一个无穷循环,并在其中尝试分配内存,如果它无法满足内存需求,就该调用 new-handler。它也应该有能力处理 0 bytes 申请。Class 专属版本则还应该处理“比正确大小更大的(错误)申请”。
  • operator delete 应该在收到 null 指针时不做任何事。Class 专属版本则还应该处理“比正确大小更大的(错误)申请”。

条款 52:写了 placement new 也要写 placement delete

  • 当你写一个 placement operator new ,请确定也写出了对应的 placement operator delete。如果没有这么做,你的程序可能会发生隐微而时断时续的内存泄漏。
  • 当你声明 placement new 和 placement delete,请确定不要无意识(非故意)地遮掩了它们的正常版本。

条款 53:不要轻忽编译器的警告

  • 严肃对待编译器发出的警告信息。努力在你的编译器的最高(最严苛)警告级别下争取“无任何警告”的荣誉。
  • 不要过度倚赖编译器的报警能力,因为不同的编译器对待事情的态度并不相同。一旦移植到另一个编译器上,你原本倚赖的警告信息有可能消失。

条款 54:让自己熟悉包括 TR1 在内的标准程序库

  • C++ 标准程序库的主要机能由 STL、iostreams、locales 组成。并包含 C99 标准程序库。
  • TR1 添加了智能指针(例如 tr1::shared_ptr)、一般化函数指针(tr1::function)hash-based 容器、正则表达式(regular expressions)以及另外 10 个组件的支持。
  • TR1 自身只是一份规范。为获得 TR1 提供的好处,你需要一份实物。一个好的实物来源是 Boost

条款 55:让自己熟悉 Boost

  • Boost 是一个社群,也是一个网站。致力于免费、源码开放、同僚复审的 C++ 程序库开发。Boost 在 C++ 标准化过程中扮演深具影响力的角色。
  • Boost 提供许多 TR1 组件实现品,以及其他许多程序库。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

咕噜咕噜的喵喵

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值