1. 为什么不要重新定义继承而来的非虚函数——实际论证
假设我告诉你一个类D public继承类B,在类B中定义了一个public成员函数mf。Mf的参数和返回类型并不重要,所以假设它们都是void。实现如下:
class B {
public:
void mf();
...
};
lass D: public B { ... }
我们不需要了解B,D或者mf的任何细节,考虑一个类型D的对象x,
D x; // x is an object of type D
你会感到很吃惊,如果下面的语句:
B *pB = &x; // get pointer to x pB->mf(); // call mf through pointer
同下面的语句行为不一样
D *pD = &x; // get pointer to x pD->mf(); // call mf through pointer
因为两种情况中你都触发了对象x的成员函数mf。因为两种情况使用了相同的对象和相同的函数,行为就应该是相同的,不是么?
应该是这样。但是可能也不是这样。特别的,如果mf是非虚函数并且D定义了自己的mf版本的情况下,上面的行为会不一样:
class D: public B {
public:
void mf(); // hides B::mf; see Item 33 ... };
pB->mf(); // calls B::mf pD->mf(); // calls D::mf
这种两面性行为的原因是像B::mf和D::mf这样的非虚函数都是静态绑定的(statically bound Item
37)。这意味着因为pB被声明为指向B的指针,通过pB触发的非虚函数都会是定义在类B上的函数,即使pB指向的是B的派生类对象,也就是这个例子中所实现的那样。
而虚函数是动态绑定的(dynamically bound
Item 37),所以它们不会有上面的问题。如果mf是一个虚函数,无论通过pB或者pD对mf进行调用都会触发D::mf,因为pB或者pD真正指向的是类型D的对象。
如果你实现一个类D并且重新定义继承自类B的非虚函数mf,D对象也会表现出不一致的行为。特别的,通过D对象调用mf函数的行为可能会像B也可能像D,而对象本身并不是决定因素,决定因素是指向D对象的指针类型。引用所展示出来的行为会同指针一样。
2. 为什么不要重新定义继承而来的非虚函数——理论论证
上面只是实际论证。我知道你真正想了解的是“不要重新定义继承而来的非虚函数”的理论证明。看下面的分析:
Item 32解释了pulibc继承意味着”is-a”,Item34中描述了为什么在一个类中声明一个非虚函数就是为这个类建立了一个特化上的不变性(invariant over
specialization)。如果你将这些观察应用到类B或者D以及非虚成员函数B::mf上,你会发现:
- 应用在类B上的任何事情同样能够应用在类D上,因为每个D对象都是一个B对象
- 派生自类B的类同时继承了mf的接口和实现,因为mf是B中的非虚函数。
现在,如果D重新定义了mf,你的设计就会出现矛盾。如果D真的想实现一个不同于B的mf,并且如果每个B对象——无论怎么特化——真的必须为mf使用B的实现,每个D对象都是一个B对象就不是真的了。在这种情况下,D不应该public继承自B。从另外一个方面,如果D真的必须public继承自B,并且D真的需要实现一个不同于B的mf,那么mf为B反应出来的特化上的不变性就不再为真。在这种情况下,mf应该是virtual的。最后,如果每个D真的是一个B,并且如果mf真的为B反应出了特化上的不变性,那么D真的不需要重新定义mf,也不应该尝试去这么做。
无论哪个观点,结论都相同,禁止重新定义一个继承而来的非虚函数。
3. 你应该对此条款似曾相识
如果阅读这个条款的时候给你一种似曾相识的感觉,可能是因为你已经读过Item7了,这个条款解释了为什么多态基类中的虚函数应该为virtual的。如果你违反了Item7中的条款(也就是你在多态基类中声明一个非虚析构函数),你也会违反这个条款,因为派生类总是会重新定义继承而来的非虚函数:基类的析构函数。这对于没有定义析构函数的派生类来说也为真,因为正如Item 5中所解释的,如果你自己没有声明析构函数,编译器会为你自动生成一个。从本质上来说,Item7只是这个条款的一个特例而已,尽管它足够重要到单独成为一个条款。
4. 总结
- 永远不要重新定义一个继承而来的非虚函数。