洛译小筑

别来无恙,我的老友…
随笔 - 45, 文章 - 0, 评论 - 172, 引用 - 0
数据加载中……

[ECPP读书笔记 条目33] 避免隐藏继承而来的名字

莎士比亚对于“名字”有着独特的见解。“名字意味着什么?玫瑰不叫玫瑰,依然芬芳如故。”大师还写道:“倘若有人偷窃了我的好名字……事实上会让我变得一贫如洗。”让这两段名句引领我们去探究C++中继承的名字。

事实上,本节讨论的问题与继承并没有太大关系。它仅仅关系到作用域。我们都能读懂下面的代码:

int x;                             // 全局变量

 

void someFunc()

{

  double x;                        // 局部变量

 

  std::cin >> x;                   // 读一个新值赋给局部变量x

}

为x赋值的语句是关于局部变量x的,而不是全局变量x,这是因为内部作用域隐藏了(“遮挡了”)外部作用域的名字。我们可以将这种域间状况用下图描述:


当编译器执行至someFunc的作用域内并且遇到名字x时,它将在局部作用域内查找,以便确认此处是否包含与x这个名字相关的操作。因为如果有的话,编译器就不会再去检查其它任何作用域了。在这上面的示例中,someFunc中的xdouble类型的,全局变量xint类型的,但是这无关紧要,C++的名字隐藏准则只会做一件事情:隐藏名字。至于名字相关的类型是否一致并不重要。本例中,double类型的x隐藏了int类型的x

引入继承。我们知道当我们在一个派生类的成员函数中企图引用基类的某些内容(比如成员函数、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();

 

  ...

};


本示例中同时存在公共的、私有的名字,另外同时包含了数据成员和成员函数的名字。成员函数还包括纯虚函数、简单虚函数(非纯虚的)和非虚函数。这就是向大家强调,我们此处讨论的中心话题就是名字。示例中还可以添加类型的名字,比如枚举类型、嵌套类以及预定义类型的名字。这里讨论的核心是:它们都是名字,而它们是为哪些东西命名的并不重要。示例中使用了单一继承结构,然而一旦你了解了C++中单一继承的行为方式之后,多重继承的行为也就不难推断了。

假定继承类中mf4是这样实现的(部分内容):

void Derived::mf4()

  ...

  mf2();

  ...

}

当编译器看到这个函数中使用了mf2这个名字,它就能够找到mf2的出处。编译器是这样做到的:它通过搜寻名字为mf2的那处声明所在的作用域。首先它在本地作用域(也就是mf4以内)查找,但是没有找到任何名字为mf2的声明。随后编译器搜寻当前包含它的域,也就是Derived类的作用域。仍然没有找到,于是又转向搜索上一层作用域,也就是基类。在这里编译器终于找到了名叫mf2的东西,于是搜索结束。如果Base类中依然没有mf2,那么搜索仍会继续,从包含Base的名字空间开始,到全局作用域为止。

虽然我刚刚描述的查找过程是精确的,但是其对于C++中名字查找机制的描述依然没有做到面面俱到。所幸我们的目标并不是对名字查找机制刨根问底从而去编写一个编译器。我们的目标是避免恼人的意外发生,针对这一点,我们掌握的信息已经足够了。

请再次考虑上面的示例,这次我们做一些小的改动:为mf1mf3个添加一个重载版本,并且在Derived中为mf3添加一个新版本。(如条目36所讲,Derived中重载版本的mf3(一个继承而来的非虚函数)将会使这样的设计存在无法避免的潜在危险,但是在此问题的焦点是继承下名字的可见性,我们暂且忽略这一问题。)

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();

  ...

};


这段代码的行为将会使每个乍看到它的C++程序员吃上一惊。由于基于作用域的名字隐藏机制并没有改变,因此基类中所有名叫mf1mf3的函数都被派生类中的mf1mf3所隐藏。从名字查找的角度看,Base::mf1Base::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

就像你所看到的,即使同一函数在基类和派生类中的参数表不同,基类中该函数依然会被隐藏,而且这一结论不会因函数是否为虚函数而改变。在本条目最开端的示例中,someFunc中的double x隐藏了全局的int x,此处的情况类似,Derived类中的函数mf3也会将Base类中名叫mf3但类型不同的函数隐藏起来。

C++这一特性的理论基础是:可以防止一类继承意外的发生,那就是当你为一个库或应用框架创建一个新的派生类时,你可能会去继承远族基类中的重载版本。遗憾的是,我们通常情况下恰恰希望这么做。事实上,如果你使用公共继承,但不继承重载的元素,那么就有悖于公共继承的一项基本原则——基类和派生类之间是“Derived是一个Base”关系(见条目32)。既然如此,你就需要时时刻刻重载C++默认情况下隐藏的继承而来的名字。

这一工作通过使用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;       // 让基类中所有名为mf1mf3的东西

  using Base::mf3;       // Derived的作用域中可见(并且是公有的)

 

  virtual void mf1();

  void mf3();

  void mf4();

  ...

};


现在,继承将按部就班进行:

Derived d;

int x;

 

...

 

d.mf1();               // 依然正常,依然调用Derived::mf1

d.mf1(x);              // 现在可以了,调用了Base::mf1

 

d.mf2();               // 依然正常,依然调用Derived::mf1

 

d.mf3();               // 正常,调用Derived::mf3

d.mf3(x);              // 现在可以了,调用了Base::mf3

                       // (此处的xint隐式转换为double

                       // 从而使Base::mf3的调用合法。)

这意味着如果你继承一个包含重载函数的基类,并且你仅期望对其中一部分进行重定义或重载,你就应该为每一个不期望被隐藏的名字添加一条using声明。如果你不这样做,一些你希望继承下来的名字将可能被隐藏。

不难想象,某些场合你可能不想把基类中所有的函数继承下来。但是在公共继承体系下这是无论如何不可行的,再次声明,这是违背公共继承“Derived是一个Base”关系的。(这也是为什么上文中using声明要置于派生类中的公共元素部分:因为基类中公有的名字在公共派生类中必须是公有的。)然而在私有继承体系下(参见条目39),这种不完全继承在某些情况下是有意义的。比如,假设Derived类私有继承自Base,并且Derived只希望继承mf1不包含参数的那个版本。using声明在此就不会奏效了,因为它将使该名字所代表的所有继承版本的函数在派生类中可见。在这种情况下可以使用另一种技术,我们称之为“转发函数”:

class Base {

public:

  virtual void mf1() = 0;

  virtual void mf1(int);

 

  ...                              // 同上

};

 

class Derived: private Base {

public:

  virtual void mf1()               // 转发函数

  { Base::mf1(); }                 // 隐式内联(参见条目30

  ...                              // (关于对纯虚函数的调用,请参见条目34

};

 

...

 

Derived d;

int x;

 

d.mf1();                           // 正常,调用Derived::mf1

d.mf1(x);                          // 错误!Base::mf1()被隐藏了

内联转发函数的另一个用途是:在使用古老的编译器时,它们通常不支持使用using声明来为继承类的作用域引入继承的名字(这实际上是编译器的缺陷)。此时可以使用内联转发函数。

以上是继承和名字隐藏的全部内容,但是如果继承涉及模板,那么我们将以另一种形式面对“继承而来的名字被隐藏”这一问题。对于模板继承的全部细节,请参见条目43。

时刻牢记

派生类中的名字会将基类中的名字隐藏起来。在公共继承体系下,这是我们永远不希望见到的。

为了让被隐藏名字再次可见,可以使用using声明或者转发函数。

posted on 2008-05-01 01:11 ★ROY★ 阅读(2631) 评论(2)  编辑 收藏 引用 所属分类: Effective C++

评论

# re: 【读书笔记】[Effective C++第3版][第33条] 防止隐藏继承的名字  回复  更多评论   

哈哈,是的,但是C#中不会影藏!
2008-05-04 08:56 | 梦在天涯

# re: 【读书笔记】[Effective C++第3版][第33条] 防止隐藏继承的名字  回复  更多评论   

“ l 派生类中的名字会将基类中的名字隐藏起来。在公有继承体系下,这是我们所不希望见到的。”

---------------------------
使用PCLINT应该可以检查出来吧~~建议大家写完代码林特林特~~
2008-05-10 10:27 | zhang某人

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理