Effective C++ 学习笔记 条款33 避免遮掩继承来的名称

莎士比亚对名字颇有感触。他问道:"名字算什么?玫瑰不叫玫瑰,依然芬芳如故。"这位诗人也写道:"窃取我名声之人......真使我变得贫穷。"言之有理。这引出了 C++ 中继承来的名称。

实际上,这个问题与继承无关,而是与作用域有关。我们都知道,在下面这段代码中:

读入 x 的语句引用的是局部变量 x,而非全局变量 x,因为内层作用域中的名称会隐藏("遮蔽")外层作用域中的名称。我们可以把作用域情况可视化如下:

当编译器处于 someFunc 的作用域中,遇到名称 x 时,会先在局部作用域中查找是否存在该名称。因为有,就不会再去检查其他作用域。在这个例子中,someFunc 的 x 类型是 double,而全局的 x 类型是 int,但这并不影响结果------C++ 的名称隐藏规则就是隐藏名称,无论这些名称对应的类型是否相同。在这个例子中,一个名为 x 的 double 隐藏了一个名为 x 的 int。

现在引入继承。我们知道,当我们在派生类成员函数内部引用基类中的某些东西(比如成员函数、typedef 或数据成员)时,编译器能找到我们所引用的对象,因为派生类继承了基类中声明的那些东西。其实际运作方式是:派生类的作用域嵌套在基类的作用域之内。例如:

在这个例子中,既有 public 和 private 名称,也有数据成员和成员函数的名称。成员函数包括纯虚函数、普通虚函数和非虚函数------这样安排是为了强调我们讨论的是名称。这个例子本来也可以包含类型名称,比如枚举、嵌套类和 typedef。在我们的讨论中,唯一重要的是它们是名称,至于这些名称具体是什么,则无关紧要。例子使用的是单继承,但一旦你理解了单继承下的运作机制,C++ 在多重继承下的行为也就不难推断了。

假设派生类中的 mf4 有一部分实现如下:

当编译器看到这里用到了名称 mf2 时,必须搞清楚它指的是什么。做法是在各作用域中搜索名为 mf2 的声明。首先查找局部作用域(即 mf4 的作用域),但没有找到任何名为 mf2 的声明。接着查找外围作用域,即 Derived 类的作用域,仍然没有。再往外查找基类的作用域,这次找到了名为 mf2 的东西,于是搜索停止。如果 Base 中也没有 mf2,搜索会继续,先进入包含 Base 的命名空间(如果有的话),最后到达全局作用域。

我刚才描述的查找过程是准确的,但并不是 C++ 中名称查找机制的完整全貌。不过,我们的目标并不是要懂到能写一个编译器的程度,而是足够了解以避免一些不必要的麻烦------就这个目的而言,我们已经掌握了足够的信息。

再回到前面的例子,但这次我们让 mf1 和 mf3 有重载版本,并且在 Derived 中也加一个 mf3 的版本(条款36指出,在派生类中重载继承来的非虚函数 mf3 会立刻让这个设计显得可疑,但为了理解继承下名称可见性的问题,我们先暂且忽略这一点):

这段代码的行为,会让每个C++程序员在第一次遇到时都感到惊讶。基于作用域的名称隐藏规则并没有改变------基类中所有名为 mf1 和 mf3 的函数,都被派生类中名为 mf1 和 mf3 的函数隐藏了。从名称查找的角度来看,Base::mf1 和 Base::mf3 不再被 Derived 继承!

如你所见,即使基类和派生类中的函数接受不同的参数类型,这一规则仍然适用;无论函数是虚函数还是非虚函数,也同样如此。就像本条款开头那样,函数 someFunc 中的 double x 隐藏了全局作用域中的 int x------此处 Derived 中的函数 mf3 也隐藏了基类中一个同名但类型不同的函数 mf3。

这种行为背后的设计初衷,是为了防止你在库或应用程序框架中创建新的派生类时,意外地从遥远的基类中继承了重载函数。然而,遗憾的是,你通常恰恰希望继承这些重载版本。事实上,如果你使用的是 public 继承,却没有继承这些重载函数,就违反了条款32所阐述的基类与派生类之间的 is-a 关系,而该关系正是 public 继承的基石。既然如此,你几乎总是需要覆盖 C++ 对继承名称的默认隐藏行为。

你可以通过 using 声明来实现这一点:

现在继承的行为就符合预期了:

这意味着,如果你从带有重载函数的基类继承,并且只打算重新定义或覆盖其中一部分,你就需要为每一个你本来会被隐藏的名称加上 using 声明。如果不这样做,一些你希望继承的名称就会被隐藏。

有时候,你也许并不想继承基类中的所有函数。在 public 继承下,这种情况本不该发生------因为那样做会再次违反 public 继承所要求的基类与派生类之间的 is-a 关系(这就是为什么上面的 using 声明放在了派生类的 public 区域:基类中为 public 的名称,在 public 派生的派生类中也应为 public)。但在 private 继承(参见条款39)下,这样做可能就有意义了。例如,假设 Derived 以 private 方式继承自 Base,而 Derived 只希望继承 mf1 中那个无参版本。这时 using 声明就派不上用场了,因为它会把所有同名函数都引入派生类的作用域。这种情况下,需要改用另一种技术:简单的转发函数。

内联转发函数的另一个用途,是应对那些古老编译器(它们的 using 声明不支持将继承名称导入派生类作用域)的情况。

以上就是关于继承与名称隐藏的全部内容了。但当继承与模板结合时,还会出现另一种形式的"继承名称被隐藏"问题。关于模板中涉及尖括号的那些细节,请参见条款43。

切记:

1.派生类中的名称会隐藏基类中的名称。在 public 继承下,这从来都不是我们想要的。

2.要让被隐藏的名称重新可见,可以使用 using 声明或转发函数。

相关推荐
Go Work17 分钟前
docker 占满根分区时先分清谁在吃盘
经验分享·笔记
汉克老师40 分钟前
《从C++小白到算法高手:GESP 1—8级成长地图》第一讲:认识GESP——从编程新手到算法探索者
c++·gesp·信息学竞赛·小学生学c++
3095566661 小时前
股票接口返回 404 与空数组的排查笔记:证书状态与级别参数
java·笔记·python·ios·iphone·股票数据api·股票数据接口
蒸蒸yyyyzwd1 小时前
从邓明学习的后端笔记
笔记·学习
岑梓铭2 小时前
《考研408数据结构》第八章(排序)复习笔记
数据结构·笔记
机器学习之心2 小时前
基于变量相关图与残差学习的 GCN 多变量回归方法——混凝土 28 天抗压强度预测
学习·回归·gcn
晴天的雨.9922 小时前
【C++】继承篇
java·开发语言·c++
青少儿编程课堂2 小时前
蓝桥杯青少组图形化 + 全国青少年信息素养大赛 + 电子学会图形化考级:校园大富翁经营闯关解析
c++·python·算法·bfs·信息学竞赛
another heaven2 小时前
【C/C++ 第一次跨越(上):用 C++11/14 停止制造新的旧代码】
c语言·c++·制造
qq_406981032 小时前
c++ lamda(三)Lambda表达式
java·前端·c++