《Effective C++》第三版-6. 继承与面向对象计划(Inheritance and Object- ...

打印 上一主题 下一主题

主题 930|帖子 930|积分 2790

目录

条款32:确定你的public继承塑模出is-a关系(Make sure public inheritance models “is-a”)

本条款内容比较简单,略写
public继承的含义

public继承意味着“is-a”的关系
假如一个类D(derived)public继承自类B(base):

  • 每个类型D的对象也是一个类型B的对象,反之则否则
  • B比D表示了一个更为一样平常的概念,而D比B表现了一个更为特别的概念
  • 任何可以利用类型B的地方,也能利用类型D;但可以利用类型D的地方却不可以利用类型B
  • D是B,B不是D
  1. class Person { ... };
  2. class Student: public Person { ... };
  3. void eat(const Person& p);  //任何人都可以吃
  4. void study(const Student& s);  //只有学生才到校学习
  5. Person p;  //p是人
  6. Student s;  //s是学生
  7. eat(p);  //没问题,p是人
  8. eat(s);  //没问题,s是学生,学生也是人
  9. study(s);        
  10. study(p);  //错误!p不是个学生
复制代码
计划良好的继承关系

考虑鸟和企鹅的关系:
  1. class Bird {
  2. public:
  3.         virtual void fly();  //鸟可以飞   
  4.         ...
  5. };
  6. class Penguin: public Bird {  //企鹅是一种鸟,但不会飞,故直接继承有问题
  7.         ...
  8. };
复制代码
编译时会报错的计划:
  1. class Bird {
  2.         ...  //未声明fly函数
  3. };
  4. class FlyingBird: public Bird {
  5. public:
  6.     virtual void fly();  //会飞的鸟类
  7. };
  8. class Penguin :public Bird {
  9.         ...  //企鹅不会飞,未声明fly函数
  10. };
  11. Penguin p;
  12. p.fly();  //错误!
复制代码
运行时会报错的计划:
  1. class Bird {
  2. public:
  3.     virtual void fly();
  4. };
  5. void error(const std::string& msg);
  6. class Penguin :public Bird {
  7. public:
  8.     virtual void fly() {
  9.         error("Attempt to make a penguin fly!");
  10.     }
  11. };
复制代码
应优先选择在编译期间会报错的计划,而非在运行期间才报错的计划
is-a的例外

考虑矩形和正方形的关系:
classDiagram        graph LR        note "继承关系合理吗?"        Rectangle> x;  //局部变量赋值}[/code]当全局和局部存在同名变量时,在局部作用域中,优先利用局部变量,全局变量会被隐藏
flowchart LR    subgraph Global scope    x    subgraph SomeFunc's scope    b[x]    end    endC++的名称遮掩规则(name-hiding rules)只遮掩名称,无论名称否是同一类型。
继承的隐藏

在继承中唯一关心的是成员变量和成员函数的名称,其类型没有影响:
  1. class Rectangle {
  2. public:
  3.     virtual void setHeight(int newHeight);  //高
  4.     virtual void setWidth(int newWidth);  //宽
  5.     virtual void height()const;  //返回高
  6.     virtual void width()const;  //返回宽
  7.     ...
  8. };
  9. void makeBigger(Rectangle& r) //这个函数用来增加r的面积
  10. {
  11.     int oldHeight = r.height();  //取得旧高度
  12.     r.setWidth(r.width() + 10);  //设置新宽度
  13.     assert(r.height() == oldHeight);  //永远为真,因为高度未改变
  14. }
  15. class Square :public Rectangle { ... };
  16. Square s;  //正方形类
  17. ...
  18. assert(s.width() == s.height());  //永远为真,因为正方形的宽和高相同
  19. makeBigger(s);  //由于继承,可以增加正方形的面积
  20. assert(s.width() == s.height());  //对所有正方形来说,理应还是为真
复制代码
flowchart LR    subgraph 基类的作用域    a[x(成员变量)\nmf1(1个函数)\nmf2(1个函数)\nmf3(1个函数)]    subgraph 派生类的作用域    e[mf1(1个函数)\nmf4(1个函数)]    end    end在派生类的fm4()函数中调用了fm2函数,对于fm2函数的查找顺序如下:

  • 在fm4函数中查找,若没有进行下一步
  • 在派生类类中查找,若没有进行下一步
  • 在基类Base中查找,若没有进行下一步
  • 在Base所在的namespace中查找,若没有进行下一步
  • 在全局作用域查找
在派生类中重载mf3:
  1. int x;  //全局变量
  2. void someFunc()
  3. {
  4.     double x;  //局部变量
  5.     std::cin >> x;  //局部变量赋值
  6. }
复制代码
flowchart LR    subgraph 基类的作用域    a[x(成员变量)\nmf1(2个函数)\nmf2(1个函数)\nmf3(2个函数)]    subgraph 派生类的作用域    e[mf1(1个函数)\nmf3(1个函数)\nmf4(1个函数)]    end    end继承重载的函数


  • 通过using声明式
  1. class Base
  2. {
  3. private:
  4.     int x;
  5. public:
  6.     virtual void mf1() = 0;
  7.     virtual void mf2();
  8.     void mf3();
  9.     ...
  10. };
  11. class Derived :public Base
  12. {
  13. public:
  14.     virtual void mf1();  //重写(覆盖)
  15.     void mf4();
  16.     ...
  17. };
  18. //假设派生类中的mf4定义如下
  19. void Derived::mf4()
  20. void Derived::mf4()
  21. {
  22.         ...
  23.         mf2();
  24.         ...
  25. }
复制代码

  • 利用转交函数(forwarding function)
  1. class Base
  2. {
  3. private:
  4.     int x;
  5. public:
  6.     virtual void mf1() = 0;
  7.     virtual void mf1(int);
  8.     virtual void mf2();
  9.     void mf3();
  10.     void mf3(double);
  11.     ...
  12. };
  13. class Derived :public Base
  14. {
  15. public:
  16.     virtual void mf1();  //基类中的所有mf1()都被隐藏
  17.     void mf3();  //基类中的所有fm3()都被隐藏
  18.     void mf4();
  19.     ...
  20. };
  21. //调用如下
  22. Derived d;
  23. int x;
  24. ...
  25. d.mf1();  //正确
  26. d.mf1(x);  //错误!被隐藏了
  27. d.mf2();  //正确
  28. d.mf3();  //正确
  29. d.mf3(x);  //错误!被隐藏了
复制代码
Tips:

  • 派生类内的名称会覆盖基类内的名称,在public继承中不应如此
  • 可利用using声明式或转交函数来继承被覆盖的名称
条款34:区分接口继承和实现继承(Differentiate between inheritance of interface and inheritance of implementation)

public继承由两部分组成:函数接口(function interface)继承和函数实现(function implementation)继承
对于基类的成员函数可以大抵做下面三种方式的处置处罚:

  • 纯虚函数:只继承成员函数的接口(也就是声明),让派生类去实现
  • 虚函数:同时继承函数的接口和实现,也能覆写(override)所继承的实现
  • 普通函数:同时继承函数的接口和实现,且不允许覆写
以表示几何图形的类为例:
  1. class Base
  2. {
  3. private:
  4.     int x;
  5. public:
  6.     virtual void mf1() = 0;
  7.     virtual void mf1(int);
  8.     virtual void mf2();
  9.     void mf3();
  10.     void mf3(double);
  11.     ...
  12. };
  13. class Derived :public Base
  14. {
  15. public:
  16.     using Base::mf1;  //Base所有版本的mf1函数在派生类作用域都可见
  17.     using Base::mf3;  //Base所有版本的mf3函数在派生类作用域都可见
  18.     virtual void mf1();  //重写mf1()函数
  19.     void mf3();  //隐藏了mf1(),但是mf3(double)没有隐藏
  20.     void mf4();
  21.     ...
  22. };
  23. //调用如下
  24. Derived d;
  25. int x;
  26. d.mf1();  //正确,调用Derived::mf1
  27. d.mf1(x);  //正确,调用Base::mf1
  28. d.mf2();  //正确,调用Derived::mf2
  29. d.mf3();  //正确,调用Derived::mf3
  30. d.mf3(x);  //正确,调用Base::mf3
复制代码
成员函数的接口总是会被继承

  • public继承Shape类意味着对其正当的事一定对派生类正当
纯虚函数

纯虚函数具有两个突出特性:

  • 继承它们的具象类必须重新声明
  • 在抽象类中无界说
声明一个纯虚函数的目的是为了让派生类只继承函数接口

  • Shape类必要可以或许画出,~但不同形状的画法不同,故只留出通用接口而不提供详细实现
  1. class Base {
  2. public:
  3.         virtual void mf1() = 0;
  4.         virtual void mf1(int);
  5.         ...
  6. };
  7. class Derived: private Base {
  8. public:
  9.         virtual void mf1()  //转交函数
  10.         { Base::mf1(); }  //暗自成为inline
  11.         ...
  12. };
  13. ...
  14. //调用如下
  15. Derived d;
  16. int x;
  17. d.mf1();  //正确,调用Derived::mf1
  18. d.mf1(x);  //错误! Base::mf1()被覆盖了
复制代码
虚函数

声明简朴的impure virtual 函数的目的,是让派生类继承该函数的接口和缺省实现

  • Shape类继承体系中必须支持遇到错误可调用的函数,但处置处罚错误的方式可由派生类界说,也可直接利用Shape类提供的缺省版本
若派生类忘记覆写这类函数,则会直接调用基类的实现版本,这可能会导致严峻问题,解决方案如下:

  • 将默认实现分离成单独函数

    • 可能因过度类似的函数名称而污染类定名空间

  • 利用纯虚函数提供默认实现
  1. class Shape {
  2. public:
  3.         virtual void draw() const = 0;
  4.         virtual void error(const std::string& msg);
  5.         int objectID() const;
  6.         ...
  7. };
  8. class Rectangle: public Shape { ... };
  9. class Ellipse: public Shape { ... };
复制代码
  1. Shape *ps = new Shape;  //错误!Shape是抽象的
  2. Shape *ps1 = new Rectangle;  // 没问题
  3. ps1->draw();  // 调用Rectangle::draw
  4. Shape *ps2 = new Ellipse;  // 没问题
  5. ps2->draw();  // 调用Ellipse::draw
  6. ps1->Shape::draw();  // 调用Shape::draw
  7. ps2->Shape::draw();  // 调用Shape::draw
复制代码
普通函数

声明non-virtual函数的目的是令派生类继承函数的接口以及一份强制性实现

  • 意味是它并不计划在派生类中有不同的行为
  • 其不变形(invariant)凌驾特异性(specialization)
Tips:

  • 接口继承和实现继承不同。在public继承下,派生类总是继承基类的接口
  • 纯虚函数只详细指定接口继承
  • 简朴的虚函数详细指定接口继承以及缺省实现继承
  • non-virtual函数详细指定接口继承以及强制性实现继承.
条款35:考虑virtual函数以外的其他选择(Consider alternatives to virtual functions)

以游戏脚色为例:
  1. //将默认实现分离成单独函数
  2. class Airplane {
  3. public:
  4.     virtual void fly(const Airport& destination) = 0;
  5.     ...
  6. protected:
  7.     void defaultFly(const Airport& destination);
  8. };
  9. void Airplane::defaultFly(const Airport& destination) {
  10.         //飞机飞往指定的目的地(默认行为)
  11. }
  12. class ModelA :public Airplane {
  13. public:
  14.     virtual void fly(const Airport& destination) {
  15.         defaultFly(destination);
  16.     }
  17.     ...
  18. };
  19. //ModelB同ModelA
  20. class ModelC :public Airplane {
  21. public:
  22.     virtual void fly(const Airport& destination);
  23. };
  24. void ModelC::fly(const Airport& destination) {
  25.         //将C型飞机飞至指定的目的地
  26. }
复制代码
藉由Non-Virtual Interface手法实现Template Method模式

考虑全部虚函数应险些总是private的主张:
  1. //利用纯虚函数提供默认实现
  2. class Airplane {
  3. public:
  4.     virtual void fly(const Airport& destination) = 0;
  5.     ...
  6. };
  7. void Airplane::fly(const Airport& destination) {
  8.         //缺省(默认)行为,将飞机飞至指定的目的地
  9. }
  10. class ModelA :public Airplane {
  11. public:
  12.     virtual void fly(const Airport& destination) {
  13.         Airplane::fly(destination);
  14.     }
  15.     ...
  16. };
  17. //ModelB同ModelA
  18. class ModelC :public Airplane {
  19. public:
  20.     virtual void fly(const Airport& destination);
  21. };
  22. void ModelC::fly(const Airport& destination) {
  23.         //将C型飞机飞到指定目的地
  24. }
复制代码
NVI(non-virtual interface)手法

  • 通过public non-virtual成员函数间接调用private virtual函数
  • Template Method计划模式(和C++ templates无关)的独特表现情势
  • non-virtual函数称为virtual函数的外覆器(wrapper)

    • 外覆器使得在non-virtual函数中能做准备和善后工作:

      • 事前工作:锁定互斥器、制造运转日志记录项(log entry)、验证类约束条件、验证函数先决条件等等
      • 事后工作:互斥器解锁、验证函数的事后条件、再次验证类约束条件等等


  • 会在派生类中重新界说private虚函数——重新界说它们不调用的函数

    • 派生类重新界说虚函数可控制怎样实现功能
    • 基类保留函数何时被调用的权利

  • 没有规定虚函数必须是private

    • 某些类继承体系要求派生类在虚函数的实现中必须调用其基类的对应部分,则虚函数必须是protected而非private,否则此类调用不正当
    • 有时虚函数必须是public的(如多态基类中的析构函数),但是此时没法利用NVI手法

藉由Function Pointers实现Strategy模式

NVI没有免去界说虚函数
考虑每个人物的构造函数接受一个指向康健盘算函数的指针:
  1. class GameCharacter {
  2. public:
  3.     virtual int healthValue() const;  //返回人物的健康指数,派生类可覆写
  4.     ...
  5. };
复制代码
这个做法是常见的Strategy计划模式的简单应用,其相对于继承体系内的虚函数具有一些弹性:

  • 同一个人物类型之间可以有不同的康健盘算函数
  • 某已知人物的康健函数可在运行期变更
  1. class GameCharacter {
  2. public:
  3.     int healthValue() const {   //派生类不应该重新定义它
  4.         ...  //事前工作,详下              
  5.         int retVal = doHealthValue();  //真正的工作
  6.         ...  //事后工作,详下               
  7.         return retVal;
  8.     }
  9. private:
  10.     virtual int doHealthValue() const  //派生类可以重新定义它
  11.     {
  12.             ...  //缺省,计算健康指数
  13.     }
  14. };
复制代码
上述方法的应用范围:

  • 当全局函数可以根据类的public接口来取得信息并且加以盘算,则没有问题
  • 若盘算必要访问到类的non-public信息,则全局函数不可利用

    • 唯一的解决方法:弱化类的封装。例如:

      • 将这个全局函数界说为类的friend
      • 为其某一部分提供public访问函数


藉由Function Pointers实现Strategy模式

若利用tr1::funciton对象替换函数指针用,则约束会大大减少:
这些对象可以持有任何可调用实体(callable entity,即函数指针、函数对象、成员函数指针),只要其签名式和需求兼容
  1. class GameCharacter;        // 前置声明*forward declaration)
  2. int defaultHealthCala(const GameCharacter& gc);  //计算健康指数的缺省算法
  3. class GameCharacter {
  4. public:
  5.   typedef int(*HealthCalcFunc)(const GameCharacter& gc);  //函数指针别名
  6.   explicit GameCharacter(HealthCalaFunc hcf = defaultHealthCalc)
  7.     : healthFunc(hcf)
  8.   {}   
  9.   int healthValue()
  10.   { return healthFunc(*this); }  //通过函数指针调用函数
  11.   ...
  12. private:
  13.         HealthCalcFunc healthFunc;  //函数指针
  14. };
复制代码
上述代码中HealthCalFunc具有如下性质:

  • 是对一个实例化的tr1::function的typedef

    • 其行为像一个泛化函数指针类型

  • tr1::function实例的目标签名(target signature)表明:

    • 函数接受一个指向const GameCharacter的引用
    • 返回一个int类型

  • 该tr1::function类型的对象可以持有任何同这个目标签名相兼容的可调用物,兼容的含义为:

    • 可调用物的参数要么是const GameCharacter&,要么可隐式转换成这个类型
    • 其返回值要么是int,要么可以隐式转换成int

上述计划和上一个GameCharacter持有一个函数指针的计划根本相同,唯一的不同是GameCharacter现在持有一个tr1::function对象——一个指向函数的泛化指针。从而使客户现在在指定康健盘算函数上有了更大的弹性:
  1. //同一个人物类型之间可以有不同的健康计算函数
  2. class EvilBadGuy: public GameCharacter {
  3. public:
  4.         explicit EvilBadGuy(HealthCalaFunc hcf = defaultHealthCalc)
  5.           : GameCharacter(hcf)
  6.   { ... }
  7.         ...
  8. };
  9. int loseHealthQuickly(const GameCharacter&);  //健康指数计算函数1
  10. int loseHealthSlowly(const GameCharacter&);  //健康指数计算函数2
  11. EvilBadGuy ebg1(loseHealthQuickly);  //相同类型的人物搭配
  12. EvilBadGuy ebg2(loseHealthSlowly);  //不同的健康计算方式
复制代码
古典的Strategy模式

古典的Strategy做法会将盘算康健的函数计划为一个分离的继承体系中的virtual成员函数:
---title: 分离继承体系中virtual成员函数---classDiagram    GameCharacter mf();  //调用B::mfpD->mf();  //调用D::mf[/code]造成差异的缘故原由:

  • non-virtual函数都是静态绑定的(statically bound)

    • pB被声明为指向B的指针,通过pB触发的non-virtual函数都会是界说在类B上的函数,纵然pB指向的是B的派生类对象

  • virtual函数是动态绑定的(dynamically bound)

    • 若mf是一个虚函数,无论通过pB或者pD对mf进行调用都会触发D::mf,因为pB或者pD真正指向的是类型D的对象。

  • 引用也会有相同的问题
理论层面的来由:

  • 条款32解释了pulibc继承意味着is-a(是一种)的关系

    • 适用于B对象的每一件事都适用于D对象,因为每一个D对象都是一个B对象

      • 若D覆写了mf,则is-a关系不成了,那么D不应该以public情势继承B


  • 条款34描述了为什么在一个类中声明一个non-virtual函数,则其不变性(invariant)凌驾于特异性(specialization)

    • B的派生类一定会继承mf的接口和实现,因为mf是B的non-virtual函数

      • 若D必要以public继承B且覆写了mf,则不变性就没有凌驾于特异性,那么mf应该声明为虚函数(如析构函数)


  • 综上所述,禁止重新界说一个继承而来的non-virtual函数
Tips:

  • 绝对不要重新界说继承而来的non-virtual函数
条款37:绝不重新界说继承而来的缺省参数值(Never redefine a function’s inherited default parameter value)

虚函数和缺省参数值

虚函数是动态绑定,而缺省参数值是静态绑定的

  • 静态绑定,又名前期绑定(early binding)
  • 动态绑定,又名后期绑定(late binding)
  1. class GameCharacter;  //如前
  2. int defaultHealthCala(const GameCharacter& gc);  //如前
  3. class GameCharacter {
  4. public:
  5.     //只是将函数指针改为了function模板,其接受一个const GameCharacter&参数,并返回int
  6.     typedef std::tr1::function<int(const GameCharacter&)> HealthCalcFunc;       
  7.     explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
  8.             : healthFunc(hcf)
  9.     {}
  10.     int healthValue() const
  11.     { return healthFunc(*this); }
  12.     ...
  13. private:
  14.     HealthCalcFunc healthFunc;
  15. };
复制代码
---title: 类继承图---classDiagram    Shape draw(Shape::Red);  //调用Rectangle::draw(Shape::Red)        pr->draw();  //调用Rectangle::draw(Shape::Red) ![/code]

  • 虚函数是动态绑定的,故哪个函数被调用是由发出调用的对象的动态类型来决定的
  • 虑函数带缺省参数值的虚函数时,默认参数是静态绑定的,则可能函数界说在派生类中却利用了基类中的默认参数
接纳上述运行方式的缘故原由和效率相关:

  • 假如一个默认参数是动态绑定的,编译器必须在运行时为虚函数决定适当的参数缺省值

    • 这比起在编译期决定这些参数的机制更慢且更复杂

避免重复的缺省参数值

同时提供缺省参数值给基类和派生类会导致代码重复且具有相依性(with dependencies):
  1. short calcHealth(const GameCharacter&);  //计算健康指数的函数
  2. struct HealthCalculator {  //计算健康指数的函数对象
  3.     int operator()(const GameCharacter&)const {}
  4. };
  5. class GameLevel {
  6. public:
  7.     float health(const GameCharacter&) const;  //成员函数,用以计算健康
  8.     ...
  9. };
  10. //人物1,使用函数来计算健康指数
  11. EvilBadGuy ebg1(calcHealth);
  12. //人物2,使用函数对象来计算健康指数
  13. EyeCandyCharacter ecc1(HealthCalculator());
  14. //人物2,使用GameLevel类的成员函数来计算健康指数
  15. GameLevel currentLevel;
  16. ...
  17. EvilBadGuy ebg2(std::tr1::bind(&GameLevel::health, currentLevel, _1));
复制代码
可利用NVI手法解决上述问题:

  • 用基类中的public非虚函数调用一个private虚函数,private虚函数可以在派生类中重新被界说
  • 用非虚函数指定默认参数,而用虚函数来做实际的工作
  1. class GameCharacter;  //前置声明
  2. class HealthCalcFunc {  //计算健康指数的类
  3. public:
  4.     virtual int calc(const GameCharacter& gc)const {}
  5. };
  6. HealthCalcFunc defaultHealthCalc;
  7. class GameCharacter {
  8. public:
  9.                 //具有弹性,它为现存的健康计算算法的调整提供了可能性
  10.     explicit GameCharacter(HealthCalcFunc* hcf = &defaultHealthCalc)  
  11.             : pHealthCalc(hcf)
  12.           {}
  13.     int healthValue() const
  14.     { return pHealthCalc->calc(*this); }
  15. private:
  16.     HealthCalcFunc* pHealthCalc;
  17. };
复制代码
Tips:

  • 绝对不要重新界说一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而唯一应该覆写的虚函数却是动态绑定。
条款38:通核复合塑模出has-a或“根据某物实现出”(Model “has-a” or “is-implemented-in-terms-of” through composition)

复合的含义

复合(composition)是类型之间的一种关系:

  • 这种关系见于一种类型的对象包含别的一种类型的对象时
  • 又称分层(layering)、包含( containment)、聚合 (aggregation)、和植入(embedding)
  1. class B {
  2. public:
  3.         void mf();
  4.         ...
  5. };
  6. class D: public B { ... }
复制代码
复合根据软件中两种不同的范畴(domain)有有两种含义:

  • has-a:对象是应用域(application domain)的一部分

    • 对应天下上的真实存在的东西,如人、汽车、视频画面等

  • is-implemented-in-terms-of(根据某物实现出):对象是实现域(implementation domain)的一部分

    • 对应纯实现层面的人工制品,如像缓存区(buffers)、互斥器(mutexs)、查找树(search trees)等

区分不同的关系

区分is-a和has-a很简单;区分is-a和is-implemented-in-terms-of比较困难,过程见下:

  • 假设必要一个类模板表示由不重复对象组成的set
  • 首先考虑利用尺度库的set模板以实现复用(reuse)
  • 不幸的是,set的实现对于其中的每个元素都会引入三个指针的开销

    • set通常作为一个均衡查找树(balanced search trees)来实现,以保证查找、插入、删除元素时具有对数时间(logarithmic-time)效率
    • 当速度比空间重要时,此计划合理
    • 当空间比速度重要时,此计划不合理,必要自己实现模板

  • 考虑利用C++尺度库中的list template以在底层利用linked lists,从而实现sets

    • 此方法会让Set template继承std::list,即Set继承list
    • 但是,list对象可以包含重复元素而Set不可以,这违反了is-a关系,不应利用public继承

  1. D x;  //x是一个类型为D的对象
  2. B *pB = &x;                              
  3. pB->mf();  //通过B*调用mf                     
  4. D *pD = &x;                              
  5. pD->mf();  //通过D*调用mf   
复制代码

  • 精确的方式是Set对象可以被implemented in terms of为一个list对象

    • Set成员函数的实现可以依靠list已经提供的功能和尺度库的其他部分,以简化实现

  1. class D: public B {
  2. public:
  3.         void mf();         
  4.         ...                     
  5. };
  6. pB->mf();  //调用B::mf
  7. pD->mf();  //调用D::mf
复制代码

  • 以上过程展示了is-implemented-in-terms-of而非is-a的关系
Tips:

  • 复合的意义和public继承完全不同
  • 在应用域,复合意为has-a(有一个);在实现域,复合意味着is-implemented-in-terms-of(根据某物实现出)
条款39:明智而审慎地利用private继承(Use private inheritance judiciously)

private继承

考虑private继承的例子:
  1. //一个用以描述几何形状的类
  2. class Shape {
  3. public:
  4.         enum ShapeColor { Red, Green, Blue };
  5.         //所有形状都必须提供一个函数,用来绘出自己
  6.         virtual void draw(ShapeColor color = Red) const = 0;
  7.         ...
  8. };
  9. class Rectangle: public Shape {
  10. public:
  11.         //赋予不同的缺省参数值,不好!
  12.         virtual void draw(ShapeColor color = Green) const;
  13.         ...
  14. };
  15. class Circle: public Shape {
  16. public:
  17.         virtual void draw(ShapeColor color) const;
  18.         //注意,以上这么写则当客户以对象调用此函数,一定要指定参数值
  19.         //因为静态绑定下这个函数并不从其base继承缺省参数值
  20.         //但若以指针(或引用)调用此函数,可以不指定参数值
  21.         //因为动态绑定下这个函数会从其基类继承缺省参数值
  22.         ...
  23. };
复制代码
private继承的规则:

  • 类之间为private继承时,编译器不会将派生类对象(Student)转换成为基类对象(Person)

    • 因此对象s调用eat会失败

  • 由基类private继承而来的全部成员(包括protected和public成员),在派生类中都会变成private属性
private继承的意义:

  • private继承意味着is-implemented-in-terms-of(根据某物实现出)

    • 假如你让类D private继承自类B,你的用意是因为你想利用类B中的一些让你感爱好的性质,而不是因为在类型B和类型D之前有任何概念上的关系

  • private继承纯粹只是一种实现技能

    • 因此从private基类中继承而来的任何东西在派生类中都变为了private

      • 全部东西只被继承了的实现部分


  • private继承意味着只有实现部分被继承,而接口应略去

    • 假如类D是private继承自类B,就意味着D对象的实现依靠于类B对象,没有其他含义

  • private继承在软件实现层面才故意义,在软件计划层面没故意义
private继承和复合

只管利用复合(composition),必要时才利用private继承

  • 要访问基类的protected成员或要重界说基类的虚函数时
  • 必要寻求极致的空间时
考虑Widget类,必要了解:

  • 成员函数的利用频率
  • 调用比例怎样变革

    • 带有多个执行阶段(execution phases)的程序,可能在不同阶段拥有不同的行为轮廓(behavioral profiles)

      • 例如编译器在分析(parsing)阶段所用的函数不同于在最优化(optimization)和代码生成(code generation)阶段所用的函数


因此必要设定定时器以提醒统计调用次数的时机:
  1. Shape *ps;  //静态类型为Shape*,无动态类型,未指向任何对象                        
  2. Shape *pc = new Circle;  //静态类型为Shape*,动态类型为Circle*            
  3. Shape *pr = new Rectangle;  //静态类型为Shape*,动态类型为Rectangle*
  4. ps = pc;  //ps的动态类型变为Circle*
  5. ps = pr;  //ps的动态类型变为Rectangle*  
  6. pc->draw(Shape::Red);  //调用Circle::draw(Shape::Red)   
  7. pr->draw(Shape::Red);  //调用Rectangle::draw(Shape::Red)        
  8. pr->draw();  //调用Rectangle::draw(Shape::Red) !
复制代码
Widget必须继承自Timer以重界说Timer内的虚函数:

  • Widget不是一个Timer,故public继承不符合
  • 可利用private继承,但非必要
  • 可利用复合,且应选择复合

    • 防止Widget的派生类重写onTick函数

      • Widget的派生类无法访问作为private成员的WidgetTimer类,则无法重写onTick

    • 可以将Widget的编译依存性降至最低

      • 利用private继承必须#include "Timer.h”
      • 利用复合则可以无需#include任何与Timer有关的东西

        • 若将WidgetTimer界说在Widget之外,然后在Widget内界说一个WidgetTimer的指针,此时Widget可以只带着WidgetTimer的声明式



  1. class Shape {
  2. public:
  3.         enum ShapeColor { Red, Green, Blue };
  4.         virtual void draw(ShapeColor color = Red) const = 0;
  5.         ...
  6. };
  7. class Rectangle: public Shape {
  8. public:
  9.         virtual void draw(ShapeColor color = Red) const;
  10.         ...
  11. };
复制代码
classDiagram    Widget *-- WidgetTimer    Timer sizeof(int),一个Empty数据成员也会占用空间对于大多数编译器来说,sizeof(Empty)为1

  • 因为C++法则处置处罚大小为0的独立对象时会默认向空对象中插入一个char

    • 不能被应用在派生类对象的基类部分中,因为它们不是独立的

齐位需求(alignment,见条款50)可能导致编译器向HoldsASnInt这样的类中添加衬垫(padding)

  • HoldsAnInt对象可能不只获得一个char的大小,实际上会被增大到可容纳第二个int
</ul>若继承Empty则其非独立,其大小可以为0:
由于EBO(empty base optimization,空缺基类最优化),险些可以确定sizeof(HoldsAnInt)==sizeof(int)。EBO一样平常在单一继承而非多重继承下才可行
  1. class Shape {
  2. public:
  3.         enum ShapeColor { Red, Green, Blue };
  4.         void draw(ShapeColor color = Red) const   
  5.         {                                                                 
  6.                 doDraw(color);                           
  7.         }                                                  
  8.         ...                                                
  9. private:                                       
  10.         virtual void doDraw(ShapeColor color) const = 0;
  11. };
  12. class Rectangle: public Shape {
  13. public:
  14.         ...
  15. private:
  16.         virtual void doDraw(ShapeColor color) const;
  17.         ...                                                               
  18. };         
复制代码
事实上,空类不是真的空:

  • 固然它们永远不会拥有非静态数据成员,它们通常会包含typedefs、enums、静态数据成员或non-virtual非虚函数

    • STL就包含很多技能用途的空类,其中包含有效的成员(通常为typedefs)的空类

      • 包括基类unary_function和binary_function,用户界说的函数对象会继承这些类
      • EBO使得这些继承很少会增加派生类的大小


Tips:

  • private继承意为is-implemented-in-terms-of(根据某物实现出),通常优先利用而非private继承,但是当派生类必要访问protected基类成员或必要重新界说虚函数时可以利用
  • 和复合不同,private继承可以使空基类最优化,有利于对象尺寸最小化
条款40:明智而审慎地利用多重继承(Use multiple inheritance judiciously)

本条款主要讨论两种观点:

  • 单一继承(single inheritance,SI)是好的,多重继承(multiple inheritance,MI)会更好
  • 单一继承是好的,但多重继承不值得拥有
接口调用的歧义性

以下例子具有歧义(ambiguity):
  1. class Address { ... };  // 住址
  2. class PhoneNumber { ... };
  3. class Person {
  4.         public:
  5.         ...
  6. private:
  7.         std::string name;  //合成成分物(composed object)
  8.         Address address;  //同上
  9.         PhoneNumber voiceNumber;         //同上
  10.         PhoneNumber faxNumber;  //同上
  11. };   
复制代码
对checkout的调用是歧义的:

  • 纵然只有两个函数中的一个是可取用的(checkout在BorrowableItem中是public的而在ElectronicGadget中是private的),歧义仍旧存在
  • 这与C++用来分析(resolving)重载函数调用的规则相符:

    • 在看到是否有个函数可取用之前,C++首先识别出对此调用而言的最佳匹配函数
    • 找到最佳匹配函数之后才会查抄函数的可取用性
    • 本例的两个checkOut具有相同的匹配程度(所以造成歧义),没有最佳匹配,因此ElectronicGadget::checkOut的可取用性未被编译器审查

  • 为了解决这个歧义,必须指定调用哪个基类的函数
  1. template<typename T>   //把list用于Set。错误做法!               
  2. class Set: public std::list<T> { ... };     
复制代码
菱形继承与虚(virtual)继承

多重继承意味着继承多个基类,但是这些基类在继承体系往往没有更高条理的基类,否则会导致致命的”钻石型多重继承”:
  1. template<class T>  //把list用于Set。正确做法                        
  2. class Set {                                       
  3. public:                                            
  4.         bool member(const T& item) const;
  5.         void insert(const T& item);            
  6.         void remove(const T& item);         
  7.         std::size_t size() const;                  
  8. private:                                          
  9.         std::list<T> rep;  //用以表述Set的数据            
  10. };   
  11. //依赖list已经提供的功能和标准库的其他部分实现Set的成员函数
  12. template<typename T>
  13. bool Set<T>::member(const T& item) const
  14. {
  15.         return std::find(rep.begin(), rep.end(), item) != rep.end();
  16. }
  17. template<typename T>
  18. void Set<T>::insert(const T& item)
  19. {
  20.         if (!member(item)) rep.push_back(item);
  21. }
  22. template<typename T>
  23. void Set<T>::remove(const T& item)
  24. {
  25.         typename std::list<T>::iterator it = //见条款42对
  26.         std::find(rep.begin(), rep.end(), item); //typename的讨论
  27.         if (it != rep.end()) rep.erase(it);
  28. }
  29. template<typename T>
  30. std::size_t Set<T>::size() const
  31. {
  32.         return rep.size();
  33. }
复制代码
---title: 菱形继承---classDiagram    File
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

汕尾海湾

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表