决不要重新定义继承而来的非虚函数
假设类D公有继承于类B,并且类B中定义了一个公有成员函数mf。mf的参数和返回类型不重要,所以假设都为void。
class B
{
public:
void mf();
};
class D: public B
{
};
int main()
{
D x;//x是类型D的一个对象
B *pB = &x;//得到x的指针
pB->mf();//通过指针调用mf
//和下面这么做的执行行为不一样;
D *pD = &x;// 得到x的指针
pD->mf();//通过指针调用mf
return 0;
}
两种情况下调用的都是对象x的成员函数mf,两种情况下都是相同的函数和相同的对象,所以行为会相同,对吗?
对,会相同!
但,也许不会相同。特别是,如果mf是非虚函数而D又定义了自己的mf版本,行为就不会相同:
class B
{
public:
void mf();
};
class D: public B
{
public:
void mf();//隐藏了B::mf;就近原则
};
int main()
{
D x;
B *pB = &x;
pB->mf();//调用B::mf,就近原则
D *pD = &x;
pD->mf();//调用D::mf,就近原则
return 0;
}
行为的两面性产生的原因在于,像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对象,因为每个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,也就决不能这样做。
所以任何条件下,我们都要禁止重新定义继承而来的非虚函数。
决不要重新定义继承而来的缺省参数值
缺省参数只能作为函数的一部分而存在;另外,只有两种函数可以继承:虚函数和非虚函数。因此,重定义缺省参数值的唯一方法是重定义一个继承而来的函数。然而,重定义继承而来的非虚函数是一种错误(上面刚讲过),所以,我们完全可以把讨论的范围缩小为 "继承一个有缺省参数值的虚函数" 的情况。
但是,虚函数是动态绑定而缺省参数值是静态绑定的!
对象的静态类型是指你声明的存在于程序代码文本中的类型。看下面这个类层次结构:
enum ShapeColor {RED,GREEN,BLUE };
// 一个表示几何形状的类
class Shape
{
public:
// 所有的形状都要提供一个函数绘制它们本身
virtual void draw(ShapeColor color = RED) const = 0;
};
class Rectangle: public Shape
{
public:
// 注意:定义了不同的缺省参数值 ---- 不好!
virtual void draw(ShapeColor color = GREEN) const;
};
class Circle: public Shape
{
public:
virtual void draw(ShapeColor color) const;
};
用图形来表示是下面这样:
Shape
/\
/ \
/ \
Rectangle Circle
现在看看这些指针:
Shape *ps;//静态类型 = Shape*
Shape *pc = new Circle;//静态类型 = Shape*
Shape *pr = new Rectangle;//静态类型 = Shape*
ps, pc,和pr都被声明为Shape指针类型,所以它们都以此作为自己的静态类型。注意,这和它们真的所指向的对象的类型绝对没有关系 ---- 它们的静态类型总是Shape* 。
对象的动态类型是由它当前所指的对象的类型决定的。
即,对象的动态类型表示它将执行何种行为。上面的例子中,pc的动态类型是Circle*,pr的动态类型是Rectangle*。至于ps,实际上没有动态类型,因为它还没有指向任何对象。
动态类型,顾名思义,可以在程序运行时改变,典型的方法是通过赋值:
ps = pc;// ps的动态类型
// 现在是Circle*
ps = pr;// ps的动态类型
// 现在是Rectangle*
虚函数是动态绑定的,意思是说,虚函数通过哪个对象被调用,具体被调用的函数就由那个对象的动态类型决定:
pc->draw(RED);// 调用Circle::draw(RED)
pr->draw(RED);// 调用Rectangle::draw(RED)
但是,将虚函数和缺省参数值结合起来分析就会产生问题,因为,如上所述,虚函数是动态绑定的,但缺省参数是静态绑定的。这意味着你最终可能调用的是一个定义在派生类,但使用了基类中的缺省参数值的虚函数:
pr->draw();// 调用Rectangle::draw(RED)!
这种情况下,pr的动态类型是Rectangle*,所以Rectangle的虚函数被调用,正如我们所期望的那样。Rectangle::draw中,缺省参数值是GREEN。但是,由于pr的静态类型是Shape*,这个函数调用的参数值是从Shape类中取得的,而不是Rectangle类!所以结果将十分奇怪并且出人意料,因为这个调用包含了Shape和Rectangle类中Draw的声明的组合。
不用说,ps, pc,和pr都是指针的事实和产生问题的原因无关。如果它们是引用,问题也会继续存在。问题仅仅出在,draw是一个虚函数,并且它的一个缺省参数在子类中被重新定义了。
为什么C++坚持这种有违常规的做法呢?
答案和运行效率有关。如果缺省参数值被动态绑定,编译器就必须想办法为虚函数在运行时确定合适的缺省值,这将比现在采用的在编译阶段确定缺省值的机制更慢更复杂。做出这种选择是想求得速度上的提高和实现上的简便,所以大家现在才能感受得到程序运行的高效!