六、继承与面向对象设计条款32-34

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

is-a即“是一种”的关系,比如Drive继承自Base,那么我们有:

  • 每一个Derive对象都是Base对象。Base对象可以派上用场的地方Derive照样可以。
  • 但是Derive可以派上用场的地方,Base却无法效劳。因为Derive里面有Base的成分,反之没有。

例如,Student继承自一个Person类,那么:

void eat(Person &p)
{
    ...
}
void study(Student &s)
{
    ...
}
Person p;
Student s;
eat(s);     // 正确调用,每个s都是p的对象
study(p);   // 错误调用,p对象不能代表s!

可以明显看到,Person类不能传递给一个Student类。

public继承的错觉

一般来说,我们的基类都是比较广泛的一个类,子类是更具体化的类,它继承了基类的特性,并且丰富了自己特有的性质。

但是世上的事没有那么的绝对,比如我有一个鸟类作为基类,鸟会飞,于是我把飞行动作作为一个虚函数,让以后的子类重写它:

class Bird
{
public:
    virtual void fly()
    {
        ...
    }
};

问题来了:企鹅是鸟类,但是企鹅会飞吗? 明显不能,那我们就不该将fly声明为虚函数!

基于此问题的一般解决方法如下有两种:

一、提供一个运行期的错误

class Penguin : public Bird
{
public:
    virtual void fly
    {
        error("penguin can't fly");
    }
};

我们重写必要的虚函数,但是实现的内部我们只用一个error提供错误信息,这样在运行的时候我们就可以明确知道这个pneguin;类是无法提供fly操作的。

二、在编译期就解决问题

这是作者比较推崇的方法,能在编译期解决的事情就不要留在运行期。

这时候我们就要修改我们class的设计了。既然鸟类也有不会飞的,那么我们继承鸟类的时候要提供一个会飞的鸟基类,在这个类中提供飞行动作,让会飞的去继承这个即可,其它的直接继承鸟类。

class Bird
{
    ...
};
// 提供飞行函数,会飞的鸟的基类
class FlyingBird : public Bird
{
public:
    virtual void fly()
    {
        ...
    }
};
// 不会飞的鸟直接继承Bird
class Penguin : public Bird
{
public:
    virtual void fly
    {
        error("penguin can't fly");
    }
};

将我们的设计声明为如此形式就可以在编译期解决问题。

综合上面的情况,我们要明确一个思想,不要把其它领域(如数学)的直觉施加到程序上面来,这样有时候可能并不奏效。代码可以通过编译,但是不代表它的逻辑、结果都是正确的呀!

作者总结

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

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

继承而来的函数如果被重写,那个Base类的重载函数也不可见了:

class Base
{
public:
    virtual void f1() = 0;
    virtual void f1(int);
    virtual void f2();
    void f3();
    void f3(double);
    ...
};
class Derive : public Base
{
public:
    //using Base::f1;
    //using Base::f3;
    virtual void f1();
    void f3();
    void f4();
};

进行如下调用:

Derive d;
int x;
d.f1();     // 正确。调用Derive::f1
d.f1(x);    // 错误。被遮掩了。
d.f2();     // 正确。调用Base::f2
d.f3();     // 正确。调用Derive::f3
d.f3(x);    // 错误。被遮掩了。

通过这几个调用可以看到,即便Base类中有多个重载函数,但是一旦Derive类重写了一个继承而来的同名函数,那么其他几个重载的也不会被继承了。如f3,Base类中有两个f3函数,但是Derive中重写了f3,那么带参数的f3在derive类中就不再可见了。

如果要让他们可见的话,把两行using Base::的注释去掉,就可以正常访问了。又或者使用转交函数的方法指定作用域。比如:

virtual void f1()
{
    Base::mf1();    
}

作者总结

derived classes内的名称会遮掩base classes内的名称。在public继承下从来没有人希望如此。

为了让被遮掩的名称再见天日,可使用using声明式或转交函数。

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

当我们继承的时候,public的成员函数总是会被继承,包括它们的实现。在继承体系中,我们一般声明为虚函数,这样才能为多态提供保证:

  • pure virtual。 声明一个pure virtual函数的目的只是为了让Derived类继承此接口,而不包括实现。一般实现都是Derived classes自己编写。
  • impure virtual. 是为了让derived classes继承该类的接口以及缺省的实现。

接下来运用书上的例子,来看看怎么恰当的使用impure/pure virtual函数。

情景

某航空公司有A和B型两种飞机,它们都采用一样的飞行方式,写成类:

// 目的地机场
class Airport
{
  ...  
};
class Airplane
{
public:
    virtual void fly(const Airport &destination)
    {
        ...
    }
    ...
};
class ModelA : public Airplane
{
    ...
};
class ModelB : public Airplane
{
    ...
};

基于当前情景,现在的设计还是一个好的设计。两种飞机都采用同一种飞行方式,那么我们就直接继承自基类的飞行方式,这样就不会A和B都另外写一份同样的代码,显得冗余,避免了代码重复。

现在该公司又生产了新型飞机C。采取不一样的飞行方式,但是我们却忘记在ModelC中给出我们新的fly函数,那么就会导致我们会调用基类的飞行方式,显然那不是我们想要的。

没错,是忘记。理论上我们只需要记得在ModelC里面重写这个虚函数就可以保证正确执行。但是实际上我们真的可能忘记。

所以作者在文中推荐的一种方式:不管你是不是调用默认的方式,都要自己写一下调用

class Airplane
{
public:
    virtual void fly(const Airport &destination) = 0;
protected:
    void defaultFly(const Airport &destination)
    {
        ... // 缺省的飞行行为
    }
    ...
};
class ModelA : public Airplane
{
public:
    virtual void fly(const Airport &destination)
    {
        defaultFly(destination);
    }
    ...
};
class ModelB : public Airplane
{
public:
     virtual void fly(const Airport &destination)
    {
        defaultFly(destination);
    }
    ...
};

这个设计虽然不能完美解决问题,因为我们还是可能因为复制黏贴而调用错误,但至少我们手动再调用一次会比之前的设计更好。
总结如下:

(1) 将fly函数写成pure virtual,就是只提供接口。

(2) 写一个protected的默认飞行函数。如果是老式飞机就在继承的fly函数中调用此函数。新型飞机就自己写实现方式。

这样就提供了更好的一层保障。

该怎么声明取决于实际情况

声明non-virtual函数的目的是为了令derived classes继承函数的接口以及一份强制性实现。

pure virtual,impure virtual,non-virtual函数之间的差异在于你要精确指定你想要derived classes继承的东西:只继承接口还是继承接口和一份缺省实现?或是一份继承接口和一份强制性实现?

作者总结

接口继承是实现继承不同。在public继承下,derived classes总是继承base class的接口。

pure virtual函数只具体指定接口继承。

impure vitual函数具体指定接口继承及缺省实现继承。

non-virtual函数具体指定接口继承及强制性实现继承。

posted @ 2018-09-24 10:47  _NewMan  阅读(269)  评论(0编辑  收藏  举报