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. 【刷题】BZOJ 3667 Rabin-Miller算法

    Input 第一行:CAS,代表数据组数(不大于350),以下CAS行,每行一个数字,保证在64位长整形范围内,并且没有负数.你需要对于每个数字:第一,检验是否是质数,是质数就输出Prime 第二,如 ...

  2. 手机H5显示一像素的细线

    手机屏幕分辨率的问题,导致h5的1像素看起来比较粗,网上找了一个办法,记下来 主要就是通过scale来缩小宽度 .line1px{     border: none;     border-botto ...

  3. BZOJ 1007 水平可见直线 | 计算几何

    BZOJ 1007 水平可见直线 题面 平面直角坐标系上有一些直线,请求出在纵坐标无限大处能看到哪些直线. 题解 将所有直线按照斜率排序(平行的直线只保留最高的直线),维护一个栈,当当前直线与栈顶直线 ...

  4. USACO Section 1.5 Prime Palindromes 解题报告

    题目 题目描述 题目就是给定一个区间[a,b]((5 <= a < b <= 100,000,000)),我们需要找到这个区间内所有既是回文串又是素数的数字. 输入样例 5 500 ...

  5. 【BZOJ4709】【Jsoi2011】柠檬

    Description 传送门 题意简述:将序列划分成任意多段,从每一段选出一个数\(x\),获得\(在这一段出现的次数x*(x在这一段出现的次数)\)的贡献.求总贡献最大值. Solution ​ ...

  6. url参数中有+、空格、=、%、&、#等特殊符号的问题解决

    url出现了有+,空格,/,?,%,#,&,=等特殊符号的时候,可能在服务器端无法获得正确的参数值,如何是好?解决办法将这些字符转化成服务器可以识别的字符,对应关系如下:URL字符转义 用其它 ...

  7. PHP正则表达式基本语法

    本章主要学习正则表达式的基本语法: 正则表达式就是一个匹配的模式,正则表达式本身也就是一个字符串(有一些语法规则,特殊符号组成) 正则表达式这个字符串一定要在对应的函数中使用才有意义(分割,替换函数结 ...

  8. Redis学习基础三

    回顾: 上一基础上浅尝了redis的存储数据类型,这一节将分别介绍数据类型的基础使用 一.启动本地Redis服务 1.打开cmd 窗口 使用 cd 命令切换至redis 安装根目录 运行: redis ...

  9. kaggle竞赛

    sklearn实战-乳腺癌细胞数据挖掘(博客主亲自录制视频教程) https://study.163.com/course/introduction.htm?courseId=1005269003&a ...

  10. JavaScript中的apply()和call()

    可以将call()和apply()看做是某个对象的方法,通过调用方法的形式来间接调用函数. call()和apply()的第一个实参是要调用函数的母对象,它是调用上下文,在函数体内通过this来获得对 ...