读书笔记 effective C++ Item 33 避免隐藏继承而来的名字

简介: 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,因为内部作用域的名字将外部作用域的变量隐藏掉了。

 

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函数来达到目的。

 


作者: HarlanC

博客地址: http://www.cnblogs.com/harlanc/
个人博客: http://www.harlancn.me/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出, 原文链接

如果觉的博主写的可以,收到您的赞会是很大的动力,如果您觉的不好,您可以投反对票,但麻烦您留言写下问题在哪里,这样才能共同进步。谢谢!

目录
相关文章
|
13天前
|
安全 Java 编译器
C++进阶(1)——继承
本文系统讲解C++继承机制,涵盖继承定义、访问限定符、派生类默认成员函数、菱形虚拟继承原理及组合与继承对比,深入剖析其在代码复用与面向对象设计中的应用。
|
4月前
|
存储 安全 Java
c++--继承
c++作为面向对象的语言三大特点其中之一就是继承,那么继承到底有何奥妙呢?继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用,继承就是类方法的复用。
107 0
|
7月前
|
安全 C++
【c++】继承(继承的定义格式、赋值兼容转换、多继承、派生类默认成员函数规则、继承与友元、继承与静态成员)
本文深入探讨了C++中的继承机制,作为面向对象编程(OOP)的核心特性之一。继承通过允许派生类扩展基类的属性和方法,极大促进了代码复用,增强了代码的可维护性和可扩展性。文章详细介绍了继承的基本概念、定义格式、继承方式(public、protected、private)、赋值兼容转换、作用域问题、默认成员函数规则、继承与友元、静态成员、多继承及菱形继承问题,并对比了继承与组合的优缺点。最后总结指出,虽然继承提高了代码灵活性和复用率,但也带来了耦合度高的问题,建议在“has-a”和“is-a”关系同时存在时优先使用组合。
401 6
|
9月前
|
C++ 开发者
C++学习之继承
通过继承,C++可以实现代码重用、扩展类的功能并支持多态性。理解继承的类型、重写与重载、多重继承及其相关问题,对于掌握C++面向对象编程至关重要。希望本文能为您的C++学习和开发提供实用的指导。
143 16
|
9月前
|
编译器 数据安全/隐私保护 C++
【C++面向对象——继承与派生】派生类的应用(头歌实践教学平台习题)【合集】
本实验旨在学习类的继承关系、不同继承方式下的访问控制及利用虚基类解决二义性问题。主要内容包括: 1. **类的继承关系基础概念**:介绍继承的定义及声明派生类的语法。 2. **不同继承方式下对基类成员的访问控制**:详细说明`public`、`private`和`protected`继承方式对基类成员的访问权限影响。 3. **利用虚基类解决二义性问题**:解释多继承中可能出现的二义性及其解决方案——虚基类。 实验任务要求从`people`类派生出`student`、`teacher`、`graduate`和`TA`类,添加特定属性并测试这些类的功能。最终通过创建教师和助教实例,验证代码
195 5
|
11月前
|
编译器 C++ 开发者
【C++】继承
C++中的继承是面向对象编程的核心特性之一,允许派生类继承基类的属性和方法,实现代码复用和类的层次结构。继承有三种类型:公有、私有和受保护继承,每种类型决定了派生类如何访问基类成员。此外,继承还涉及构造函数、析构函数、拷贝构造函数和赋值运算符的调用规则,以及解决多继承带来的二义性和数据冗余问题的虚拟继承。在设计类时,应谨慎选择继承和组合,以降低耦合度并提高代码的可维护性。
190 1
【C++】继承
|
安全 编译器 程序员
C++的忠实粉丝-继承的热情(1)
C++的忠实粉丝-继承的热情(1)
107 0
|
8月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
4月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
110 0
|
4月前
|
存储 编译器 程序员
c++的类(附含explicit关键字,友元,内部类)
本文介绍了C++中类的核心概念与用法,涵盖封装、继承、多态三大特性。重点讲解了类的定义(`class`与`struct`)、访问限定符(`private`、`public`、`protected`)、类的作用域及成员函数的声明与定义分离。同时深入探讨了类的大小计算、`this`指针、默认成员函数(构造函数、析构函数、拷贝构造、赋值重载)以及运算符重载等内容。 文章还详细分析了`explicit`关键字的作用、静态成员(变量与函数)、友元(友元函数与友元类)的概念及其使用场景,并简要介绍了内部类的特性。
185 0