Effective C++ 总结(三)

五.实现    
    


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

 

  如果你定义了一个变量且该类型带一个构造函数或析构函数,当程序到达该变量时,你要承受构造成本,而离开作用域时,你要承受析构成本。为了减少这个成本,最好尽可能延后变量定义式的出现时间。举例说明:

string encryptPassword(const string& password) 
{
  string encrypted; //(1)
  if (password.length() < MINIMUM_PASSWORD_LENGTH) {
   throw logic_error("Password is too short");
 }
 //进行必要的操作,将口令的加密版本放进encrypted之中;
 
  string encrypted; //(2)
  return encrypted;
}

  encrypted应该在(2)处定义,因为如果在(1)处定义,如果抛出异常,那么encrypted的构造和析构还是要执行,浪费系统资源!

  通过默认构造函数构造出一个对象然后对它赋值”比“直接在构造函数时指定初值”效率差。“尽可能延后”的真正意义应该是:你不只应该延后变量的定义,直到非得使用该变量的前一刻为止,甚至应该尝试延后这份定义直到能够给它初值实参为止。

    //方法A:定义循环外
    Widget w;
     for (int i = 0; i < n; ++i) 
    { 
        w = some value dependent on i;
        ...
    }//1个构造函数+1个析构函数+n个赋值操作;
    //方法B:定义循环外
     for (int i = 0; i < n; ++i) 
    {
        Widget w(some value dependent on i);  
        ...
    }//n个构造函数+n个析构函数

  除非:1.你知道赋值成本比“构造+析构”成本低;2.你正在处理代码中效率高度敏感的部分,否则应该使用方法B。


    请记住:

  • 尽可能延后变量定义式的出现。这样做可增加程序的清晰度并改善程序效率。

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

  C++规则的设计目标之一是,保证“类型错误”绝不可能发生。不幸的是,转型(casts)破坏了类型系统。那可能导致任何种类的麻烦,有些容易辨识,有些非常隐晦。
  C风格的转型动作看起来像这样:
     (T)expression    //将expression转型为T  

  函数风格的转型动作看起来像这样:
     T(expression)    //将expression转型为T
  

  C++还提供四种新式转型:
  const_cast:通常被用来将对象的常量性转除;即去掉const。
  dynamic_cast:主要用来执行“安全向下转型”,即:基类指针/引用到派生类指针/引用的转换。如果源和目标类型没有继承/被继承关系,编译器会报错;否则必须在代码里判断返回值是否为NULL来确认转换是否成功。有条件转换,动态类型转换,运行时类型安全检查(转换失败返回NULL)
  reinterpret_cast:意图执行低级转型,将数据从一种类型的转换为另一种类型,也就是说将数据以二进制存在的形式进行重新解释。实际动作可能取决于编译器,这也就表示它不可移植
  static_cast:用来静态类型转换,强制类型转换,运行时不做类型检查,因而可能是不安全的。例如将non-const转型为const,int转型为double等等。 static_cast也可以进行基类和子类之间转换:其中子类指针转换成父类指针是安全的;但父类指针转换成子类指针是不安全的。(基类和子类之间的动态类型转换建议用dynamic_cast)

  upcast:Just same as dynamic_cast. 由于不用做runtime类型检查,效率比dynamic_cast高;

  downcast:不安全。不建议使用。

#include <iostream>  
using namespace std;  
class Base  
{  
public:  
    virtual int foo(){  
        cout<<"Base"<<endl;  
        return 0;  
    };  
};  
  
class Derived:public Base  
{  
public:  
    int foo(){  
        cout<<"Derived"<<endl;  
        return 0;  
    }  
};  
int main()  
{  
    Base *b=new Base;  
    Derived *d1=static_cast<Derived*>(b);  
    d1->foo(); // 正确  
    Derived *d2=dynamic_cast<Derived*>(b);  
    d2->foo(); //错误  
} 

尽量使用新式转型:

  • 它们很容易在代码中被辨识出来,因而得以简化“找出类型系统在哪个地点被破坏”的过程。
  • 各转型动作的目标愈窄化,编译器愈可能诊断出错误的运用。   

    请记住:

  • 如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_casts。如果有个设计需要转型动作,试着发展无需转型的替代设计。
  • 如果转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调用该函数,而不需将转型放进他们自己的代码内。
  • 宁可使用C++-style(新式)转型,不要使用旧式转型。前者很容易辨识出来,而且也比较有着分门别类的执掌。

 


 

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

class Point  
{  
public:  
    Point(int x, int y);  
    void SetX(int newVal);  
    void SetY(int newVal);  
private:  
    int x_cor;  
    int y_cor;  
};  
  
struct RectData  
{  
    Point ulhc;    // 矩形左上角的点  
    Point lrhc;    // 矩形右下角的点  
};  
class Rectangle  
{  
private:  
    shared_ptr<RectData> pData;  
public:  
    Point& upperLeft()const{ return pData->lrhc; }  
    Point& lowerRight()const{ return pData->ulhc; }  
};  

  上面的代吗中Point是表示坐标系中点的类,RectData表示一个矩形的左上角与右下角点的点坐标。Rectangle是一个矩形的类,包含了一个指向RectData的指针。

我们可以看到了uppLeft和lowerRight是两个const成员函数,它们的功能只是想向客户提供两个Rectangle相关的坐标点,而不是让客户修改Rectangle。但是两个函数却都返回了references指向了private内部数据,调用者于是可以通过references更改内部数据

  这给了我们一些警示:成员变量的封装性只等于“返回其reference”的函数的访问级别;如果const成员函数传出一个reference,后者所指数据与对象自身有关联,而它又被存储于对象之外,那么这个函数的调用者可以修改那笔数据。

  handles(号码牌,用于取得某个对象)指reference、指针和迭代器,它们返回一个“代表对象内部数据”的handle。

  解决方法:我们可以对上面的成员函数返回类型上加上const

public:  
    const Point& upperLeft()const{ return pData->lrhc; }  
    const Point& lowerRight()const{ return pData->ulhc; } 

  但是函数返回一个handle代表对象内部成分还总是危险的,因为可能会造成dangling handles(空悬的号牌)。比如某个函数返回GUI对象的外框(bounding box)。

class GUIObject{  
    //..  
};  
const Rectangle boundingBox(const GUIObject &obj);          
GUIObject* pgo;  
const Point* pUpperLeft = &(boundingBox(*pgo).upperLeft());  

  对boundingBox返回的是一个新的,暂时的Rectangle对象temp,而pUpperLeft指向的是temp的Points。

  当const Point* pUpperLeft = &(boundingBox(*pgo).upperLeft());执行完之后,temp将会被销毁,从而导致temp内的Points析构,最终将导致pUpperLeft 指向一个不存在的对象,从而造成悬空、虚吊。

 请记住:

  • 避免返回handles(包括reference、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助const成员函数的行为像个const,并将发生“虚吊号码牌”(dangling handles)的可能性降至最低。   

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

 

   Inline(内联)函数,多棒的点子!它们看起来像函数,动作像函数,比宏好得多,可以调用它们又不需蒙受函数调用所招致的额外开销。你实际获得的比想象的还多,编译器有能力对执行语境相关最优化。然而编写程序就像现实生活一样,没有白吃的午餐。inline函数也不例外,这样做可能增加你的目标码
  如果 inline 函数的本体很小,编译器针对“函数本体”所产生的码可能比针对“函数调用”所产出的码更小。果真如此,将函数inlining确实可能导致较小的目标码和较高的指令高速缓存装置击中率。
     记住,inline 只是对编译器的一个申请,不是强制命令。这项申请可以隐喻提出,也可以明确提出。隐喻方式是将函数定义于class定义式内,这样的函数通常是成员函数,friend函数也可被定义于class内,如果真是那样,它们也是被隐喻声明为inline。明确声明inline函数的做法则是在其定义式钱加上关键字inline。
     Inline函数通常一定被置于头文件内,因为大多数建置环境(building environment)在编译过程中进行inlining,而为了将一个“函数调用”替换为“被调用函数的本体”,编译器必须知道那个函数长什么样子。
     Template通常也被置于头文件内,因为它一旦被使用,编译器为了将它具现化,需要知道它长什么样子。
     Template的具现化与inlining无关。如果你正在写一个template而你认为所有根据此template具现出来的函数都应该inlined,请将此template声明为inline;但如果你写的template没有理由要求它所具现的每一个函数都是inlined,就应该避免将这个template声明为inline。
     一个表面上看似inline的函数是否真实inline,取决于你的建置环境(building environment),主要取决于编译器。
     有的时候虽然编译器有意愿inlining某个函数,还是可能为该函数生成一个函数本体(函数指针,构造函数,析构函数)。比如,当程序要取某个inline函数的地址,编译器通常必须为此函数生成一个oulined函数本体,因为编译器没有能力让一个指针指向一个并不存在的函数。

inline void f(){..}   
void (*pf)()=f;  
..  
f();  //这个将被inline  
pf(); //这个或许不被inline,因为它通过函数指针完成

  对程序开发而言,将上述所有考虑牢记在新很是重要,但若从纯粹实用观点出发,有一个事实比其它因素更重要:大部分调试器面对inline函数都束手无策。
  这使我们在决定哪些函数该被声明为inline而哪些函数不该时,掌握一个合乎逻辑的策略。一开始先不要将任何函数声明为inline,或至少将inlining施行范围局限在那些“一定成为inline”或“十分平淡无奇”的函数身上。

 

总结:

1. inline函数的调用,是对函数本体的调用,是函数的展开,使用不当会造成代码膨胀。

2. 大多数C++程序的inline函数都放在头文件,inlining发生在编译期。

3. inline函数只代表“函数本体”,并没有“函数实质”,是没有函数地址的(内联优化从目标文件中去掉了该函数的入口点,符号表中也没有该函数的名称)。

4. inlining在大多数编译器中编译期行为, inline 函数无法随着程序库的升级而升级。换句话说如果f 是程序库内的一个inline 函数,客户将”f 函数本体”编进其程序中,一旦程序库设计者决定改变f ,所有用到f 的客户端程序都必须重新编译。但如果f是non-inline函数,当修改f后,客户端只需要重新连接就好了。

 

值得注意的是:

1. 构造函数与析构函数往往不适合inline。因为这两个函数都包含了很多隐式的调用,而这些调用付出的代价是值得考虑的。可能会有代码膨胀的情况。

2. inline函数无法随着程序库升级而升级。因为大多数都发生在编译期,升级意味着重新编译。

3. 大部分调试器(VS2010可以)是不能在inline函数设断点的。因为inline函数没有地址。

 

     请记住:

  • 将大多数inlining限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级更容易,也可使潜在的代码膨胀问题最小化,是程序的速度提升机会最大化。
  • 另外,对function templates的inline也要慎重,保证其所有实现的函数都应该inlined后再加inline。   

 


 

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

 

  这个问题产生是源于希望编译时影响的范围尽量小,编译效率更高,维护成本更低,这一需求。

  实现这个目标首先第一个想到的就是,声明与定义的分离,用户的使用只依赖声明,而不依赖定义(也就是具体实现)。

  但C++的Class的定义式却不仅仅只有接口,还有实现细目(这里指实现接口需要的私有成员)。而有时候我们需要修改的通常是接口的实现方法,而这一修改可能需要添加私有变量,但这个私有变量对用户是不应该可见的。但这一修改却放在了定义式的头文件中,从而造成了,使用这一头文件的所有代码的重新编译。

  于是就有了pimpl(pointer to implementation)的方法。用pimpl把实现细节隐藏起来,在头文件中只需要一个声明就可以,而这个poniter则作为private成员变量供调用。

  这里会有个有意思的地方,为什么用的是指针,而不是具体对象呢?这就要问编译器了,因为编译器在定义变量时是需要预先知道变量的空间大小的,而如果只给一个声明而没有定义的话是不知道大小的,而指针的大小是固定的,所以可以定义指针(即使只提供了一个声明)。

  这样把实现细节隐藏了,那么实现方法的改变就不会引起别的部分代码的重新编译了。而且头文件中只提供了impl类的声明,而基本的实现都不会让用户看见,也增加了封装性。

  结构应该如下:

class PersonImpl;  
class Person {  
public:  
    ...  
private:  
    std::tr1::shared_ptr<PersonImpl> PersonImpl;  
};  

  这一种类也叫handle class

  另一种实现方法就是用带factory函数的interface class。就是把接口都写成纯虚的,实现都在子类中,通过factory函数或者是virtual构造函数来产生实例。

  声明文件时这么写:

class Person  
{  
public:  
    static shared_ptr<Person> create(const string&,  
        const Data&,  
        const Adress&);  
}; 

  定义实现的文件这么写

class RealPerson :public Person  
{  
public:  
    RealPerson(...);  
    virtual ~RealPerson(){}  
    //...  
private:  
    // ...  
};  

  有了RealPerson之后,就可以写出Person::create函数了:

std::tr1::shared_ptr<Person> Person::create(const string& name  
                                            const Data& birthday  
        <span style="white-space:pre">            </span>    const Address& adrr)  
{  
    return std::tr1::shared_ptr<Person>(new RealPerson(name,birthday,addr));  
}  

请记住:

  • 支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是Handle classed和Interface classes。
  • 程序库头文件应该以“完全且仅有声明式”(full and declaration-only forms)的形式存在。这种做法不论是否涉及templates都适用

六.继承与面向对象设计
  

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

  以C++进行面向对象编程,最重要的一个规则是:public inheritance(公有继承)意味is-a(是一种)的关系。
  如果你令class D以public形式继承class B,你便是告诉C++编译器(以及你的代码读者)说,每一个类型为D的对象同时也是一个类型为B的对象,反之不成立。你的意思是B比D表现出更一般化得概念,而D比B表现出更特殊化的概念。你主张:“B对象可派上用场的任何地方,D对象一样可以派上用场”,因为每一个D对象都是一种(是一个)B对象。反之如果你需要一个D对象,B对象无法效劳,因为虽然每个D对象都是一个B对象,反之并不成立。
     在C++领域中,任何函数如果期望获得一个类型为基类的实参(而不管是传指针或是引用),都也愿意接受一个派生类对象(而不管是传指针或是引用)。(只对public继承才成立。)

     好的接口可以防止无效的代码通过编译,因此你应该宁可采取“在编译期拒绝”的设计,而不是“运行期才侦测”的设计。
    
     请记住:

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

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


    C++的名称遮掩规则所做的唯一事情就是:遮掩名称。只要Derived class有和base class中相同的函数名,不管其返回值,参数类型,参数个数是否相同,也不管成员函数是纯虚函数,非纯虚函数或非虚函数。只要名称相同就发生覆盖。派生类的作用域嵌套在基类的作用域内。
     (1)derived classes内的名称会遮掩base classes内的名称(即使变量的类型不同,或者函数的参数不同)。如下面代码所示:

class Base {  
 private:  
  int x;  
 public:  
  virtual void mfl() = 0;  
  virtual void mfl(int);  
  virtual void mf2();  
  void mf3 ();  
  void mf3(double);  
};  
   
class Derived: public Base {  
 public:  
  virtual void mfl();  
  void mf3 ();  
  void mf4 ();  
  …  
};  
  
Derived d;  
int x;  
  
d.mfl();   //正确,调用Derived::mfl  
d.mfl(x);  //错误,名称被覆盖  
d.mf2();   //正确,调用Base::mf2  
d.mf3();   //正确,调用Derived::mf3  
d.mf3(x);  //错去,名称被覆盖</span>  

  (2) 为了让被遮掩的名称再见天日,可使用using 声明式或转交函数( forwarding functions) 。

 

  (a) 使用using声明式

class Base {  
 private:  
  int x;  
 public:  
  virtual void mfl() = 0;  
  virtual void mfl(int);  
  virtual void mf2();  
  void mf3 ();  
}  
   
void mf3(double); class Derived: public Base {  
 public:  
  using Base::mfl; //使用using 声明式  
  using Base: :mf3; //使用using 声明式  
  virtual void mfl();  
  void mf3 ();  
  void mf4();  
}  
   
Derived d  
int x;  
d.mf1 () ; //仍调用Derived: :mfl  
d.mf1 (x); //调用Base: :mfl  
d.mf2 () ; //调用Base: :mf2  
d.mf3 ();//调用Derived: :mf3  
d.mf3 (x); //调用Base: :mf3</span> 

  (b) 使用转交函数

class Derived: private Base (  
 public:  
  virtual void mfl () //转变函数(forwading  function) ,  
  { Base:: mfl ( );} //暗自成为inline  
}  
Derived d;  
int x;  
d.mfl();    //正确,调用Derived::mfl  
d.mfl(x);   //错误,名称被遮掩

请记住:

  • derived calsses内的名称会遮掩base classes内的名称。在public继承下从来没有人希望如此。   
  • 为了让被遮掩的名称再见天日,可使用using声明式或转交函数(forwarding function)。


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

  本条款主要讲函数接口继承(也就是声明)和函数实现继承,以及pure virtual 函数、simple(impure) virtual 、non-virtual函数之间的差异。
(1)接口继承和实现继承不同。在public 继承之下, derived classes 总是继承base class的接口。
(2) pure virtual 函数只具体指定接口继承。(要求继承者必须重新实现该接口)
(3) 简朴的(非纯) impure virtual 函数具体指定接口继承及缺省实现继承(继承者可自己实现该接口也可使用缺省实现)。
(4) non-virtual 函数具体指定接口继承以及强制性实现继承。(继承者必须使用该接口的实现)
class Shape {  
 public:  
  virtual void draw( ) const = 0; //pure virtual 函数  
  virtual void error(const std::string& msg); //简朴的(非纯) impure virtual 函数  
  int objectID ( ) const;// non-virtual 函数  
};  

请记住:

  • 接口继承和实现继承不同。在public继承之下,derived classes总是继承base class的接口。
  • pure virtual函数只具体制定接口继承。
  • 简朴的(非纯)impure virtual函数具体制定接口继承及缺省实现继承。
  • non-virtual函数具体制定接口继承以及强制性实现继承。

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

   

  本条款告诉程序员,当需要使用virtual 函数时,可以考虑其他选择。

  Virtual函数的替代方案是:
  (1) 使用non-virtual interface(NVI)手法。思想是:将virutal函数放在private中,而在public中使用一个non-virtual函数调用该virtual函数。优点是:可以做一些预处理、后处理工作。

class GameCharacter {  
 public:  
  int healthValue() const{             // 1. 子类不能重定义  
    ...                               // 2. preprocess  
    int retVal = doHealthValue();     // 2. 真正的工作放到虚函数中  
    ...                               // 2. postprocess  
   return retVal;  
  }  
 private:  
  virtual int doHealthValue() const {   // 3. 子类可重定义  
    ...  
   }  
};  

  例如:

class Base {  
public:  
    Base(int i):val(i){};  
  
    int healthValue() const  
    {                                              
        int retVal = doHealthValue();  
        return retVal;  
    }  
private:  
    virtual int doHealthValue() const   
    {             
        return val;  
    }  
      
    int val;  
};  
class Derived: public Base {  
public:  
    Derived(int i,int j):Base(i),val(j){};  
private:  
    virtual int doHealthValue() const   
    {  
        return val;  
    }  
    int val;  
};  
  
int main()  
{  
    Derived d(1,2);  
    cout<<d.healthValue()<<endl;  
    return 0;  
}  
  输出:2

  (2)将virtual函数替换为“函数指针成员变量”(这是Strategy设计模式中的一种表现形式),见下面代码。优点是每个对象拥有自己的函数实现,也可在运行时改变计算函数;缺点是:该函数不能访问类中的私有成员(若要访问,必须由公有成员提供接口)
class GameCharacter;  
int defaultHealthCalc(const GameCharacter& gc); // default algorithm  
class GameCharacter {  
public:  
 typedef int (*HealthCalcFunc)(const GameCharacter&);  
 explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)  
  : healthFunc(hcf)  
 { }  
 int healthValue() const {  
  return healthFunc(*this);  
 }  
 ...  
private:  
 HealthCalcFunc healthFunc;  
};  
  (3) 以tr1::function成员变量替换virtual函数,这允许使用任何可调用物搭配一个兼容于需求的签名式。这也是Strategy设计模式的某种形式。这种方式比上面的函数指针更灵活、限制更少:
    [1]返回值不一定是int,与其兼容即可; 
    [2]可以是function对象; 
    [3]可以是类的成员函数。
  
  (4) 继承体系内的virtual函数替换为另一个继承体系内的virtual函数。这是Strategy设计模式的传统实现手法。这种方式最大的优点是:可以随时添加新的算法。举例:
class GameCharacter;  
   
class HealthCalcFunc {  
   
 public:  
   
  ...  
   
  virtual int calc(const GameCharacter& gc) const  
   
   { ... }  
   
  ...  
   
};  
   
HealthCalcFunc defaultHealthCalc;  
   
class GameCharacter {  
   
 public:  
   
  explicit GameCharacter(HealthCalcFunc *phcf = &defaultHealthCalc)  
   
   : pHealthCalc(phcf)  
   
    {}  
   
  int healthValue() const {  
   
   return pHealthCalc->calc(*this);  
   
 }  
   
  ...  
   
private:  
   
HealthCalcFunc *pHealthCalc;  
   
};  

 


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

//类的定义  
class B{  
public:  
        void func(){ cout<<“B”;}  
 virtual void func2(){ cout<<“B”;}  
};  
  
class D:public B{  
publicvoid func() { cout<<“D”;}  
 virtual void func2(){ cout<<“D”;}  
};  
  
//下面是对B和D的使用  
D dObject;  
B* bPtr = &dObject;  
D* dPtr = &dObject;  
  
//下面这两种调用方式:  
bPtr->func();  //调用B::func  
dPtr->func();  //调用D::func  
bPtr->func2();  //调用D::func  
dPtr->func2();  //调用D::func 
  解释:在C++继承中,virtual函数是动态绑定的,调用的函数跟指针或者引用实际绑定的对象有关,而non-virtual函数是静态绑定的,调用的函数只跟声明的指针或者引用的类型相关。
 
  • 不要重新定义继承而来的non-virtual函数。   

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


    对于non-virtual函数,上一条款说到,“绝不重新定义继承而来的non-virtual函数”,而对于继承一个带有缺省参数值的virtual函数,也是如此。即绝不重新定义继承而来的缺省参数值。因为:virtual函数系动态绑定(dynamically bound),而缺省参数值确实静态绑定(statically bound)。意思是你可能会在“调用一个定义于派生类内的虚函数”的同时,却使用基类为它所指定的缺省参数值。

class Shape{  
   
 public:  
   
  enum Color{RED,GREEN,BLUE};  
   
  virtual void draw(Color color = RED)const = 0;  
   
  ...  
   
};  
   
class Circle:public Shape{  
   
 public:  
   
  //竟然改变缺省参数值  
   
  virtual void draw(Color color = GREEN)const{ ... }  
   
};  
   
Shape* pc = new Circle; //静态绑定为RED  
pc->draw(); //注意调用的是: Circle::draw(RED),也就是说,此处的draw函数是基类和派生类的“混合物”  
Circle* pc = new Circle; //静态绑定为GREEN  

  为什么缺省参数是静态绑定而不是动态绑定呢?主要原因是运行效率。如果缺省参数采用动态绑定,那么编译器就必须有某种方法在运行期为virtual函数决定适当的缺省参数,这样会使程序的执行效率低下并且实现机理更加复杂。

  聪明的做法是考虑替代设计,如条款35中的一些virutal函数的替代设计,其中之一是NVI手法,令base class内的一个public non-virtual函数调用private virtual函数。

class Shape  
{  
public:  
    enum ShapeColor{ Red, Green, Blue };  
    virtual void draw(ShapeColor color = Red)const  
    {  
        doDraw(color);  
    }  
private:  
    virtual void doDraw(ShapeColor color)const = 0;  
};  
class Rectangle :public Shape  
{  
private:  
    virtual void doDraw(ShapeColor color)const;  
};  

请记住:

  • 绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而virtual函数——你唯一应该覆写的东西——却是动态绑定。

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

 

  复合或包含意味着has-a。如果我们想设计一个自己的set,我们思考后觉得可以用list来实现它,但是如果我把它设计出list的一个派生类,就会有问题,因为父类的所有行为在派生类都是被允许的,而list允许元素重复,而set则显然不行,所以set与list之间不符合is-a关系,我们可以把list设计为set的一个成员,即包含关系(has-a)。

 

 


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

 

   明智而审慎地使用private继承
  (1)如果class之间的继承关系是private。编译器不会自动将一个derived class对象转化为一个base class对象。由private base class继承而来的所有成员,在derived class中都会变成private属性,纵使它们在base class中原来是protected或public属性。

class Person {..}  
class Student:private Person{..}  
  
void eat(const Person&);  
Person p;  
Student s;  
  
eat(p);  //正确  
eat(s);  //错误  

  (2)private继承“并不存在is-a关系”的classes,而是has-a或is-implemented-in-terms-of的继承模型。如果我们只想利用base class的一部分功能,而又不想把base class暴露给其他外部对象使用,可以使用private继承。

 

      主要有三种使用场合:

      (a)其中一个derived class 需要访问base class的protected成员;

      (b)derived class 需要重新定义base class中一个或多个virtual函数。

      (c)需要对empty classes的空间最优化,如下面的代码:

class Empty{ }; //empty class  
class HoldsAnyInt{  
private:  
 int x;  
 Empty e;  
};//sizeof(HoldsAnyInt) =8。//对于大小为0的独立对象,通常C++会在对象内需要安插一个char, 并且有位对齐要求。  
  
class HoldsAnyInt::private Empty{  
private:  
 int x;  
}; //sizeof(HoldsAnyInt) == sizeof(int),这个就是EBO(empty based optimization 空白基类优化)。

 


 

 条款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的初始化工作,无论是多少层的继承都是。

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。