zoukankan      html  css  js  c++  java
  • effective C++ 条款 34:区分接口继承和实现继承

    身为class设计者,有时候你希望derived class只继承成员函数的接口(也就是声明):有时候你又希望derived class同时继承函数的

    接口和实现,但又希望能够覆写(override)它们所继承的实现:又有时候你希望derived class同时继承函数的接口与实现,并且不允许覆写任何东西。

    让我们考虑一个展现绘图程序中各种几何形状的class继承体系:

    class Shape {
    public:
        virtual void draw() const = 0;
        virtual void error(const std::string& msg);
        int objectID() const;
    };
    class Rectangle : public Shape {...};
    class Ellipse : public Shape {...};

    Shape的pure virtual函数draw使它成为一个抽象class。Shape强烈影响了所有以public形式继承它的derived classes。因为:

    成员函数的接口总是会被继承

    首先考虑pure virtual函数draw:pure virtual函数有两个最突出的特性:它们必须被任何“继承了它们”的具象class重新声明,而且它们在抽象class中通常没有定义。所以:

    声明一个pure virtual函数的目的是为了让derived class只继承函数接口。Shape::draw的声明式乃是对具象derived class设计者说“你必须提供一个draw函数,但我不干涉你怎么实现它”

    令人意外的是,我们竟然可以为pure virtual函数提供定义。但调用它的唯一途径是“调用时明确指出其class名称”:

    Shape* ps = new Shape; //wrong! abstract class Shape
    Shape* ps1 = new Rectangle;
    ps1->draw(); //Rectangle::draw()
    Shape* ps2 = new Ellipse;
    ps2->draw(); //Ellipse::draw()
    ps1->Shape::draw();
    ps2->Shape::draw();

    它可以提供一种机制,为简朴impure virtual函数提更平常更安全的缺省实现。

    impure virtual函数会提供一份实现代码,derived classes可能覆写(override)它:

    生命简朴的(非纯)impure virtual函数的目的,是让derived class继承该函数的接口和缺省实现

    考虑Shape::error这个例子。

    其接口表示,每个class都必须支持一个“当遇上错误时可调用”的函数,但每个class可自由处理错误。如果某个class不想针对错误作出任何特殊行为,可以退回到Shape class提供的缺省错误处理行为。

    但是允许impure virtual函数同时指定函数声明和函数缺省行为,却有可能造成危险。考虑xyz航空公司设计的继承体系。该公司只有A和B两种飞机,相同方式飞行:

    class Airport {...};
    class Airplane {
    public:
        virtual void fly(const Airport& destination);
        ...
    };
    void Airplane::fly(const Airport& destination)
    {
        //缺省代码,将飞机飞至指定目的地
    }
    class ModelA: public Airplane {...};
    class ModelB: public Airplane {...};

    为表示所有飞机都一定能飞,并阐明“不同型飞机原则上需要不同的fly实现”Airplane::fly被声明为virtual。然而为了避免在ModelA和ModelB中撰写相同代码,缺省飞行行为有Airplane::fly提供,它同时被ModelA和ModelB继承。

    现在,xyz决定购买一种新式C型飞机,这种飞机的飞行方式不同。

    xyz的程序员在继承体系中对C型飞机添加了一个class,但由于他们急着让新飞机上线服务,忘了重新定义器fly函数:

    class ModelC: public Airplane {...};

    然后在代码中有一些诸如此类的动作:

    Airport PDX(...);
    Airplane* pa = new ModelC;
    ...
    pa->fly(PDX);

    这将酿成大灾难:这个程序试图用ModelA和ModelB的飞行方式来飞ModelC。

    问题不在Airplane::fly()有缺省行为,而在于ModelC在未明白说出“我要”的情况下就继承了该缺省行为。我们可以做到“提供缺省实现给derived classes,但除非它们明确要求,否则免谈”。此间伎俩在于切断“virtual 函数接口”和其“缺省实现”之间的连接。下面是一种做法:

    class Airplane {
    public:
        virtual void fly(const Airport& destination) = 0;
        ...
    protected:
        void defaultFly(const Airport& destination);
    };
    void Airplane::defaultFly(const Airport& destination)
    {
        //缺省行为,将飞机飞至目的地
    }

    fly已被改成为一个pure virtual函数,只提供飞行接口。缺省行为以defaultFly出现在Airplane class中。若想使用缺省实现(例如ModelA和ModelB),可以在fly中对defaultFly做一个inline调用:

    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);
        }
        ...
    };

    现在ModelC不可能意外继承不正确的fly实现代码了,因为Airplane中的pure virtual函数迫使ModelC必须提供自己的fly版本:

    class ModelC: public Airplane {
    public:
        virtual void fly(const Airport& destination);
        ...
    };
    void ModelC::fly(const Airport& destination)
    {
        //将C型飞机飞至指定的目的地
    }

    这个方案并非安全无虞,程序员还是可能因为剪贴(copy-and-paste)代码而招来麻烦,但它比原来的设计值得依赖。

    Airplane::defaultFly是一个non-virtual函数,这一点也很重要。因为没有任何一个derived class应该重新定义此函数。

    有些人反对以不同的函数分别提供接口和缺省实现,向上述的fly和defaultFly那样。我们可以利用“pure virtual函数必须在derived class中重新声明,但它们可以拥有自己的实现”这一事实。下面是Airplane继承体系如何给pure virtual函数一份定义:

    class Airplane {
    public:
        virtual void fly(const Airport& destination) = 0;
        ...
    };
    void Airplane::fly(const Airport& destination) // pure virtual 函数实现
    {
        //缺省行为,将飞机飞至指定目的地
    }
    class ModelA: public Airplane {
    public:
        virtual void fly(const Airport& destination)
        {
            Airplane::fly(destination);
        }
        ...
    };
    class ModelB: public Airplane {
    public:
        virtual void fly(const Airport& destination)
        {
            Airplane::fly(destination);
        }
        ...
    };
    class ModelC: public Airplane {
    public:
        virtual void fly(const Airport& destination)
        ...
    };
    void ModelC::fly(const Airport& destination)
    {
        // 将C型飞机飞至指定目的地
    }

    这几乎和前一个设计一模一样,只不过pure virtual函数Airplane::fly替换了独立函数Airplane::defaultFly。其声明部分表现表的是接口(derived class必须使用的),其定义部分表现出缺省行为(那是derived class可能使用的,但只有在它们明确提出申请时才是)。合并fly和defaultFly,就丧失了“让两个函数享有不同保护级别”的机会:习惯上被设为protected的函数(defaultFly)如今成了public(因为它在fly之中)。

    如果成员函数是个non-virtual函数,意味着它并不打算在derived classes中有不同的行为。non-virtual 成员函数所表现的不变性凌驾其特异性,无论derived class变得多么特异化,它的行为都不可以改变。

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

    来看Shape::objectID的声明:可以想做是“每个Shape对象都有一个用来产生对象识别码的函数:此识别码总是采用相同计算方法,该方法由Shape::objectID的定义式决定,任何derived class都不应该尝试改变其行为”。

  • 相关阅读:
    进军装饰器2/3
    进军装饰器1/3
    购物车
    多级菜单(高效版)
    工资管理系统
    多级菜单(低效版)
    用户登录程序
    Accessibility辅助功能--一念天堂,一念地狱
    使用FragmentTabHost+TabLayout+ViewPager实现双层嵌套Tab
    android性能优化练习:过度绘制
  • 原文地址:https://www.cnblogs.com/lidan/p/2345324.html
Copyright © 2011-2022 走看看