继承与面向对象设计

继承与面向对象设计

确定你的public 继承,表达的是is-a 关系

举例

    • 鸟会飞

      • 大部分鸟会飞
      • 一部分鸟,比如,鸵鸟,不会飞
    • 分成两类

      • 会飞
      • 不会飞
      • 这样有一个问题,可能有些程序来说,不需要区分,两种会不会飞的鸟,此时,不区分会飞的鸟和不会飞的鸟,不失为一个完美而有效的设计
    • 所有的鸟都会飞,企鹅是鸟,但是企鹅不会飞

      • 所有的鸟都有fly 的虚函数,但企鹅将其时限为运行时错误

      • 企鹅会飞,但尝试那么做的是一种错误

      • 企鹅不会飞这一限制,可以由编译器强制实施

      • 企鹅尝试飞行,是一种错误,只有运行期才能检测出来

        • 也可以通过不给企鹅类定义fly 函数,这样如果有人调用它,就会编译器报错。

public 继承主张,能够施行于base class 对象身上的每件事情,都可以施行于派生类对象身上

  • 代码通过编译并不代表就可以正确运作

避免遮掩继承而来的名称

作用域有关的问题

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(); //mf1 有重载的两个类型,我们只定义了一个
    void mf3(); // mf3 有重载的两个类型,我们只定义了一个
    void mf4();// 新函数
    //...
};
void SomeFunc()
{
    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
}

即使,base classes 和 derived classes 内的函数有不同的参数类型,不论函数是virtual 或 non-virtual。这和本条款一开始展示的道理相同,当时函数someFunc 内的double x 掩盖了global 作用域内的int x,如今Derived 内的函数mf3 掩盖了一个名为mf3 但类型不同的base 函数。
这些行为背后的基本原由:防止你的程序或应用框架内建立新的derived class 时附带从疏远的base classes 继承重载函数。通常我们会想继承重载函数。
实际上如果你正使用public 继承而又不继承那些重载函数,就是违反base 和 派生类之间的is-a 关系。我们总是想要重写c++ 对“继承而来的名称”的缺省掩盖行为。
可以使用using 声明式达成目标:

class Base {
private:
    int x;
public:
    virtual void mf1() = 0;
    virtual void mf1(int) { cout << "Base::mf1(int)" << endl; }
    virtual void mf2() { cout << "Base::mf2()" << endl; }
    void mf3() { cout << "Base::mf3()" << endl; }
    void mf3(double) { cout << "Base::mf3(double)" << endl; }
    //..
};
class Derived : public Base {
public:
    using Base::mf1;
    using Base::mf3;
    virtual void mf1() { cout << "Derived::mf1()" << endl; }
    void mf3() { cout << "Derived::mf3()" << endl; }
    void mf4() { cout << "Derived::mf4()" << endl; }
    //...
};
void SomeFunc()
{
    Derived d;
    int x = 0;
    d.mf1();    // Derived::mf1
    d.mf1(x);   // Base::mf1
    d.mf2();    // 没问题,调用Base::mf2
    d.mf3();    // 没问题,调用Derived::mf3
    d.mf3(x);   // Base::mf3
}

在这里插入图片描述

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

如果不想继承base 类的所有函数:

class Base {
private:
    int x;
public:
    virtual void mf1() = 0;
    virtual void mf1(int) { cout << "Base::mf1(int)" << endl; }
    //..
};
class Derived : private Base {
public:
    virtual void mf1() { Base::mf1(); }
    //...
};
注意
  • 派生类内的名称会掩盖基类内的名称,在public 继承下从来没有人希望如此
  • 为了让被掩盖的名称再见天日,可使用using 声明式或转换函数

区分接口继承和实现继承

只继承成员函数的接口

  • 纯虚函数

    • 可以为纯虚函数提供一份实现代码,但调用它的唯一途径就是“调用时明确指出其class 名称“
    • 提供默认,的同时,要求,子类自己选择,如果需要默认,则手动声明

同时继承函数的接口和实现

  • 允许重写函数

    • 这也是一种,允许默认实现的办法,但,如果既不想每个人都调用默认实现,又需要默认实现,就使用上面的那种,为纯虚函数提供默认实现的方法
  • 非纯虚函数

同时继承函数的接口和实现

  • 非虚函数
  • 不允许重写函数

性能

  • 2-8法则, 80% 时间,花在20%代码上,先解决这20%的代码的效率再说吧

考虑virtual 以外的其他选择

class GameCharacter
{
public:
    int healthValue()const              // 派生类,不重新定义它
    {                                   // 
        //...                           // 做一些事前工作
        int retVal = doHealthValue();   // 做真正的工作
        //...                           // 做一些事后工作
        return retVal;
    }
private:
    virtual int doHealthValue() const;// 派生类可以重新定义它
    {
        //...                           // 缺省算法,计算健康指数
    }
};

这一基础设计,就是“令客户通过public non-virtual 成员函数间接调用private virtual 函数,称为non-virtual interface 手法。它是所谓Template Method 设计模式的一个独特表现方式。我把这个non-virtual 函数称为virtual 函数的wrapper。

优点:做一些事前工作,做一些事后工作。

通过函数指针,实现Strategy 模式

Strategy 模式
策略模式(Strategy Pattern):定义一系列算法,将每一个算法封装起来,并让它们可以相互替换。策略模式让算法独立于使用它的客户而变化,也称为政策模式(Policy)。

策略模式是一种对象行为型模式。
https://design-patterns.readthedocs.io/zh_CN/latest/behavioral_patterns/strategy.html#strategy

class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc) { return 0; }
class GameCharacter
{
public:
    typedef int(*HealthCalcFunc) (const GameCharacter &);
    explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc):healthFunc(hcf){}
    int healthValue() const
    {
        return healthFunc(*this);
    }
private:
private:
    HealthCalcFunc healthFunc;
};
  1. 同一人物类型之不同实体可以有不同的健康计算函数
  2. 某已知人物之健康计算函数可在运行期变更。可以提供一个setHealthCalculator ,用来替换当前的健康指数计算函数。

计算其生命值的函数,不再是GameCharacter 继承体系内的成员函数”。如果人物的健康可以纯粹根据该人物public 接口得来的信息加以计算,这没问题,如果需要on-public 信息进行精准计算,有问题。

一般,唯一的能够解决“需要以non-member 函数访问class 的non-public 成分”的方法;弱化class 的封装。例如,class 可声明那个non-member 函数为friends,或是为其实现的某一部分提供public 访问函数。

由tr1::function 完成Strategy 模式

class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc) { return 0; }
class GameCharacter
{
public:
    typedef std::tr1::function<int(const GameCharacter &)> HealthCalcFunc;

    explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc):healthFunc(hcf){}
    int healthValue() const
    {
        return healthFunc(*this);
    }
private:
private:
    HealthCalcFunc healthFunc;
};

函数对象:

short calcHealth(const GameCharacter&);
struct HealthCalculator {
    int operator()(const GameCharacter &)const {

    }
};
class GameLevel {
public:
    float health(const GameCharacter &)const;
};
class EvilBadGuy :public GameCharacter {
    // 同前
};
class EyeCandyCharacter:public GameCharacter{// 另一个人物类型,假设其构造函数与EvilBadGuy 同
	//...
};
EvilBadGuy ebg1(calcHealth);// 人物1,使用某个函数计算健康指数
EyeCandyCharacter eccl(HealthCalculator());//人物2,使用某个函数对象
GameLevel currentLevel;

EvilBadGuy ebg2(std::tr1::bind(&GameLevel::health,currentLevel,_1));//人物3,使用某个成员函数计算健康指数

古典的Strategy 模式

在这里插入图片描述

class GameCharacter;
class HealthCalcFunc {
public:
    //...
    virtual int calc(const GameCharacter& gc)const
    {//...
    }
    //...
};
HealthCalcFunc defaultHealthCalc;
class GameCharacter
{
public:
    typedef std::tr1::function<int(const GameCharacter &)> HealthCalcFunc;

    explicit GameCharacter(HealthCalcFunc hcf = &defaultHealthCalc):phealthFunc(hcf){}
    int healthValue() const
    {
        return phealthFunc->calc(*this);
    }
private:
private:
    HealthCalcFunc* phealthFunc;
};
  • virtual 函数的替代方案包括NV1 手法及Strategy 设计模式的多种形式。NV1 手法自身是一个特殊形式的Template Method 设计模式
  • 将机能从成员函数移到class 外部函数,带来的一个缺点,非成员函数无法访问class 的non-public 成员
  • tr1::function 对象的行为就像一般函数指针。这样的对象可接纳“与给定之目标签名式兼容”的所有可调用物。

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

非虚函数,静态绑定。如果调用非虚函数,父类指针指向子类对象时,调用的是父类函数,子类指针调用子类的。
public 继承是,is-a 关系。在class 内声明一个non-virtual 函数会为该class 建立一个不变性,凌驾其特异性。如果你将这两个观点施行于两个classes B 和 D,以及non-virtual 成员函数B::mf 身上,则:

  • 适用于B 对象的每一件事,D 适用,每个D 都是一个B
  • B 的派生类,继承mf 的接口和实现,因为mf 为非虚函数。

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

范围:继承一个带有缺省参数值的virtual 函数
virtual 函数系动态绑定,而缺省参数值却是静态绑定。

对象的所谓静态类型,就是它在程序中被声明时所采用的类型。

class Shape {
public:
    enum ShapeColor{Reg,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* pc = new Circle;
Shape* pr = new Rectangle;

ps,pc,pr 都是pointer-to-Shape 类型,都为静态类型。动态类型:”目前所指对象的类型“。
虚函数系动态绑定而来,调用一个虚函数时,取决于发出调用的那个对象的动态类型。
但,缺省参数是静态绑定,”调用一个定义于派生类内的虚函数“同时,使用基类为它所指定的缺省参数值。很好理解,传参时,是静态的。

通过复合,构建has-a 或”根据某物实现出“关系

某种类型的对象内含它种类型的对象。

比如,人有一个名称,一个地址,一部手机。

根据某物实现出,举例:借助于stl 中的list 实现了自己的SET,此set 底层不适用二叉树。

明智而谨慎地使用private 继承

  • 如果类之间的继承关系是private,继承器不会自动将一个派生类对象转换为一个基类对象。这和共有继承的情况不同。
  • 由私有继承而来的所有成员,在派生类中都会变成私有属性

私有继承意味着,根据某物实现出。如果让class D 以私有形式继承class B,用意是为了采用class B 内已经备妥的某些特性,不是因为B 对象和D 对象存在有任何观念上的关系。私有继承纯粹是一种实现技术。

尽可能地使用复合,必要时才使用private 继承-----当protected 成员和/或virual 函数牵扯进来地时候。还有,当空间方面的利害关系足以踢翻private 继承的支柱时。

Widget 仅仅需要一个定时操作,但没有需要导出共有的OnClick 接口(即,如果public 继承,它应该可以被当作一个timer 使用)此时,有两种办法:

  1. 私有继承,
  2. 共有继承+导入一个新的class。
class Timer {
public:
    explicit Timer(int tickFrequency);
    virtual void onTick() const;// 定时器每滴答一次,此函数自动被调用一次
};
class Widget :private Timer {
private:
    virtual void onTick()const;// 查看Widget 的数据...等等
                                // 共有继承,将误导外部调用onTick 函数
};

class Widget :
    class WidgetTimer : public Timer {
    public:
        virtual void onTick()const;// 查看Widget 的数据...等等
                                    // 共有继承,将误导外部调用onTick 函数
    };
    WidgetTimer timer;
};

如果Widget 继承Timer,当widget 被编译时Timer 的定义必须可见,所以定义Widget 的那个文件必须#include Timer.h 。但如果WidgetTimer 移除Widget 之外而Widget内含指针指向一个WidgetTimer,Widget 可以只带着一个简单的WidgetTimer 声明式,不再需要#include 任何与Timer 有关的东西。

当你面对”并不存在is-a 关系“的两个classes,其中一个需要访问另一个的protected 成员,或需要重新定义其一或多个virtual 函数,private 继承极其有可能称为正统设计策略。

  1. private 继承意味着根据某物实现出。它通常比复合的级别低。但是当派生类需要访问被保护的基类的成员,或需要重新定义继承而来的virtual 函数,这么设计是合理的。
  2. 和复合不同,私有继承可以造成空base 最优化。这对致力于”对象尺寸最小化“的程序库开发者而言,可能很重要。

明智而审慎地使用多重继承

当MI 进入设计景框,程序有可能从一个以上的base 类继承相同名称—>导致较多的歧义---->通过指定基类来消除这种分歧。需要注意:
即使两个函数之中只有一个可取用(一个public,一个private)。c++ 在看到是否有一个函数可取用之前,C++ 首先确认这个函数对此调用之言是最佳匹配。找出最佳匹配后才检验其可取用性。本例的两个checkouts有相同的匹配程度,没有所谓最佳匹配。因此,private 的那个可取用性也就未被编译器审查。

  • 钻石型多重继承
    两个问题,共同的基类的成员变量经由每一条路径被复制?只有一份副本?
    默认,每一条路径被复制。
    如果想只有一个副本:virtual base class。
    所有,直接继承自这个共有基类的classes采用,virtual 继承即可。
    在这里插入图片描述

使用virtual 继承的那些classes 所产生的对象往往比使用non-virtual 继承的兄弟们体积大,访问virtual base classes 的成员变量时,也比访问non-virtual base classes 的成员变量速度慢。

virtual 继承的成本的其他方面:支配”virtual base classes 初始化“的规则比起non-virtual bases 的情况原为复杂且不直观。virtual base 的初始化责任是由继承体系中的最底层负责。

  1. classes 若派生自virtual bases 而需要初始化,必须认知其virtual bases-不论那些bases 距离多远
  2. 当一个新的derived class 加入继承体系中,它必须承担其virtual bases 的初始化责任(不论直接或间接)

建议:
3. 非必要,不适用virtual bases
4. 如果必须使用virtual base classes,尽可能避免在其中放置数据。这么,不需要担心这些classes 身上的初始化(和赋值)所带来的问题。

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

记住

  1. 多重继承比单一继承复杂。它可能导致新的歧义性,以及对virtual 继承的需要
  2. virtual 继承会增加大小、速度、初始化(及赋值)复杂度等等成本,如果virtual base clases 不带任何数据,将是最具实用价值的情况
  3. 多重继承的确有正当用途,其中一个情节涉及”public 继承某个interface class“和”private 继承某个协助实现的class“的两相组合。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值