前言
- 在条款36介绍过,在继承体系中,派生类最好只重写覆盖virtual函数,而不要去隐藏基类的non-virtual函数
- 因此,本条款要介绍的“不要重新定义继承而来的缺省参数值”,是针对于virtual函数而言的
- 一个重要的概念:virtual函数是动态绑定的,而virtual函数的缺省参数值却是静态绑定的
一、静态类型、动态类型
- 静态类型:在被声明时所采用的的类型
- 动态类型:目前所知对象的类型
演示案例
- 下面是一个继承体系
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 = 0; }; class Circle :public Shape { public: virtual void draw(ShapeColor color)const = 0; };
- 现在我们定义下面的代码,它们都被声明为pinter-to-Shpae类型,因此它们不论它们指向什么,静态类型都是Shape*:
Shape* ps; //静态类型为Shape* Shape* pc = new Circle; //静态类型为Shape* Shape* pr = new Rectangle; //静态类型为Shape*
- 动态类型是指该该对象将会有什么行为。例如:
Shape* ps; Shape* pc = new Circle; Shape* pr = new Rectangle; ps = pc; //ps的动态类型如今是Circle* ps = pr; //ps的动态类型如今是Rectangle*
- 根据语法我们知道,对于virtual函数的调用,是根据其动态类型决定的。例如:
Shape* ps; Shape* pc = new Circle; Shape* pr = new Rectangle; pc->draw(Shape::Red); //调用Circle::draw(Shape::Red) pr->draw(Shape::Red); //调用Rectangle::draw(Shape::Red)
二、virtual函数的缺省参数值是静态绑定的
- 虽然对于virtual函数的调用时动态绑定的,但是对于virtual函数的缺省参数值却是静态绑定的
-
见下面的代码:
- 我们知道virtual函数是动态绑定的,pr的动态类型为Rectangle,所以调用的是Rectangle::draw()
- 但是virtual函数的缺省参数值是静态绑定的,在上面类的定义中Rectangle的draw()函数无参数,但是由于pr指针的静态类型是Shape,因此其draw()函数的缺省参数值就是Shape::draw()函数中的参数值,为Shape::Red
Shape* pr = new Rectangle;
pr->draw(); //调用的是Rectangle::draw(Shape::Red)
//解释:此一调用的缺省参数值来自class Shape而非class Rectangle
//Circle也是相同的道理
Shape* pc = new Circle;
pc->draw(); //调用的是Circle::draw(Shape::Red),而不是Circle::draw(Shape::Green)
- 为什么要设计这种行为:在于运行效率。如果缺省参数值也是动态绑定,编译器就必须有某种办法在运行期为virtual函数决定适当的参数缺省值,这比目前实行的“在编译期决定”的机制更慢而且更复杂
三、不要重新定义继承而来的缺省参数值
- 通过二,我们知道virtual函数的缺省参数值是静态绑定的。因此,我们不要重新定义继承而来的缺省参数值,因为这会在调用virtual函数时产生意想不到的效果(上面代码中,通过pc调用draw()就是一个例子)
四、针对于virtual函数的缺省参数值,给出的建议
先看一个效率低下的方案
- 为了保持基类与派生类中的一致性,一种低效率的方法是将基类和派生类中的virtual函数的缺省参数值设置为一致的
- 例如:
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 = Red)const; }; class Circle :public Shape { public: virtual void draw(ShapeColor color = Red)const; };
- 低效率的原因:
- ①代码重复
- ②依赖性太高,如果基类中的缺省参数值改变了,那么需要将派生类中的缺省参数值都修改一遍
以NVI手法定义class
- 条款36介绍了NVI手法,对于virutal函数的缺省参数值,为了避免基类与派生类中的缺省参数值不一致,我们可以采取这种方法
- 定义的代码如下:
class Shape { public: enum ShapeColor { Red, Green, Blue }; void draw(ShapeColor color = Red)const { //因为是non-virtual函数,因此不建议派生类隐藏 doDraw(Red); } private: //真正的工作在此处完成,派生类可以重写 virtual void doDraw(ShapeColor color)const = 0; }; class Rectangle :public Shape { private: virtual void doDraw(ShapeColor color)const = 0; }; class Circle :public Shape { public: virtual void draw(ShapeColor color)const = 0; };
- 上面的doDraw()函数完成真正的功能,并且接受一个ShapeColor类型
- 我们又定义了一个non-virtual函数draw,其参数默认为Red,并且non-virtual函数不建议派生类隐藏,因此不论是基类还是派生类调用draw()函数,参数的默认值将永远是Red,达到了我们最终的目的
五、总结
- 绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而virtual函数——你唯一应该覆盖的东西——却是动态绑定