1. 普通作用域中的隐藏

名字实际上和继承没有关系。有关系的是作用域。我们都知道像下面的代码:

 int x;                 // global variable

 void someFunc()
{
double x; // local variable std::cin >> x; // read a new value for local x }

读入x的声明指向的是本地的x而不是全局的x,因为内部作用域的名字将外部作用域的变量隐藏掉了。我们将作用域的这种情况用以下方式进行可视化:

当编译器在someFunc的作用域中时,遇到了名字x,它们先在本地作用域中查询是否有以这个名字命名的一些东西。因为存在,它们就不再检查别的作用域了。在这种情况中,someFunc的x是double类型,而global的x是int类型,这没有关系。C++的名字隐藏规则会对其进行处理:隐藏名字。不管与名字对应的是相同还是不同的类型都没有关系。在这种情况中,被命名为x的double隐藏了命名为x的int。

2. 编译器是如何在继承体系中寻找名字的

现在进入继承。我们知道当我们处在一个派生类成员函数内部时,并且引用了一些基类的东西(例如,一个成员函数,一个typedef或者一个数据成员),编译器能够找到我们所引用的东西,因为派生类继承了声明在基类中的这些东西。实际的工作方式是派生类的作用域被镶嵌在基类作用域内部。举个例子:

这个类中既有public的名字也有private的名字,既有数据成员也有成员函数。成员函数包括纯虚函数,普通虚函数(非纯虚函数)以及非虚函数。这是为了强调我们要讨论的是名字而不是别的。例子中也能包含类型名称,例如,枚举,嵌套类和typedefs。在这次的讨论中我们唯一关心的是他们是名字。他们是什么样的类型无关紧要。这个例子使用的是单继承,但是一旦你明白了在单继承下会发生什么,多继承下的C++行为很容易就能够预推测出来。

假设派生类中的mf4定义如下:

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

当编译器看到这里使用了名字mf2,它们必须理解mf2指向的是什么。它们会在作用域中寻找名字为mf2的一个声明。首先他们在local作用域中寻找,但是发现没有任何名字为mf4的声明。接下来寻找包含(containing)作用域,也就是类Derived。它们仍然没有发现mf2,所以它们进入了下一个包含作用域,也就是基类。在这里找到了名字为mf2的声明,搜索结束了。如果在基类中没有mf2,搜索会继续进行,首先在包含Derived的namespace(s)中寻找,最后在全局作用域内寻找。

我们刚刚描述的流程是准确的,但是对于名字如何在C++中被发现来说是不全面的。我们的目标不是为了了解足够的名字寻找规则然后实现一个编译器,然而,我们应该了解足够的规则来避免碰到一些让人感到意外的事情,对这个工作,我们已经了解了大量的信息了。

3. 继承体系中的名字是如何被隐藏的

考虑前面的例子,这次我们除了要重载mf1和mf3之外,也在Derived中添加一个mf3版本。(正如Item36中解释的,Derived中mf3的声明——一个继承而来的非virtual函数——会让这个设计看起来很可疑,但是为了理解继承下的名字可见性,我们忽略这个问题。)

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

这段代码产生的行为会让每个首次碰到这种情况的C++程序员都感到吃惊。基于作用域的名字隐藏规则并没有变化,所以基类中的名字为mf1和mf3的所有函数被派生类中相同名字的函数隐藏掉了。从名字搜寻的角度来看,Base::mf1和Base::mf3不再被Derived继承了!

 Derived d;
int x;
...
d.mf1(); // fine, calls Derived::mf1
d.mf1(x); // error! Derived::mf1 hides Base::mf1 d.mf2(); // fine, calls Base::mf2 d.mf3(); // fine, calls Derived::mf3 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声明来达到这个目的:

现在的继承就按照你所期望的进行工作了:

 Derived d;
int x;
...
d.mf1(); // still fine, still calls Derived::mf1
d.mf1(x); // now okay, calls Base::mf1 d.mf2(); // still fine, still calls Base::mf2 d.mf3(); // fine, calls Derived::mf3 d.mf3(x); // now okay, calls Base::mf3 (The int x is
// implicitly converted to a double so that
// 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函数:

 class Base {
public:
virtual void mf1() = ;
virtual void mf1(int); ... // as before };
class Derived: private Base {
public:
virtual void mf1() // forwarding function; implicitly
{ Base::mf1(); } // inline — see Item 30. (For info
... // on calling a pure virtual
}; // function, see Item 34.)
...
Derived d;
int x;
d.mf1(); // fine, calls Derived::mf1
d.mf1(x); // error! Base::mf1() is hidden

另外一种使用inline forwarding函数的地方是在使用比较老的编译器的时候,它们不支持通过using 声明来将继承而来的名字导入到派生类作用域中。

这就是关于继承和名字隐藏的全部故事,但是当继承同模板结合起来的时候,一个完全不同的“继承而来的名字被隐藏”问题就会出现,详情见 Item 43。

5. 总结

    • 派生类中的名字会将基类中的名字隐藏掉。在公共继承中,这绝不是令人满意的。
    • 为了让隐藏起来的名字重见天日,使用using声明或者forwarding函数来达到目的。

读书笔记 effective C++ Item 33 避免隐藏继承而来的名字的更多相关文章

  1. effective C++ Item 33 避免隐藏继承而来的名字

    1. 普通作用域中的隐藏 名字实际上和继承没有关系.有关系的是作用域.我们都知道像下面的代码: int x; // global variable void someFunc() { double x ...

  2. 读书笔记 effective c++ Item 32 确保public继承建立“is-a”模型

    1. 何为public继承的”is-a”关系 在C++面向对象准则中最重要的准则是:public继承意味着“is-a”.记住这个准则. 如果你实现一个类D(derived)public继承自类B(ba ...

  3. 读书笔记 effective c++ Item 34 区分接口继承和实现继承

    看上去最为简单的(public)继承的概念由两个单独部分组成:函数接口的继承和函数模板继承.这两种继承之间的区别同本书介绍部分讨论的函数声明和函数定义之间的区别完全对应. 1. 类函数的三种实现 作为 ...

  4. 读书笔记 effective c++ Item 43 了解如何访问模板化基类中的名字

    1. 问题的引入——派生类不会发现模板基类中的名字 假设我们需要写一个应用,使用它可以为不同的公司发送消息.消息可以以加密或者明文(未加密)的方式被发送.如果在编译阶段我们有足够的信息来确定哪个信息会 ...

  5. 读书笔记 effective c++ Item 53 关注编译器发出的警告

    许多程序员常常忽略编译器发出的警告.毕竟,如果问题很严重,它才将会变成一个error,不是么?相对来说,这个想法可能在其它语言是无害的,但是在C++中,我敢打赌编译器的实现者对于对接下来会发生什么比你 ...

  6. 读书笔记 effective c++ Item 52 如果你实现了placement new,你也要实现placement delete

    1. 调用普通版本的operator new抛出异常会发生什么? Placement new和placement delete不是C++动物园中最常遇到的猛兽,所以你不用担心你对它们不熟悉.当你像下面 ...

  7. 读书笔记 effective c++ Item 36 永远不要重新定义继承而来的非虚函数

    1. 为什么不要重新定义继承而来的非虚函数——实际论证 假设我告诉你一个类D public继承类B,在类B中定义了一个public成员函数mf.Mf的参数和返回类型并不重要,所以假设它们都是void. ...

  8. 读书笔记 effective c++ Item 15 在资源管理类中提供对原生(raw)资源的访问

    1.为什么需要访问资源管理类中的原生资源  资源管理类是很奇妙的.它们是防止资源泄漏的堡垒,没有资源泄漏发生是设计良好的系统的一个基本特征.在一个完美的世界中,你需要依赖这样的类来同资源进行交互,绝不 ...

  9. 读书笔记 effective c++ Item 21 当你必须返回一个对象的时候,不要尝试返回引用

    1. 问题的提出:要求函数返回对象时,可以返回引用么? 一旦程序员理解了按值传递有可能存在效率问题之后(Item 20),许多人都成了十字军战士,决心清除所有隐藏的按值传递所引起的开销.对纯净的按引用 ...

随机推荐

  1. Powerful array CodeForces - 86D(莫队)

    给你n个数,m次询问,Ks为区间内s的数目,求区间[L,R]之间所有Ks*Ks*s的和.1<=n,m<=200000.1<=s<=10^6 #include <iostr ...

  2. hdu 5638 Toposort (拓扑排序+线段树)

    Toposort Time Limit: 10000/5000 MS (Java/Others)    Memory Limit: 131072/131072 K (Java/Others)Total ...

  3. Python画统计图

    https://blog.csdn.net/jenyzhang/article/details/52046372

  4. ZJOI2018外省选手酱油记Day1

    Day0 上午考试...又爆零了 下午讲完题后放假 然后就滚回去收拾行李准备去\(ZJ\) Day1 衢州?我怎么从来没听过这个地方..肯定是我见识少 下午 上高铁出发,\(3个小时\),看了一下电影 ...

  5. yii2 查询数据库语法

    $query0 = ImGroupUser::find()->where(['gid'=>'56680dfc60b215d62104a4d8'])->select('user_cli ...

  6. iPhone X 的原深感模组

    物理与数字世界正走向融合,我们每天醒来的时间.睡眠时长.心率和步数等数据都会被分享.上传并转化为分析数据.无处不自的 AI.互联互通和软件平台将改变用户对现实的感知. 2018 年的 CES 展(国际 ...

  7. DB2 Vs MySQL系列 | MySQL与DB2的数据类型对比

    随着MySQL数据库的应用越来越广泛,DB2向MySQL数据库的迁移需求也越来越多.进行数据库之间迁移的时候,首先遇到的并且也是最基本最重要的就是两种数据库数据类型之间的转换. 相关阅读: 从商用到开 ...

  8. BZOJ 3731: Gty的超级妹子树

    3731: Gty的超级妹子树 Time Limit: 7 Sec  Memory Limit: 32 MBSubmit: 346  Solved: 96[Submit][Status][Discus ...

  9. 九省LNOI2018退役记

    立个flag不会退役. Day 0: 水一发. 大连大学的键盘敲起来就跟敲纸似的. 膜拜要进队的gqh,yxd,sjq. (都进啦2333) (高斯消元,高原反应,分麾下治……) 给我这只弱鸡烧根香. ...

  10. uoj35 后缀排序

    题目链接:http://uoj.ac/problem/35 这是一道模板题. 读入一个长度为 n 的由小写英文字母组成的字符串,请把这个字符串的所有非空后缀按字典序从小到大排序,然后按顺序输出后缀的第 ...