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

时间: 2024-08-25 23:12:12

Effective C++ 总结(三)的相关文章

Effective C++ 条款三 尽可能使用const

参考资料:http://blog.csdn.net/bizhu12/article/details/6672723      const的常用用法小结 1.用于定义常量变量,这样这个变量在后面就不可以再被修改     const int val = 90;      val = 100;   错误 2. 保护传参时参数不被修改,如果使用引用传递参数或按地址传递参数给一个函数,在这个函数里这个参数的值若被修改, 则函数外部传进来的变量的值也发生改变,若想保护传进来的变量不被修改,可以使用const

《Effective Java 第三版》新条目介绍

前言 从去年的3月份起我就在开始读<Effective Java 第二版>,当然,我读的是中文版的:可能是我理解能力还不行,对于书中的内容总是感觉理解困难:就拿第一章的内容「创建和销毁对象」来说吧,这是我读的次数最多的一章,想必原因大家也是明白的,每次我读不下去的时候,我就从头开始读,所以,现在我对这本书的第一章是最为熟悉的了.后来,有一次我上网看到有网友说这本书确实和绝大部分的翻译书籍一样,对于有些原文中的内容翻译的不是很流畅,所以会导致阅读的人感觉难以理解:于是,我就斗胆下了本英文的原版来

Effective Java 第三版——3. 使用私有构造方法或枚类实现Singleton属性

Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将近8年的时间,但随着Java 6,7,8,甚至9的发布,Java语言发生了深刻的变化. 在这里第一时间翻译成中文版.供大家学习分享之用. 3. 使用私有构造方法或枚类实现Singleton属性 单例是一个仅实例化一次的类[Gamma95].单例对象通常表示无状态对象,如函数(条目 24)或一个本质上唯一的系统

Effective Java 第三版——10. 重写equals方法时遵守通用约定

Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将近8年的时间,但随着Java 6,7,8,甚至9的发布,Java语言发生了深刻的变化. 在这里第一时间翻译成中文版.供大家学习分享之用. 10. 重写equals方法时遵守通用约定 虽然Object是一个具体的类,但它主要是为继承而设计的.它的所有非 final方法(equals.hashCode.toStr

Effective Java 第三版——12. 始终重写 toString 方法

Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将近8年的时间,但随着Java 6,7,8,甚至9的发布,Java语言发生了深刻的变化. 在这里第一时间翻译成中文版.供大家学习分享之用. 12. 始终重写 toString 方法 虽然Object类提供了toString方法的实现,但它返回的字符串通常不是你的类的用户想要看到的. 它由类名后跟一个"

Effective Java 第三版——14.考虑实现Comparable接口

Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将近8年的时间,但随着Java 6,7,8,甚至9的发布,Java语言发生了深刻的变化. 在这里第一时间翻译成中文版.供大家学习分享之用. ?14.考虑实现Comparable接口 与本章讨论的其他方法不同,compareTo方法并没有在Object类中声明. 相反,它是Comparable接口中的唯一方法.

Effective Java 第三版——18. 组合优于继承

Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将近8年的时间,但随着Java 6,7,8,甚至9的发布,Java语言发生了深刻的变化. 在这里第一时间翻译成中文版.供大家学习分享之用. 18. 组合优于继承 继承是实现代码重用的有效方式,但并不总是最好的工具.使用不当,会导致脆弱的软件. 在包中使用继承是安全的,其中子类和父类的实现都在同一个程序员的控制之

Effective Java 第三版——19. 如果使用继承则设计,并文档说明,否则不该使用

Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将近8年的时间,但随着Java 6,7,8,甚至9的发布,Java语言发生了深刻的变化. 在这里第一时间翻译成中文版.供大家学习分享之用. 19. 如果使用继承则设计,并文档说明,否则不该使用 条目 18中提醒你注意继承没有设计和文档说明的"外来"类的子类化的危险. 那么为了继承而设计和文档

Effective Java 第三版——20. 接口优于抽象类

Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将近8年的时间,但随着Java 6,7,8,甚至9的发布,Java语言发生了深刻的变化. 在这里第一时间翻译成中文版.供大家学习分享之用. 20. 接口优于抽象类 Java有两种机制来定义允许多个实现的类型:接口和抽象类. 由于在Java 8 [JLS 9.4.3]中引入了接口的默认方法(default met

Effective Java 第三版——21. 为后代设计接口

Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将近8年的时间,但随着Java 6,7,8,甚至9的发布,Java语言发生了深刻的变化. 在这里第一时间翻译成中文版.供大家学习分享之用. 21. 为后代设计接口 在Java 8之前,不可能在不破坏现有实现的情况下为接口添加方法. 如果向接口添加了一个新方法,现有的实现通常会缺少该方法,从而导致编译时错误. 在