而来的名称)"/>
《Effective C++》学习笔记(条款33:避免遮掩继承而来的名称)
最近开始看《Effective C++》,为了方便以后回顾,特意做了笔记。若本人对书中的知识点理解有误的话,望请指正!!!
1 同名变量分别在全局作用域和函数作用域
这个题材和继承无关,而是和作用域(scope)有关,我们都知道在下面的代码中:
int x; // global变量
void someFunc()
{double x; // local变量std::cin>>x; // 读一个值,赋给local变量
}
这个读取数据的语句指涉的是函数中的局部变量 x,而不是全局变量 x,因为内层作用域的名称会遮掩外围作用域的名称。用矩形来表示上面代码的作用域:
当编译器处于 someFunc()
的作用域内并遭遇名称 x 时,它在局部作用域内查找是否有什么东西带着这个名称。如果找到,就不再查找这个作用域;如果找不到,向更大范围查找。
本例的 someFunc()
中 x 是 double 类型,但 global x 是 int类型,但那不要紧。C++的名称遮掩规则所做的唯一事情就是:遮掩名称。(对于名称的类型是否相同,并不重要)
2 继承体系中遇到同名变量
当位于一个派生类成员函数内指涉基类内的某物(也许是个员函数、typedef、成员变量)时,编译器可以找出我们指涉的东西,因为派生类继承了声明于基类的所有东西。实际是派生类作用域被嵌套在基类的作用域内,像这样:
class Base {
private:int x;
public:virtual void mf1() = 0;virtual void mf2();void mf3();...
};class Derived : public Base {
public:virtual void mf1();void mf4();...
};
这个例子中混合了 public 和 private 域内的名称,以及成员变量和成员函数,这些成员函数还包括纯虚、虚、普通函数三种,这是为了强调我们讨论的是名称,和其它无关。这个例子还可以加入其它各种类型(如 enum 、typedef等)。本例使用单一继承,一旦了解单一继承下发生的事,很容易推想C++在多重继承下的行为。假设 Derived
类的 mf4()
的实现代码部分是这样:
void Derived::mf4()
{...mf2();...
}
当编译器看到使用名称 mf2,必须要知道它指什么,有没有声明过,所以开始查各作用域:
- 查找local作用域(就是 mf4 所在函数的作用域),没有
- 查找外围作用域,
Derived
类的作用域,没有 - 再向外扩一轮,
Base
类的作用域,找到, 停止查找,就用它了。
如果在 Base
类的作用域,也没有查找到,会继续查找 Base
类所在的 namespace作用域,最后找到 global 作用域。
我们将例1的例子改一下,重载 mf1()
和 mf3()
,并添加一个新版 mf3()
到 Derived
。
会如同后面的条款36 所言,Derived
类重载了 mf3()
,但这是一个继承而来的non-virtual函数。
class Base {
private:int x;
public:virtual void mf1() = 0;virtual void mf1(int);virtual void mf2();void mf3();void mf3(double);...
};class Derived : public Base {
public:virtual void mf1();void mf3();void mf4();...
};
以作用域为基础的“名称遮掩规则”并没有改变,因此 Base
类所有名为 mf1 和 mf3 的函数都被 Derived
类的 mf1 和 mf3 函数都遮盖掉了。从名称查找观点来看, Base::mf1()
和 Base::mf3()
不再被 Derived
继承:
Derived d;
int x;
...
d.mf1(); // 没问题,调用Derived::mf1
d.mf1(x); // 错误! 因为Derived::mf1遮掩了Base::mf1
d.mf2(); // 没问题,调用Base::mf2
d.mf3(); // 没问题,调用Derived::mf3
d.mf3(x); // 错误! 因为Derived::mf3遮掩了Base::mf3
这个例子说明了,即使 Base
类和 Derived
类的函数有不同的参数类型也都适用,而且不论函数是virtual 或者 non-virtual 一体适用.
这些行为背后的理由基本理由是为了防止你在程序库或应用框架内建立新的 Derived
类时附带地从疏远的 Base
类继承重载函数。
不幸的是,你一直想继承重载函数。实际上 如果你正在适用public继承而又不继承重载函数,就是违反 Base
类和 Derived
类之间的 is-a 关系,而条款32说过 is-a 是 public 继承的基石。
你可以使用 using 来让基类的名称可见:
class Base {
private:int x;
public:virtual void mf1() = 0;virtual void mf1(int);virtual void mf2();void mf3();void mf3(double);...
};
class Derived : public Base {
public:using Base::mf1; // 让 Base class内名为mf1和mf3的所有东西在Derived 作用域都可见using Base::mf3;virtual void mf1();void mf3();void mf4();...
};
现在,继承机制如往常一样运作:
Derived d;
int x;
...
d.mf1(); // 仍然没问题,调用Derived::mf1
d.mf1(x); // 现在没问题了,调用Base::mf1
d.mf2(); // 仍然没问题,调用Base::mf2
d.mf3(); // 仍然没问题,调用Derived::mf3
d.mf3(x); // 现在没问题了,调用Base::mf3
这意味如果你继承Base
类并加上重载函数,而你又希望重新定义或覆写其中一部分,那么你必须为那些原本会被遮掩的每个名称引入一个 using 声明式,否则其他东西会被覆盖掉。
有时候,并不想继承 Base
类的所有函数,但在public 继承中,这绝对不可能发生,因为违反了 is-a 关系。然而在 private 继承中,它却可以发生。
例如,假设Derived
以 private 形式继承 Base
,而 Derived
唯一想继承的 mf1 是那个无参数版本。
using 声明式在这里排不上用场,因为 using 声明式会令继承而来的某给定名称之所有同名函数(即名称相同的所有函数,不理会返回值和参数列表)在 Derived
类中都可见。
所以,我们需要一个 转交函数(forwarding function):
class Base {
public:virtual void mf1() = 0;virtual void mf1(int);... // 与之前相同
};class Derived : private Base {
public:virtual void mf1() { // 转交函数,隐式成为inline(原因,见条款30)Base::mf1(); //即重写mf1的实现,并用作用域符号 :: 调用基类的mf1()}...
};...
Derived d;
int x;
d.mf1(); // 很好,调用的是Derived::mf1
d.mf1(x); // 错误! Base::mf1被遮掩了
inline 转交函数的另一个用途是为那些不支持 using 声明式的老旧编译器的一条新路,将继承而得的名称汇入Derived
类作用域内。
Note:
- 派生类 内的名称会遮掩基类 内的名称。在 public 继承下从来没人希望如此。
- 为了让被遮掩的名称再见天日,可使用 using声明式 或 转交函数。
条款34:区分接口继承和实现继承
更多推荐
《Effective C++》学习笔记(条款33:避免遮掩继承而来的名称)
发布评论