条款37: 决不要重新定义继承而来的非虚函数
有两种方法来看待这个问题:理论的方法和实践的方法。让我们先从实践的方法开始。毕竟,理论家一般都很耐心。
假设类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这样的非虚函数是静态绑定的(参见条款38)。这意味着,因为pB被声明为指向B的指针类型,通过pB调用非虚函数时将总是调用那些定义在类B中的函数 ---- 即使pB指向的是从B派生的类的对象,如上例所示。
相反,虚函数是动态绑定的(再次参见条款38),因而不会产生这类问题。如果mf是虚函数,通过pB或pD调用mf时都将导致调用D::mf,因为pB和pD实际上指向的都是类型D的对象。
所以,结论是,如果写类D时重新定义了从类B继承而来的非虚函数mf,D的对象就可能表现出精神分裂症般的异常行为。也就是说,D的对象在mf被调用时,行为有可能象B,也有可能象D,决定因素和对象本身没有一点关系,而是取决于指向它的指针所声明的类型。引用也会和指针一样表现出这样的异常行为。
实践方面的论据就说这么多。我知道你现在想知道的是,不能重新定义继承而来的非虚函数的理论依据是什么。我很高兴解答。
条款35解释了公有继承的含义是 "是一个",条款36说明了为什么 "在一个类中声明一个非虚函数实际上为这个类建立了一种特殊性上的不变性"。如果将这些分析套用到类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,也就决不能这样做。
不管采用上面的哪一种论据都可以得出这样的结论:任何条件下都要禁止重新定义继承而来的非虚函数。