zoukankan      html  css  js  c++  java
  • 六、继承与面向对象设计条款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函数具体指定接口继承及强制性实现继承。

  • 相关阅读:
    由保存当前用户引发的springboot的测试方式postman/restlet还是swagger2
    VS集成opencv编译C++项目遇到的问题
    利用StringUtils可以避免空指针问题
    springboot集成Guava缓存
    Oracle 课程四之索引
    Oracle 课程三之表设计
    Oracle 课程二之Oracle数据库逻辑结构
    Oracle 课程一之Oracle体系结构
    Oracle权限一览表
    Informatica元数据库解析
  • 原文地址:https://www.cnblogs.com/love-jelly-pig/p/9694966.html
Copyright © 2011-2022 走看看