32~40继承与面向对象设计


一、确定你的public继承塑模出is-a关系(32)

  • piublic inheritance意味着“is-a”的关系。适用于base classes身上 的每一件事情一定也适用于derived classe身上,因为每一个derived classe对象也是一个base classe对象 。
  • 现实或数学逻辑上是is-a关系的事务,在程序的世界并不一定是is-a的关系。考虑Penguin(企鹅) 、Bird和Square、Rectangle的例子。
  • is-a的关系要保证在Base class中式public的在Derived class也是public的。

二、避免遮掩继承而来的名称(33)

  • 全局变量和局部变量的遮掩
int x;                     //global 变量
void someFunc(){
    double x;              //local 变量  
    std::cin >> x;         //读一个新值赋予local变量x
}

该例子在说明,遮掩和类型没有关系。C++的名称遮掩规则是:遮掩名称

  • 继承中的遮掩
    Derived classed内的名称会遮掩Base classed内的名称。遮掩和类型无关
    特别的,对于函数重载,派生类中同名的函数会遮掩基类所有重载的同名函数。
    Derived classes 遮掩Base classes中的重载函数示例:
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();
    void mf4();
    ...
};

Derived d;
int x;
...
d.mf1();   //没问题,调用Derived::mf1
d.mf1(x);  //错误!因为Derivd::mf1遮掩了Base::mf1
d.mf2();   //没问题,调用Base::mf2
d.mf3();   //没问题,调用Derived::mf3
d.mf3(x);  //错误!因为Derivd::mf3遮掩了Base::mf3
  • 如何推翻对继承而来的名称的缺省遮掩行为?
    使用using声明—— using声明式会令继承而来的某给定名称之所有同名函数在derived 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:
using Base::mf1;//让Base class内名为mf1的所有东西在
using Base::mf3;//Derived class作用域内都可见(并且为public)
    virtual void mf1();
    void mf3();
    void mf4();
    ...
};
  • 转交函数
    • 对于private继承,如果想继承Base class的某个函数,可以使用转交函数
    • 对于不支持using声明式的旧编译器可以使用转交函数
class Base{
private :
    int x;
public:
    virtual void mf1() = 0;
    virtual void mf1(int);
    ...
};
class Derived:private Base{
public:
    virtual void mf1()//转交函数,暗自成为inline
    {
        Base::mf1();
    }
    ...
};

三、 区分接口即成和实现继承(34)

  • 声明一个pure virtual函数的目的是为了让derived classes只继承函数接口可以为pure virtual提供定义,调用它的唯一途径是“调用时指出其class名称”,这个特性用处不大。
  • 声明impure virtual函数的目的,是让derived classes继承该函数的接口和缺省实现
    impure virtual函数的缺点是:当derived classes 忘记重写base class的impure virtual函数时,derived classes 会继承base class的缺省实现。从而导致未预期的行为。可通过独立的缺省实现方法+pure virtual 函数解决,如下:
class Airport{...};
class Airplane{
public:
virtual void fly(const Airport& destination) = 0;
...
protected:
    void defaultFly(const Airport& destination);
};
void Airplane::defaultFly(const Airport& destination){
    //缺省行为
}
class ModelA:public Airplane{
public:
    virtual void fly(const Airport& destination){
        Airplane::defaultFly(destination);
    }
};

通过将接口声明为pure virtual 函数,再将缺省行为独立为protected级别的non-virtual函数,可以避免derived class忘记重写base class的impure virtual 函数。
另一种接口和缺省实现的方法:

class Airport{...};
class Airplane{
public:
virtual void fly(const Airport& destination) = 0;
...
};
void Airplane::fly(const Airport& destination){
    //缺省行为
}
class ModelC :public Airplane{
public:
    virtual void fly(const Airport& destination);
    ...
};
 void ModelC::fly(const Airport& destination){
     Airplane::fly(destination);
 }
  • 声明non-virtual函数的目的是为了令derived classes继承函数的接口及一份强制性实现
    non-virtual函数代表意义是不变性凌驾特异性,所以它绝不应该再derived class中被重新定义。

四、考虑virtual函数以外的其他选择(35)

4.1 Non-Virtual Interface(NVI手法)

  • NVI手法:令客户通过public non-virtual 成员函数间接调用private virtual 函数。NVI手法是Template Method设计模式的一个独特表现形式。non-virtual 函数称为virtual函数的外覆器。
class Gamecharacter{
public:
    int healthValue() const{.              //derived class 不重新定义它,
        ...                                //做一些事前工作
        int retVal = doHealthValue();      //做真正的工作
        ...                                //做一些事后工作
        return retVal;
    }
    ...
private:
    virtual int doHealthValue() const {
        ...
    }
};
  • NVI手法的优点
    • 在virtual函数被调用前设定好适当场景,在调用结束之后清理场景。
    • 调用前可以做:锁定互斥器、制造日志运转记录项、验证class约束条件、验证函数先决条件等等。
    • 调用后可以做:互斥器解除锁定、验证函数的事后条件、再次验证class的约束条件等等。
  • NVI手法允许derived classes 重新定义继承而来的private virtual函数,这赋予derived classes“如何实现机能”的控制能力,base classes 保留“函数何时被调用”的权利。
  • NVI手法没有必要让virtua 函数一定得是private。当某些class继承体系要求derived class在virtual函数实现内必须调用其base class的对应兄弟,则virtual 函数必须是protected。
  • 有时候virtual必须是public(如多态性质的base classes的析构函数),这种情况就不能用NVI手法了。

4.2 藉由Function Pointer 实现Strategy模式

4.2.1 Stragegy 设计模式的简单应用:

class GameCharacter;
//以下函数是计算健康指数的缺省算法
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter{
public:
    typedef int (*HealthCalcFunc)(const GameCharacter&);
    explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
    : healthFunc(hcf)
    {}
    int healthValue() const{
        return healthFunc(*this);
    }
private:
    HealthCalcFunc healthFunc;
};
  • 同一类型不同实体可以有不同的健康计算函数
  • 某已知实体的健康指数计算函数可以在运行期变更
  • 使用外部函数的缺点在于不能访问base class的内部成分(非public成分)。

4.2.2 藉由tr1::function完成Strategy模式

tr1::function将4.2.1中的函数指针泛化,一切符合或者可转换为相同签名的可调用物皆可。

class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter{
public:
    typedef std::tr1::fuction<int (const GameCharacter&)> HealthCalcFunc;
    explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
    : healthFunc(hcf)
    {}
    int healthValue() const{
        return healthFunc(*this);
    }
private:
    HealthCalcFunc healthFunc;
};

short calcHealth(const GameCharacter&);//健康计算函数,注意返回值为非int
struct HealthCalculator{               //函数对象
   int operator(const GameCharacter&){
   ...
   }
};
class GameLevel{
public:
    float health(const GameCharacter&) const;  //成员函数,注意其非int返回类型
    ...
};

class EvilBadGuy : public GameCharacter{...};
class EyeCandyCharacter :public GameCharacter{...};
EviBadyGuy ebg1(calcHEalth);
EyeCandyCharacter ecc1(HealthCalculator());
GameLevel currentLEvel;
EviBadyGuy ebg2(std::tr1::bind(&GameLevel::health,currentLEvel,_1));//health需要2个参数:this 和 GameCharacter

4.2.3 古典的Strategy模式

Strategy模式的继承体系
Strategy继承体系
实现代码:

class GameCharacter;
class HealthCalcFunc{
public:
    ...
    virtual int calc(const GameCharacter& gc) const{
        ...
    }
};

HealthCalcFunc defaultHealthCalc;

class GameCharacter{
public:
    explicit GameCharacter(HealthCalcFunc* hcf = &defaultHealthCalc)
    : pHealthFunc(hcf)
    {}
    int healthValue() const{
        return pHealthFunc->calc(*this);
    }
private:
    HealthCalcFunc& pHealthFunc;
};

古典Strategy模式的优点在于:提供一个“将机油的健康算法纳入使用”的可能性——只要为
HealthCalcFunc继承体系添加一个derived class即可。


五、绝不重新定义继承而来的non-virtual函数(36)

  • 多态性质的base classes 中的析构函数应该为virtual,如果为non-virtual,那么derived class中就不应该定义non-virtual析构函数,也就是说派生类不应该有析构函数(这显然是做不到的,因为即使你不写编译器也会生成),否则违反本条款。—— 这说明derived classes的析构函数是对base classed 的重写,这也就解释了为什么通过base class析构函数声明为virtual时,derived classes的析构函数会得到执行,因为derived classes重写base classes的虚析构函数会将虚表中的base classes中的析构函数覆盖。

六、绝不重新定义继承而来的函数的缺省参数值(37)

  • 问题:会导致动态类型调用的行为混乱
    • 原因 动态类型调用时,会导致使用静态类型的缺省参数值
    • 示例:
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 = Red) const;
 ...
};

class Circle : public Shape{
public:
 virtual void draw(ShapeColor color) const;
 ...
};
Shape* ps;
Shape* pr = new Rectangle;
Shape* pc = new Circle;

ps、pr、pc都被声明为pointer-to-Shape类型,所以它们都以Shape为静态类型,pr的动态类型为Rectangle,pc的动态类型为Circle;
缺省参数值是静态绑定的

pr->draw(); //调用的是Rectangle::draw(Shape::Red)!

pr的动态类型为Rectangle,所以调用的是Rectangle中的draw。但是缺省参数是静态绑定的,pr的静态类型是Shape,所以调用的是Shape::draw中的缺省参数。

  • 如何解决?
    使用NVI手法,将virtual函数包裹起来,通过public 函数提供缺省值。如:
class Shape{
public:
    enum ShapeColor{Red, Green, Blue};
    void void doDraw(ShapeColor color = Red) const
    {
         draw(color);
    }
private:
    virtual void draw(ShapeColor color) const = 0;
    ...
};
class Rectangle : public Shape{
public:
    ...
private:
    virtual void draw(ShapeColor color) const;
    ...
};

七、通过复合塑模出has-a或“根据某物实现出”(38)

  • 复合有两种含义:has-a、is-implemented-in-terms-of(根据某物实现出)
  • 在应用域,复合意味着has-a
  • 在实现域,复合意味着is-implemented-in-terms-of
//has-a
class Address{...};
class PhoneNumber{...};
class Person{
public:
    ...
private:
    std::String name;
    Address address;
    PhoneNumber voiceNumber;
    PhoneNumber faxNumber;
};
//is-implememnted-in-terms-of
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;    //用来表述set的数据,private访问级别
};
  • has-a、is-implemented-in-terms-of的区别:
    两者都是复合关系,但所表现的意义不同。应用域为has-a,实现域为is-implemented-in-terms-of,实现域侧重基于某物实现某物,通常为private访问级别。
  • is-a、is-implemented-in-terms-of的区别:
    is-a用来表述D也是一个B,这种表述不为真时,考虑is-implemented-in-terms-of关系。

八、明智而审慎的使用private继承(39)

  • private继承意味着is-implemented-in-terms-of(根据某物实现出)。private继承意味着只有实现被继承,接口部分应略去,因为private继承的base class的每样东西在derived classes都是private的。因此private继承在软件设计层面没有意义,其意义只在软件实现层面。
  • 尽可能使用复合,必要时才使用private继承。
  • 什么时候使用private继承?
    • 当derived class需要访问protected base class的成员,或者需要重写继承而来的virtual函数时,使用private是合理的,因为单纯的复合无法完成这两者,稍微复杂的复合+public继承可以(实现一个内部类public继承需要private继承的class,在用复合的方式添加public继承的class)
    • 空白基类最优化(empty base optimization,EBO):现实中的空基类并不真的是empty,会含有typedef、enums、static成员变量或non-virtual函数,STL中就有很多。C++规定空基类的大小不能为0,应插入一个char对象到空对象内。再加上齐位需求,空对象的大小不只是一个char的大小。因此使用private继承可以实现EBO。EBO一般只在单一继承下才可行。

九、明智而审慎的使用多重继承(40)

对于多重继承有两个观点:

  • 如果单一继承是好的,多重继承一定更好。
  • 单一继承是好的,但多重继承不值得拥有。

9.1 多重继承会导致歧义

class BorrowableItem{
public:
    void checkOut();
    ...
};

class ElectronicGadget{
private:
    void checkOut();
    ...
};

class Mp3Player : 
    public BorrowableItem,
    public ElectronicGadget
{    ...   };

Mp3Player mp;
mp.checkcOut();

虽然两个函数中只有一个可取用(一个为public,另一个为private),但仍会产生歧义。
原因:
C++解析重载的规则:C++首先确认这个函数对此调用之言是最佳匹配。找出最佳匹配才会校验可取用性。

9.2 多重继承可能会涉及virtual继承

  • 钻石多重继承
    对于钻石多重继承,C++支持两种方案:1)缺省方案:对于每一条路径都会执行复制,对于公共的base class在derived class中会有多个副本。2)virtual 继承:公共base class采用virtual 继承,公共的base class在derived class中会有一份。
  • virtual 继承的成本
    • 会增加virtual继承而来的derived class对象的大小,如:会增加virtual base class指针。
    • 访问virtual base classes的成员变量时,增加访问间接性,比访问non-virtual base classes的成员变量慢。
    • virtua base 的初始化是由继承体系中最底层的class负责。这意味着1)classes若派生子自virtual base而需要初始化,必须认知其virtual base —— 不论那些base距离多远;2)当一个新的derived class加入继承体系中,它必须承担其virtual base(不论直接或间接)的初始化责任。
  • virtual继承的建议:1)非必要不使用virtual 继承;2)如果必须使用virtual 继承,尽可能避免在virtual base中放置数据,这样可以避免对virtual base 初始化以及访问其成员带来的诡异问题;

9.3 多重继承的使用场景

  • “public 继承某个Interface class” 和“private 继承某个协助实现的class”的两者组合
class IPerson{
public:
    virtual ~IPerson();
    virtual std::string name() const = 0;
    virtual std::string birthDate() const = 0;
};

class PersonInfo{
public:
    explicit PersonInfo(DataBaseID pid);
    virtual ~PersonInfo();
    virtual const char* theName() const;
    virtual const char* theBirthDate() const;
    virtual const char* valueDelimOpen() const;
    virtual const char* valueDelimClose() const;
};

class CPerson: public IPerson, private Person Info{
public:
    explicit CPerson(DataBaseID pid):PersonInfo(pid){}
    virtual std::string name() const
    {return PersonInfo::theName();}
    
    virtual std::string birthDate() const
    {return PersonInfo::theBirthDate();}

private:
    const char* valueDelimOpen() const {retuen "";}
    const char* valueDelimClose() const {retuen "";}
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值