1. 普通作用域中的隐藏
名字实际上和继承没有关系。有关系的是作用域。我们都知道像下面的代码:
1 int x; // global variable 2 3 void someFunc() 4 { 5 double x; // local variable 6 7 std::cin >> x; // read a new value for local x 8 9 10 }
读入x的声明指向的是本地的x而不是全局的x,因为内部作用域的名字将外部作用域的变量隐藏掉了。我们将作用域的这种情况用以下方式进行可视化:
当编译器在someFunc的作用域中时,遇到了名字x,它们先在本地作用域中查询是否有以这个名字命名的一些东西。因为存在,它们就不再检查别的作用域了。在这种情况中,someFunc的x是double类型,而global的x是int类型,这没有关系。C++的名字隐藏规则会对其进行处理:隐藏名字。不管与名字对应的是相同还是不同的类型都没有关系。在这种情况中,被命名为x的double隐藏了命名为x的int。
2. 编译器是如何在继承体系中寻找名字的
现在进入继承。我们知道当我们处在一个派生类成员函数内部时,并且引用了一些基类的东西(例如,一个成员函数,一个typedef或者一个数据成员),编译器能够找到我们所引用的东西,因为派生类继承了声明在基类中的这些东西。实际的工作方式是派生类的作用域被镶嵌在基类作用域内部。举个例子:
这个类中既有public的名字也有private的名字,既有数据成员也有成员函数。成员函数包括纯虚函数,普通虚函数(非纯虚函数)以及非虚函数。这是为了强调我们要讨论的是名字而不是别的。例子中也能包含类型名称,例如,枚举,嵌套类和typedefs。在这次的讨论中我们唯一关心的是他们是名字。他们是什么样的类型无关紧要。这个例子使用的是单继承,但是一旦你明白了在单继承下会发生什么,多继承下的C++行为很容易就能够预推测出来。
假设派生类中的mf4定义如下:
1 void Derived::mf4() 2 { 3 ... 4 mf2(); 5 ... 6 }
当编译器看到这里使用了名字mf2,它们必须理解mf2指向的是什么。它们会在作用域中寻找名字为mf2的一个声明。首先他们在local作用域中寻找,但是发现没有任何名字为mf4的声明。接下来寻找包含(containing)作用域,也就是类Derived。它们仍然没有发现mf2,所以它们进入了下一个包含作用域,也就是基类。在这里找到了名字为mf2的声明,搜索结束了。如果在基类中没有mf2,搜索会继续进行,首先在包含Derived的namespace(s)中寻找,最后在全局作用域内寻找。
我们刚刚描述的流程是准确的,但是对于名字如何在C++中被发现来说是不全面的。我们的目标不是为了了解足够的名字寻找规则然后实现一个编译器,然而,我们应该了解足够的规则来避免碰到一些让人感到意外的事情,对这个工作,我们已经了解了大量的信息了。
3. 继承体系中的名字是如何被隐藏的
考虑前面的例子,这次我们除了要重载mf1和mf3之外,也在Derived中添加一个mf3版本。(正如Item36中解释的,Derived中mf3的声明——一个继承而来的非virtual函数——会让这个设计看起来很可疑,但是为了理解继承下的名字可见性,我们忽略这个问题。)
1 class Base { 2 private: 3 int x; 4 public: 5 virtual void mf1() = 0; 6 virtual void mf1(int); 7 virtual void mf2(); 8 void mf3(); 9 void mf3(double); 10 ... 11 }; 12 class Derived: public Base { 13 public: 14 virtual void mf1(); 15 void mf3(); 16 void mf4(); 17 ... 18 };
这段代码产生的行为会让每个首次碰到这种情况的C++程序员都感到吃惊。基于作用域的名字隐藏规则并没有变化,所以基类中的名字为mf1和mf3的所有函数被派生类中相同名字的函数隐藏掉了。从名字搜寻的角度来看,Base::mf1和Base::mf3不再被Derived继承了!
1 Derived d; 2 int x; 3 ... 4 d.mf1(); // fine, calls Derived::mf1 5 d.mf1(x); // error! Derived::mf1 hides Base::mf1 6 7 d.mf2(); // fine, calls Base::mf2 8 9 d.mf3(); // fine, calls Derived::mf3 10 11 d.mf3(x); // error! Derived::mf3 hides Base::mf3
正如你所看到的,对于有相同名字的基类和派生类中的函数有,即使参数类型不同,上面的隐藏规则也同样适用,并且它和函数的虚与非虚没有关系。在这个条款开始也是同样的方式,函数someFunc中的double x隐藏了全局作用域的int x,在这里Derived中的函数mf3隐藏了基类中名字为mf3的函数,即使参数类型不一样。
4. 如何将隐藏行为覆盖掉
这种行为背后的基本原理在于当你在一个库或者应用框架中创建一个新的派生类时,它能够阻止你突然从遥远的基类中继承重载函数。不幸的是,你总是想继承重载函数。事实上,如果你正在使用public继承并且没有继承重载函数,你就违反了基类和派生类之间的”is-a”关系,这是Item 32中介绍的public继承的基本原则。这就是基本情况,你总是想对继承来的名字的默认隐藏行为进行覆盖。
4.1 方法一 使用using 声明
使用using声明来达到这个目的:
现在的继承就按照你所期望的进行工作了:
1 Derived d; 2 int x; 3 ... 4 d.mf1(); // still fine, still calls Derived::mf1 5 d.mf1(x); // now okay, calls Base::mf1 6 7 d.mf2(); // still fine, still calls Base::mf2 8 9 d.mf3(); // fine, calls Derived::mf3 10 11 12 13 d.mf3(x); // now okay, calls Base::mf3 (The int x is 14 // implicitly converted to a double so that 15 // the call to Base::mf3 is valid.)
这意味着如果你的类继承自包含重载函数的基类,你想对其中的一些函数进行重新定义或者覆盖,你需要为每个即将被隐藏掉的名字包含一个using声明,如果你不这样做,你想继承的一些名字就会被隐藏。
4.2 方法二 使用forwarding函数
有时候你并不想继承基类的所有函数,这是可能的。在public继承下,你应该永远拒绝这种行为,因为,它违反了基类和派生类之间public继承的”is-a”关系。(这也是为什么上面的using声明放在派生类的public部分:基类中的public名字在public继承的派生类中应该也是public的)。然而在private继承中(见Item 39),它也是有意义的。举个例子,假设Derived私有继承自基类Base,Derived类想继承基类函数mf1的唯一版本是不带参数的版本。Using声明在这里就不工作了,因为一个using声明会使得所有继承而来的函数的名字在派生类中是可见的。这里可以使用不同的技术,也就是简单的forwarding函数:
1 class Base { 2 public: 3 virtual void mf1() = 0; 4 virtual void mf1(int); 5 6 ... 7 8 // as before 9 10 11 }; 12 class Derived: private Base { 13 public: 14 virtual void mf1() // forwarding function; implicitly 15 { Base::mf1(); } // inline — see Item 30. (For info 16 ... // on calling a pure virtual 17 }; // function, see Item 34.) 18 ... 19 Derived d; 20 int x; 21 d.mf1(); // fine, calls Derived::mf1 22 d.mf1(x); // error! Base::mf1() is hidden
另外一种使用inline forwarding函数的地方是在使用比较老的编译器的时候,它们不支持通过using 声明来将继承而来的名字导入到派生类作用域中。
这就是关于继承和名字隐藏的全部故事,但是当继承同模板结合起来的时候,一个完全不同的“继承而来的名字被隐藏”问题就会出现,详情见 Item 43。
5. 总结
- 派生类中的名字会将基类中的名字隐藏掉。在公共继承中,这绝不是令人满意的。
- 为了让隐藏起来的名字重见天日,使用using声明或者forwarding函数来达到目的。