《Effective C++》(六)

来源:互联网 发布:智能电视直播软件2017 编辑:程序博客网 时间:2024/06/05 22:22

  • 继承与面向对象设计
    • 条款32确定你的public继承塑模出is-a关系
    • 条款33避免遮掩继承而来的名称
    • 条款34区分接口继承和实现继承
    • 条款35考虑virtual函数以外的其他选择
    • 条款36绝不重新定义继承而来的 non-virtual 函数
    • 条款37绝不重新定义继承而来的缺省参数值
    • 条款38通过复合塑模出 has-a 或 根据某物实现出
    • 条款39明智而审慎的使用private继承
    • 条款40明智而审慎的使用多重继承

6 继承与面向对象设计

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

1.”public继承” 意味 “is-a”的关系
例如,假设学生继承与人,则有:每个学生都是人,但并非每个人都是学生。这就是is-a。

2.public继承和is-a之间的等价关系听起来非常简单,但有时候可能被误导,比如:企鹅是一种鸟,鸟可以飞,但如果我们用这样的形式来描述这种关系:

class Bird  {  public:    virtual void fly();    // 鸟可以飞    ...  };  class Penguin : public Bird  {    // 企鹅是一种鸟    ...  }; 

这种继承,可能出现企鹅会飞的误导。

3.良好的解决方案:为企鹅重新定义fly函数,令它产生一个运行期错误。

void error(const std::string& msg);  class Penguin : public Bird  {  public:    virtual void fly()  {  error("Attempt to make a penguin fly!");  }    ...  }; 

4.class Square 应该以 public继承 class Rectangle 吗?
某些可施行与矩形身上的事(例如宽度可独立于其高度被外界修改)却不可实行于正方形身上(宽度总应该与高度一样)。
但是,public继承主张,能够实行于base class对象身上的每件事情,每件事情也同时可以实行于derived class身上。

5.is-a并非是唯一存在于class之间的关系。另两个常见的关系是has-a(有一个)和is-implemented-in-terms-of(根据实物实现出)

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

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

1.名称遮掩规则

    int x;                           // global变量      void someFunc()      {        double x;                   // local变量        std::cin>>x;             // 读一个值,赋给local变量      }  

当编译器处于someFunc的作用域内并遭遇名称x时,它在local作用域内查找是否有什么东西带着这个名称。如果找到,就不再查找其他作用域,如果找不到,向更大范围查找。

2.作用域的查找顺序:由内至外

    class Base  {      private:        int x;      public:        virtual void mf1() = 0;  //纯虚函数      virtual void mf2();        void mf3();        ...      };      class Derived : public Base  {      public:        virtual void mf1();        void mf4();        ...      };  

这里写图片描述
假设derived class 内的mf4的实现码部分像这样:

    void Derived::mf4()      {        ...        mf2();        ...      }  

当编译器看到使用名称mf2,必须要知道它指什么,有没有声明过,所以开始查各作用域:
▪ 查找local作用域(就是mf4函数所覆盖的那部分),没有
▪ 查找外围作用域,class Derived 作用域,没有
▪ 再向外扩一轮,base class 作用域,找到, 停止查找。
如果在 base class 作用域也没有查找到,会继续查找 base class 的namespace作用域,最后找到 global作用域。

3.名称遮掩规则以作用域为基础

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();    void mf3();  //重载或重写一个继承而来的non-virtual函数  void mf4();    ...  }; 

base class内所有名为mf1和mf3的函数都被derived class内的mf1和mf3函数遮盖(隐藏)掉了。从名称查找观点来看, Base::mf1 和 Base::mf3 不再被 Derived继承

    Derived d;      int x;      ...      d.mf1();                    // 没问题,调用Derived::mf1      d.mf1(x);                  // 错误! 因为Derived::mf1遮掩了Base::mf1       d.mf2();                    // 没问题,调用Base::mf2      d.mf3();                    // 没问题,调用Derived::mf3      d.mf3(x);                  // 错误! 因为Derived::mf3遮掩了Base::mf3  

这些行为背后的基本理由是为了防止你在程序库或应用框架内建立新的derived class时附带地从疏远的base class继承重载函数。

4.使用 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作用域都可见  using Base::mf3;    virtual void mf1();    void mf3();    void mf4();    ...  }; 
Derived d;  int x;  ...  d.mf1();                    // 没问题,调用Derived::mf1  d.mf1(x);                  // OK了,调用Base::mf1   d.mf2();                    // 没问题,调用Base::mf2  d.mf3();                    // 没问题,调用Derived::mf3  d.mf3(x);                  // OK了,调用Base::mf3 

如果你继承base class并加上重载函数,而你又希望重新定义或覆写其中一部分,那么你必须为那些原本会被遮掩的每个名称引入一个using声明式,否则某些你希望继承的名称会被遮掩。

5.inline转交函数(forwarding function)

    class Base  {      public:        virtual void mf1() = 0;        virtual void mf1(int);        ...                                      // 与之前相同      };      class Derived : private Base  {  // Derived以private形式继承Base(如果是public 继承,将继承所有)    public:        virtual void mf1()               // 转交函数,暗自成为inline(原因,见条款30)        {  Base::mf1();  }        ...      };      ...      Derived d;      int x;      d.mf1();                           // 很好,调用的是Derived::mf1      d.mf1(x);                         // 错误! Base::mf1被遮掩了

假设Derived以private形式继承Base,而Derived唯一想继承的mf1是那个无参数版本。
using声明式在这里排不上用场,因为using声明式会令继承而来的某给定名称之所有同名函数在derived class中都可见。

6.小结:
<1>derived class 内的名称会遮掩base class内的名称。在public继承下从来没人希望如此。
<2>为了让被遮掩的名称再见天日,可使用 using声明式 或 转交函数。

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

1.public继承有两部分:函数接口继承(function interfaces)和函数实现继承(function implementations),两部分可以分开控制是否继承。

2.抽象类Shape和它的继承者们

class Shape  {  public:  //成员函数的接口总是会被继承  virtual void draw() const = 0;  //声明pure virtual函数的目的是为了让derived classes只继承函数接口  virtual void error( const std::string& msg);  //声明impure virtual函数的目的是为了让derived classes继承该函数的接口和缺省实现  int objectID() const;  //声明non-virtual函数的目的是为了让derived classes继承函数接口和一份强制性实现  ...  };  class Rectangle : public Shape  {  ...  };  class Ellipse : public Shape  {  ...  };
Shape* ps = new Shape;    // error!Shape是抽象类,不能创建实体  Shape* ps1 = new Rectangle;    // 没问题  ps1->draw();    // 调用Rectangle的draw函数  Shape* ps2 = new Ellipse;    // 没问题  ps2->draw();    // 调用Ellipse的draw函数  // 调用 Shape的draw函数  ps1->Shape::draw();  //可以为pure virtual函数提供一份实现代码,但调用它的唯一途径是“调用时明确指出其class名称”ps2->Shape::draw(); 

3.“利用pure virtual函数必须在dereived classes中重新声明,但它们也可以拥有自己的实现”这一特性,将virtual函数的“接口继承”和“缺省实现继承”分离

class Airplane  {  public:    virtual void fly(const Airport& destination) = 0;    ...  };  void Airplane::fly(const Airport& destination)  // pure virtual的函数实现  {    // 缺省行为,将飞机飞向指定目的地  }  class ModelA : public Airplane  {  public:    virtual void fly(const Airport& destination)    {  Airplane::fly(destination);  }  //明确提出继承fly  ...  };  class ModelB: public Airplane  {  public:    virtual void fly(const Airport& destination)    {  Airplane::fly(destination);   }    ...  };  class ModelC: public Airplane  {  public:    virtual void fly(const Airport& destination);    ...  };  void ModelC::fly(const Airport& destination)  {    // 指定C型飞机的航线  //没有明确提出继承fly,须自行实现} 

4.避免将所有函数都声明为 non-virtual,这样会让 派生类 没有足够空间去展现它们的特点,尤其是non-virtual的析构函数。

5.避免所有的函数都声明为 virtual,这可能是class设计者缺乏坚定立场的前兆,不变行(invariant)凌驾于特异性(specialization)时,果断定义成non-virtual

6.小结:
<1>接口继承和实现继承不同。在public继承下,派生类总是继承基类的接口
<2>pure virtual函数只具体指定接口继承
<3>impure virtual函数具体指定接口继承与一份缺省实现继承
<4>non-virtual函数具体指定接口继承以及一份强制性实现继承

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

这部分涉及到几个设计模式,暂时跳过

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

1.派生类中,如果重新定义基类的函数,将会遮掩基类的相应函数,通过派生类调用该函数,永远是派生类的版本,而非基类的版本。

class B {public:    void mf();    ...};class D : public B  {public:    void mf();    // 重新定义mf,遮掩了基类B的mf函数(可详见条款33)    ...};
D x;    //x是一个类型为D的对象B* pB = &x;     // 被声明为一个pointer-to-B,获得一个指针指向xD* pD = &x;     // 获得一个指针指向xpB->mf();       // 调用 B::mfpD->mf();       // 调用 D::mf

non-virtual函数如B::mf 和 D::mf 都是静态绑定。意思是,由于pB被声明为一个pointer-to-B,通过pB调用的non-virtual函数永远是B所定义的版本,即使pB指向一个类型为“B的派生类”的对象。
virtual函数是动态绑定。假如mf是个virtual函数,不论通过pB或pD调用mf都会导致调用D::mf和pD真正指的都是一个类型为D的对象。

2.任何情况下都不该重新定义一个继承而来的non-virtual函数,否则会导致“精神分裂”行为。如果要重定义,请使用virtual函数。

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

1.virtual函数是动态绑定,缺省参数值是静态绑定。
静态绑定又名前期绑定,动态绑定又名后期绑定。

2.对象的静态类型指对象在声明时所采用的类型,动态类型则指“目前所指对象的类型”。

class Shape {public:    enum ShapeColor  { Red, Green, Blue };    //  绘制自己    virtual void draw(ShapeColor color = Red) const = 0;    ...};class Rectangle : public Shape {public:     // 重定义缺省参数值,糟糕的写法    virtual void draw(ShapeColor color = Green) const;    ...};class Circle : public Shape  {public:    virtual void draw(ShapeColor color) const;    //     ...};...Shape *ps;  // 静态类型为Shape*,无动态类型Shape *pc = new Circle;     // 静态类型为Shape*,动态类型为Circle*Shape *pr = new Rectangle;  // 静态类型为Shape*,动态类型为Rectangle*

3.virtual函数是动态绑定,意思是调用一个virtual函数时,究竟调用哪一份函数实现代码,取决于发出调用的那个对象的动态类型

pc->draw(Shape::Red);   //调用Circle::draw(Shape::Red)pr->draw(Shape::Red);   //调用Rectangle::draw(Shape::Red)

4.由于virtual函数是动态绑定,缺省参数值是静态绑定。可能会导致:调用一个定义于derived class内的virtual函数时,却使用base class为其所指定的缺省参数值

pr->draw(); //实际调用Rectangle::draw(Shape::Red)

理论上来讲,pr的动态类型是Rectangle* ,所以调用的应该是其virtual函数,其缺省参数应该是Green, 但由于 pr 的静态类型是Shape*, 所以此一调用的缺省参数值来自Shape class,也就是Red。

5.建议的写法:NVI(non-virtual interface)手法
让基类的public non-virtual函数 调用 private virtual函数(不管公有还是私有,只要是虚函数,它的函数地址都会放在虚函数表vftable中,可以调用),让 non-virtual函数负责指定缺省参数值,virtual函数负责实现具体的东西。

class Shape {public:    enum ShapeColor { Red, Green, Blue };    void draw(ShapeColor color = Red) const//non-virtual函数    {        doDraw(color);//调用一个virtual    }    ...private:    virtual void doDraw(ShapeColor color) const = 0;//真正的工作在这里完成};class Rectangle : public Shape {public:    ...private:    virtual void doDraw(ShapeColor color) const;//注意,不须指定缺省参数值    ...};

因为 non-virtual函数绝对不应该被重定义(根据条款36),所以draw函数的 color缺省值,应该永远为Red

6.小结:绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而virtual函数(唯一应该被重定义的东西)是动态绑定。

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

1.复合是类型之间的一种关系,当某种类型的对象内含它种类型对象,便是这种关系。

class Address { ... };class PhoneNumber { ... };class Person {public:    ...private:    std::string name;//合成成分物    Address address;    PhoneNumber voiceNumber;    PhoneNumber faxNumber;};

2.public 继承意味着 is-a 关系;复合意味着 has-a(有一个) 或 is-implemented-in-terms-of (根据某物实现出):当发生于应用域内的对象之间,表现出has-a关系;当发生于实现域内则表现 is-implemented-in-terms-of
以上Person类示范的是 has-a 关系。 很好理解,人有名字、地址、电话号码等。

3.理解is-implemented-in-terms-of (根据某物实现出)
假设要创建一个 Set template,希望制造出一组class用来表现不重复对象组成的sets。 好像有现成的,标准程序库提供的 set template,但它在效率上不符合需求,所以决定用 list template。

template<class T>class Set:public std::list<T>{...}; //将list应用于Set,错误做法!

因为list可以包含重复的元素,而Set不允许,这两个类之间并非is-a关系,又不符合需求,所以需要去改进它:

template<class T>class Set  {public:    bool member(const T& item) const;    void insert(const T& item);    void remove(const T& item);    std::size_t size() const;private:    std::list<T> rep;//将list应用于Set,正确做法。用list来表述Set的数据};template<typename T>bool Set<T>::member(const T& item) const{    return std::find(rep.begin(), rep.end(), item) != rep.end();}template<typename T>void Set<T>::insert(const T& item){    if(!member(item))  rep.push_back(item);}template<typename T>void Set<T>::remove(const T& item){    typename std::list<T>iterator it = std::find(rep.begin(), rep.end(), item);    if( it != rep.end() )  rep.erase(it);}template<typename T>std::size_t Set<T>::size() const{    return rep.size();}

4.小结:复合 的意义与 public继承 完全不同:在应用域,复合意味着 has-a 关系;在 实现域, 复合意味着 is-implemented-in-trems-of 关系。

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

1.private

class Person { ... };class Student: private Person { ... };//private继承void eat(const Person& p);  //任何人都会吃void study(const Student& s);   //只有学生学习Person p;Student s;eat(p);     //没问题,p是人,会吃eat(s);     //错误!难道学生不是人?

当我们调用 eat(s) 时发现,会报错,所以,显然 private继承并不代表 is-a 关系。
因为,派生类对象不会被转换为基类对象;而且派生类从基类继承而来的所有成员,都将成为private的形式。

2.private继承实际上意味着 implemented-in-terms-of,就是根据某物实现出
我们让一个派生类,private形式继承基类,是为了采用基类已经备妥的某些特性,并不是说它俩之间有任何观念上的关系。private继承在设计层面上没有意义,只在软件实现层面上有意义。

3.上一个条款刚指出,“复合”其中一个意义也是:根据某物实现出。
对于这两者,有一个原则:尽可能的使用复合,如下必要时使用private继承
①当 protected 成员 或 virtual函数 相关
②当空间方面的利害关系足以踢翻private继承支柱

4.小结:
<1>Private继承意味 is-implemented-in-terms-of(根据某物实现出)。它通常比复合(composition)的级别低。但是当派生类需要访问protected 基类的成员,或需要重新定义继承而来的virtual函数时,这样的设计是合理的。
<2>和 复合 不同,private继承可以造成empty基类最优化。这对致力于”对象尺寸最小化“的程序库开发者而言,可能很重要。

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

1.使用多重继承,程序有可能从一个以上的base class继承相同的名称(如函数、typedef等),那可能导致较多的歧义机会

class BorrowableItem {  //图书馆允许你借某些东西public:     void checkOut();    //离开时进行检查… }; class ElectronicGadget { private:     bool checkOut() const; //执行自我检测,返回是否测试成功… }; class MP3Player:    //这里多重继承    public BorrowableItem,     public ElectronicGadget { … }; MP3Player mp; mp.checkOut(); // 此处的checkOut是BorrowableItem类的还是ElectronicGadget类的呢?

为了解决这种歧义,首先要明确调用哪个基类的函数,带上那个基类

mp.BorrowableItem::checkOut();

当然,此处也可以尝试明确调用ElectronicGadget::checkOut,但是会报“无法调用private成员”的错误。

2.钻石型多重继承问题:当所继承的多个基类在它们体系中又有共同的基类,会产生钻石型多重继承问题:

class File { … }; class InputFile: public File { … }; class OutputFile: public File { … }; class IOFile: public InputFile, public OutputFile { … };

假设File class有个成员变量fileName,那么IOFile类中有多少 个这个名词的数据呢?
①IOFile应该从每个基类中获取一份成员函数,所以,它应该有两份
②IOFile对象只该有一个文件名称,所以继承而来的函数不应该重复

3.C++ 对上述两种都支持,但缺省的方法是 执行复制,也就是有两份。
如果想让其是第二种方法,就需要将高级的基类(也就是File)设置为virtual base class

class File {  ...  };class InputFile: virtual public File {  ...  };class OutputFile: virtual public File {  ...  };class IOFile: public InputFile, public OutputFile{  ...  };

4.从正确行为的观点看,public继承应该总是 virtual。但是使用virtual继承得付出代价:如所产生的对象体积更大、访问成员变量速度更慢、初始化的规则更复杂且不直观
所以:非必要,不适用 virtual base,平常使用non-virtual继承;如必须使用virtual base class,尽可能避免在内放置数据

5.小结:
<1>多重继承比单一继承复杂。它可能导致歧义,以及对virtual继承的需要。
<2>virtual继承会增加大小、速度、初始化(或者 赋值)复杂度等等成本。如果virtual base class不带任何数据,将是最具实用价值的情况。
<3>多重继承的确有正当用途。其中一个情节涉及“public 继承某个 Interface class” 和 “private继承某个协助实现的class” 的两相组合。

原创粉丝点击