代码改变世界

Effective C++ 学习笔记(22)

2011-08-07 13:17  Daniel Zheng  阅读(216)  评论(0编辑  收藏  举报

决不要重新定义继承而来的非虚函数


  假设类 D 公有继承于类B,并且类B 中定义了一个公有成员函数mf。mf的参数和返回类型不重要,所以假设都为void。换句话说,我这么写:

  

class B {
public:
  void mf();
  ...
};
class D: public B { ... };

  甚至对B,D 或mf 一无所知,也可以定义一个类型D 的对象x,

  

D x; // x 是类型D 的一个对象

  那么,如果发现这么做:

B *pB = &x; // 得到x 的指针
pB->mf(); // 通过指针调用mf

  和下面这么做的执行行为不一样:

D *pD = &x; // 得到x 的指针
pD->mf(); // 通过指针调用 mf

  你一定就会感到很惊奇。

  因为两种情况下调用的都是对象 x 的成员函数mf,因为两种情况下都是相同的函数和相同的对象,所以行为会相同,对吗?对,会相同。但,也许不会相同。特别是,如果mf 是非虚函数而D 又定义了自己的mf 版本,行为就不会相同:

  

class D: public B {
public:
void mf(); // 隐藏了B::mf; 参见条款50
...
};
pB
->mf(); // 调用B::mf
pD->mf(); // 调用 D::mf

  行为的两面性产生的原因在于,象 B::mf 和D::mf 这样的非虚函数是静态绑定的。这意味着,因为pB 被声明为指向B 的指针类型,通过pB 调用非虚函数时将总是调用那些定义在类B 中的函数 ---- 即使pB 指向的是从B 派生的类的对象,如上例所示。

  相反,虚函数是动态绑定的,因而不会产生这类问题。如果mf 是虚函数,通过pB 或pD 调用mf 时都将导致调用D::mf,因为pB 和pD 实际上指向的都是类型D 的对象。

  所以,结论是,如果写类 D 时重新定义了从类B 继承而来的非虚函数mf,D 的对象就可能表现出精神分裂症般的异常行为。也就是说,D 的对象在mf 被调用时,行为有可能象B,也有可能象D,决定因素和对象本身没有一点关系,而是取决于指向它的指针所声明的类型。引用也会和指针一样表现出这样的异常行为。

  

  公有继承的含义是 "是一个","在一个类中声明一个非虚函数实际上为这个类建立了一种特殊性上的不变性"。如果将这些分析套用到类B、类D 和非虚成员函数B::mf,那么,适用于 B 对象的一切也适用于D 对象,因为每个D 的对象 "是一个" B 的对象。

  B 的子类必须同时继承mf 的接口和实现,因为mf 在B 中是非虚函数。

  那么,如果 D 重新定义了mf,设计中就会产生矛盾。如果D 真的需要实现和B 不同的mf,而且每个B 的对象 ---- 无论怎么特殊 ---- 也真的要使用B实现的mf,那么,每个D 将不 "是一个" B。这种情况下,D 不能从B 公有继承。相反,如果D 真的必须从B 公有继承,而且D 真的需要和B 不同的mf 的实现,那么,mf 就没有为B 反映出特殊性上的不变性。这种情况下,mf 应该是虚函数。最后,如果每个D 真的 "是一个" B,并且如果mf 真的为B 建立了特殊性上的不变性,那么,D 实际上就不需要重新定义mf,也就决不能这样做。不管采用上面的哪一种论据都可以得出这样的结论:任何条件下都要禁止重新定义继承而来的非虚函数。