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

1.前言

关于程序中的变量和函数名称,都有其各自的作用域,比如下面的代码:

int x;//global变量
void someFunc()
{
    double x;//local变量
    std::cin>>x;//读取一个新值赋予local变量x

}

这个读取数据的语句指的是local变量x,而不是global变量x,因为内层作用域的名称会遮掩(覆盖)外部作用域的名称。

当编译器处于someFunc的作用域并遇到名称x时,它将在local作用域内查找是否有这个变量名称,如果找到就不再找其它作用域。本例中的someFunc的x是double类型而global x是int类型,这个不重要。c++的名称遮掩规则所做的唯一事情就是:遮掩变量名称,至于名称是否为相同的类型,并不重要。本例子中的x的double遮掩了一个名称为int 的x。

现在引入继承中的变量名遮掩(覆盖)情况。

2.实例分析

我们知道,当位于一个derived class成员函数内涉及base class内的某变量或函数时候,编译器可以找到我们我涉及到的变量,因为derived classes继承了声明于base classes内的所有东西。实际运行方式是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三种,这是为了强调我们谈的是名称,和其它无关。这个例子中也可以加入各种名称类型,例如enums,nested classes和typedefs。本节讨论的唯一重要的是这个变量的名称。至于这些变量的类型是什么并不重要。本例使用单一继承为例,然后了解单一继承下发生的事,很容易就可以推想c++在多重继承下的行为。

假设derived class内的mf4的部分实现代码如下:

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

当编译器看到这里使用名称mf2,必须会估算它涉及到什么变量,编译器的做法是查找各作用域,看看有没有某个名为mf2的函数,于是查找其外围作用域,也就是class Derived覆盖的作用域,当还是没找到任何名为mf2的函数,于是再往外围查找,本例为base class。在那编译器找到一个名为mf2的函数,于是停止查找。如果Base内还是没有mf2,查找动作将继续下去,首先找内含Base的那个namespace的作用域,最后往global作用域去找。

再次借助上个例子,这次让我们重载mf1和mf3,并且添加一个新版mf3到Derived去。正如条款36所说,这里发生的事情是:Derived重载了mf3,那是一个继承而来的non-virtual函数,这会使得整个设计立刻显得不合理,但为了充分认识继承体系内的“名称可视性”,我们暂时安之若素。

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

如你所见,上述规则都适用,即使Base classes和derived classes内的函数有不同的参数类型也适用,而且不论函数是virtual或non-virtual均适用,这和本条款一开始展示的道理相同,当时函数someFunc内的double x遮掩了global作用域内的int x,如今Derivaed内的函数mf3遮掩了一个名为mf3但类型不同的Base函数。

这些行为背后的基本理由是为了防止你在程序库或应用框架内建立新的derived class时附带地从疏远的base classes继承重载函数,不幸的是你通常会想继承重载函数,实际上如果你正在使用public继承而又不继承那些重载函数,就是违反base 和derived class之间的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:
        using Base::mf1;//让Base class内名为mf1和mf3的所有东西
        using Base::mf3;//在Derived作用域内都可见
        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();//没问题,调用Derivedc::mf3
d.mf3(x);//现在没问题,调用Base::mf3

这意味者如果继承base class并加上重造函数,而你又希望定义或重写一部分,那么你必须为那些原本会被遮掩的每个名称引入一个using声明式 ,否则你希望继承的名称将会被遮掩。

有时候你并不想继承base classes的所有函数,这是可以理解的。在public继承下,这绝对不可能发生,然而在private继承之下却是有意义的。例如假设Derived以private形式继承Base,而Derived唯一想继承的mf1是那个无参数版本。using声明式在这里派不上用场,因为using声明式会令继承而来的某给定名称之所有同名函数在derived class中都可见。

class Base{

    public:
        virtual void mf1()=0;
        virtual void mf1(int);
        ...//与前同
};
class Derived:private Base{
    public:
        virtual void mf1()//转交函数
        {
            Base::mf1();
        }
        ...
};
...
Derived d;
int x;
d.mf1();//调用的是Derived::mf1
d.mf1(x);//错误,Base::mf1()被遮掩

inline转交函数的另一个用途是为那些不支持using声明式的老旧编译器开辟一条新路,将继承而得的名称汇入derived class作用域内。

3.总结

derived classes内的名称会遮掩base classes内的名称,在public继承下从来没有人会希望这样;

为了让被遮掩的函数或变量被调用,可以使用using声明式或转交函数。

你可能感兴趣的:(c++,数据结构,开发语言)