[C++] 动态绑定和静态绑定
你不应该重新定义一个继承而来的non-virtual函数。
为继承而来的virtual函数,重新定义缺省参数值,也很糟糕。
因为,virtual函数系动态绑定(dynamically bound),
而缺省参数值却是静态绑定(statically bound)。
1. 静态类型
对象的所谓静态类型(static type),就是它在程序中被声明时所采用的类型,
考虑以下的class继承体系:
// 一个用以描述几何形状的class
class Shape{
public:
enum ShapeColor { Red, Green, Blue };
// 所有形状都必须提供一个函数,用来绘制出自己
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;
};
现在考虑这些指针:
// 以下三个指针ps,pc,pr的静态类型,都是Shape*
Shape* ps;
Shape* pc = new Circle;
Shape* pr = new Rectangle;
2. 动态类型
对象的所谓动态类型(dynamic type)则指的是“目前所指对象的类型”,
也就是说,动态类型可以表现出一个对象将会有什么行为。
以上例而言,pc的动态类型是Circle*
,pr的动态类型是Rectangle*
,ps没有动态类型,因为它尚未指向任何对象。
动态类型一如其名称所示,可在程序执行过程中改变(通常是经由赋值动作):
// ps的动态类型如今是Circle*
ps = pc;
// ps的动态类型如今是Rectangle*
ps = pr;
virtual函数系动态绑定而来,意思是调用一个virtual函数时,究竟调用哪一份函数实现代码,
取决于发出调用的那个对象的动态类型。
// 调用Circle::draw(Shape::Red)
pc->draw(Shape::Red);
// 调用Rectangle::draw(Shape::Red)
pr->draw(Shape::Red);
3. 缺省参数值
但是当你考虑带有缺省参数值的virtual函数,花样来了,
virtual函数是动态绑定,而缺省参数值却是静态绑定。
意思是,你可能会在“调用一个定义于derived class内的virtual函数”的同时,却使用base class为它所指定的缺省参数值。
// 调用Rectangle::draw(Shape::Red)
pr->draw();
此例之中,pr的动态类型是Rectangle*
,所以调用的是Rectangle
的virtual函数,一如你所预期,
Rectangle::draw
函数的缺省参数值应该是Green
,但由于pr的静态类型是Shape*
,
所以,此一调用的缺省参数值来自Shape class而非Rectangle class。
结局是这个函数调用有着奇怪并且几乎绝对没人预料得到的组合,由Shape class和Rectangle class的draw声明式各出一半力。
以上事实不只局限于“ps,pc和pr都是指针”的情况,即使把指针换成reference问题仍然存在。
重点在于draw是个virtual函数,而它的缺省参数值在derived class中被重新定义了。
这一切还好,但如果你试着遵守这条规则,并且同时提供缺省参数值给base和derived class的用户,又会发生什么事呢?
class Rectangle: public Shape{
public:
virtual void draw(ShapeColor color = Red) const;
...
};
喔欧,代码重复。更糟糕的是,代码重复又带有相依性(with dependencies)。
如果Shape内的缺省参数值改变了,所有的“重复给定缺省参数值”的那些derived class也必须改变,
否则它们最终会导致“重复定义一个继承而来的缺省参数值”。
怎么办?
4. non-virtual interface
当你想令virtual函数表现出你所想要的行为但却遭遇麻烦,聪明的做法是考虑替代设计。
其中之一是NVI(non-virtual interface)手法:
令base class内的一个public non-virtual函数调用private virtual函数,后者可被derived class重新定义。
这里我们可以让non-virtual函数指定缺省参数,而private virtual函数负责真正的工作。
class Shape{
public:
enum ShapeColor { Red, Green, Blue };
// 如今它是non-virtual
void draw(ShapeColor color = Red) const {
// 调用一个virtual
doDraw(color);
}
...
private:
// 真正的工作在此处完成
virtual void doDraw(ShapeColor color) const = 0;
};
class Rectangle: public Shape{
public:
...
public:
// 注意,不须制定缺省参数值
virtual void doDraw(ShapeColor color) const;
};
由于non-virtual函数绝对不应该被derived class覆写,这个设计很清楚的使用draw函数的color缺省值总是为Red。