《Effective C++》:条款32-条款33

条款32-条款40是介绍继承相关内容的。
条款32介绍public继承塑造出基类和派生类之间的关系。
条款33介绍继承层次中,变量的作用域以及遮掩关系。

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

以C++面向对象编程,最重要一个规则是:public inheritance(公开继承)意味着“is-a”(是一种)的关系。在这里是“直译”,例如class D: public B直译就是D是一种B。

如果让class D(“Derived”)以public形式继承class B(“Base”),这便意味着一个类型为D的对象同时也是一个类型为B的对象(或者说D对象含有B对象),但是反之不成立。B比D表现出更一般化的概念,而D比B表现出更特殊化的概念。可以使用B对象的地方,也可以使用D对象。

以一个具体例子来说明:

    class Person {……};
    class Student: public Person {……};

由生活经验可以知道,每个学生都是人,但是并非每个人都是学生。这就是这个继承体系的主张。可以预期,对人成立的每件事,对学生也都成立。但是对学生成立的事对人未必成立。在C++中,任何函数如果期望接受类型为Person对象的实参(或pointer to person,或reference to person),也都可以接受一个类型为Student对象的实参(或pointer to student,或reference to student)。

 void eat(const Person& p);
    void study(const Student& s);
    Person p;
    Student s;
    eat(p);//正确
    eat(s);//正确
    study(s);//正确
    study(p);//错误 

当然,上面正确的前提是以public继承,如果以private继承,意义将完全不同(**条款**39),protected继承也是一样。

有时public和is-a之间的关系会误导我们。例如,企鹅(penguin)是一种鸟,这是事实;鸟可以飞,这也是事实。如果以C++描述这层关系:

    class Bird{
        publicvirtual void fly();
        ……
    };
    class Penguin: public Bird{
        ……
    };

但是我们知道,企鹅不会飞,这个是事实。这个问题的原因是语言(英语)不严谨。当我们说鸟会飞时,我们表达的意思是一般的鸟都会飞,并不是表达所有的鸟都会飞。我们还应该承认一个事实:有些鸟不会飞。这样可以塑造一下继承关系

    class Bird{
        ……
    };
    class FlyingBird: public Bird{
    public:
        vitual void fly();
        ……
    };
    class Penguin: public Bird{//没有fly函数
        ……
    };

这样的设计能更好的反映我们真正要表达的意思。但是这时,我们仍未完全处理好这些鸟事。例如,如果你的系统不会区分鸟会不会飞,你关心的是鸟啄和鸟翅,这样的话,原先的“双class继承体系”更适合你的系统。并不存在完美设计,具体问题要具体讨论。

还有一个方法来处理“所有鸟都会飞,企鹅是鸟,但企鹅不会飞”这个问题,我们可以在企鹅类重新定义fly函数,让它在产生一个运行期错误:

    void error(const std::string& msg);//输出错误
    class Penguin: public Bird {
    public:
        virtual void fly(){ error("Attemp to make a penguin fly");}
        ……
    };

这里前面的解决方法不同,这里不说企鹅不会飞,当你说企鹅会飞时,会告诉你这是一个错误。但是这种解决方法之间有什么差异?从错误被侦测出来的时间来看,第一种解决方法“企鹅不会飞”这个限制条件在编译期强加事实;第二个解决方法,“企鹅会飞是错误”是在运行期检测出来的。第一种解决方法更好,**条款**18说过,好的接口可以防止无效的代码通过编译,相比之下,我们应该选择在编译期来找出这个问题。

在考虑一个例子,基础几何我们都学过,那么正方形和矩形的关系有多么复杂呢?先看下面这个例子:class Square应该以public形式基础class Rectangle吗?我们都知道正方形是特殊的矩形,如果以public继承

 class Rectangle{
    public:
        virtual void setHeight(int newHeight);
        virtual void setWidth(int newWidth);
        virtual int height() const;
        virtual int width() const;
        ……
    };

    void makeBigger(Rectangle& r)//增加r的面积
    {
        int oldHeight=r.height;
        r.setWidth(r.width()+10);//r宽度增加
        assert(r.height()==oldHeight);//判断r的高度是否改变
    }

上面的assert结果肯定为真,因为makeBigger只是改变了r的宽度,高度并未改变。

    class Square:public Rectangle{……};
    Square s;
    ……
    asseret(s.width()==s.height());//对所有正方形都为真
    makeBigger(s);//因为是public继承是is-a关系,所以可以使用这个函数
    asseret(s.width()==s.height());//对正方形也应该为真

那么现在肯定是有问题了。因为第一个assert时,长和宽多相等;之后增加了宽度,长度不变;到了第二个assert时,长和宽还相等。

前面说过,以public继承,能够施行于base class对象身上的每件事,都可以施行于derived对象身上。在正放心和矩形的例子(还有一个类似的是条款38的sets和lists),这个结论行不通,所以一public继承塑模它们之间的关系不正确。所以我们应该记住:代码通过编译不表示就可以正确运行。

is-a只是存在class继承关系中的一种,还有两个继承关系式has-a(有一个)和is-implemented-in-terms-of(根据某物实现出)。这些关系将在条款**38和条款**39讨论。在设计类时,应该了解这些classes之间的相互关系和相互差异,在去塑模类之间的关系。

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

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

这里说的名称,是和继承以及作用域有关。先看一个和作用域有关的例子:

 int x;//global
    void someFunc();
    {
        double x;//local
        std::cin>>x;//给local变量赋值
    }

这个cin是给local变量x赋值,而不是global变量x,因为内层作用域名称会遮掩外围作用域名称。当编译器在someFunc作用域内遇到名称x时,它在local作用域内查找是否有这个变量定义,如果找不到就再去找其他作用域。这个例子中的变量x类型不同,local的是double类型,而global的是int类型;但是这个并不要紧,C++的名称遮掩规则(name-hiding rules)所做的唯一事情就是:遮掩名称,至于类型并不重要。

现在来看一下继承。当一个derived class成员函数内指涉(refer to) base class内的某物(成员函数、成员变量、typedef等)时,编译器可以找到所指涉的东西,因为derived class继承了声明在base class内的所有东西。derived class的作用域被嵌套的base class作用域内。

     class Base{
        private:
            int x;
        public:
            virtual void mf1()=0;
            virtual void mf2();
            void mf3();
            ……
        };
        class Derived: public Base{
        public:
            virtual void mf1();
            void mf4();
            ……
        };

技术分享
这个例子中既有public,又有private。成员函数有pure virtual、impure virtual和non-virtual,这是为了强调我们讨论的是名称,和其他无关。这个例子是单一继承,了解单一继承很容易推断多重继承。假设在derived class的mf4内调用mf2

 void Derived::mf4()
    {
        mf2();
    }

当编译器看到mf2时,要知道它指涉(refer to)什么东西。首先在local作用域内(即mf4覆盖的作用域)查找有没有名称为mf2的东西;如果找不到,再查找外围作用域(class Derived覆盖的作用域);如果还没找到,再往外围找(base class覆盖作用域),在这里找到了。如果base内还是没找到,之后继续在base那个namespace作用域内找,最后往global作用域找。

下面把这个例子变得稍微复杂一点,重载mf1和mf3,且添加一个新版mf3到Derived中。

 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();
        ……
    };

技术分享
因为以作用域为基础的“名称遮掩规则”,base class内所有名称为mf1和mf3的函数都被derived class内的mf1和mf3函数遮掩掉了。从名称查找观点来看,Base::mf1和Base::mf3都不再被Derived继承。

 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

条款**32中说过public继承是**is-a关系,如果使用public继承而又不继承那些重载函数,就是违反了is-a关系。要想上面的函数调用都正确,可是使用using声明

 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:
        //让Base class内名为mf1和mf3的所有东西在Derived作用域内都可见,且为public
        using Base::mf1;
        using Base::mf3;
        virtual void mf1();
        void mf3();
        void mf4();
        ……
    };

这样,下面的调用都不会出错了。

 Derived d;
    int x;
    d.mf1();//正确,调用Derived::mf1
    d.mf1(x);//调用Base::mf1
    d.mf2();//正确,调用Base::mf2
    d.mf3();//正确,调用Derived::mf3
    d.mf3(x);//调用Base::mf3

如果你继承base class,且加上重载函数;你又希望重新定义或覆写其中一部分,那么要把被遮掩的每个名称引入一个using声明。

public继承暗示base和derived class之间是一种is-a关系,这也是上述using声明放在derived class的public作用域内的原因:base class内的public名称在publicly derived class内也应该是public。

如果想要private继承Base,而Derived唯一想继承的是时Base内mf1无参数的那个版本,using声明在这派不上用场,因为using声明会使继承而来的某个名称所有函数在derived class都可以见。这样的实现需要一个不同的技术,一个简单的转交函数(forwarding function):

    class Base{
    public:
        virtual void mf1()=0;
        virtual void mf1(int);
    };
    class Derived: private Base{
    public:
        virtual void mf1()//转交函数(forwarding function)
        {Base::mf1();};//隐式成为inline
    };
    Derived d;
    int x;
    d.mf1();//调用Derived::mf1
    d.mf1(x);//错误,Base::mf1被遮掩了

上面所述都是不含templates。当继承结合templates时,又会面临“继承名称被遮掩”,关于以“角括号定界”的东西,在**条款**43讨论。

总结
- derived classes内的名称会遮掩base classes内的名称。在public继承下从来没有人希望如此。
- 为了让遮掩的名称在derived class内重见天日,可以使用using声明或转交函数(forwarding function)。

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。