之前在Effective C++看到有一个条款:决不要重新定义继承而来的非虚函数,今天在看公司的代码的时候,发现有人重新定义继承而来的非虚函数,所以重新查看了这个条款。
虽然这样做(重新定义继承而来的非虚函数)不会出什么大错,只要注意声明的指针即可。但是我觉得还是不合理。
先看例子。
我把Effective C++例子补全如下:
#include "stdafx.h"
#include "stdlib.h"
#include <iostream>
using namespace std;
class B
{
public:
void mf()
{
cout << "B::mf" << endl;
}
};
class D : public B
{
public:
void mf()
{
cout << "D::mf" << endl;
}
};
void ShowPoint(int n)
{
char szNum[33] = {0};
_itoa_s(n, szNum, 10);
cout << szNum << endl;
}
int _tmain(int argc, _TCHAR* argv[])
{
D x; // x是类型D的一个对象
B* pB = &x; // 得到x的指针
ShowPoint((int)pB); // 显示指针值
pB->mf(); // 通过指针pB调用mf
D* pD = &x; // 得到x的指针
ShowPoint((int)pD); // 显示指针值
pD->mf(); // 通过指针pD调用mf
system("pause");
return 0;
}
输出的结果:
2422915
B::mf
2422915
D::mf
可以看到,都是对象x的指针去调用mf,从上面也可以看到输出的指针值是相同的,但因为指针声明不同,同一个对象调用同一个函数,产生了不同的行为。
当把基类B的成员函数改成类函数时,即:
class B
{
public:
virtual void mf()
{
cout << "D::mf" << endl;
}
};
这时输出:
1900272
D::mf
1900272
D::mf
非虚函数是静态绑定:
同一个对象调用同一个函数,产生了不同的行为产生的原因在于,非虚函数是静态绑定的。因为pB被声明为指向B的指针类型,通过pB调用非虚函数时将总是调用那些定义在类B中的函数(即使pB指向的是从B派生的类的对象)。
虚函数是动态绑定:
虚函数是动态绑定的,因而不会产生这类问题。如果mf是虚函数,通过pB或pD调用mf时都将导致调用D::mf,因为pB和pD实际上指向的都是类型D的对象。
所以如果在实现类D时,对基类B非虚函数mf重写。当D的对象在调用mf时,行为有可能象B,也有可能象D,决定因素跟对象本身没有关系,反而是取决于指向它的指针所声明的类型。引用也会和指针一样表现出这样的异常行为。
下面摘自Effective C++
条款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,也就决不能这样做。
不管采用上面的哪一种论据都可以得出这样的结论:任何条件下都要禁止重新定义继承而来的非虚函数。