Item 37:绝不重新定义继承而来的缺省参数值
Item 37:Never redefine a function's inherited default parameter value
本条款的讨论局限于:继承一个带有缺省参数值的virtual函数。
本条款成立的理由是:virtual函数是动态绑定(dynamically bound),而缺省参数却是静态绑定(statically bound)。
静态绑定又名前期绑定,early binding;动态绑定又名后期绑定,late binding。
1. 静态类型和动态类型
对象的所谓静态类型(static type),就是它在程序中被声明时所采用的类型。
对象的所谓动态类型(dynamic type)是指"目前所指对象的类型"。也就是说,动态类型可以表现出一个对象将会有什么行为。
举个例子:
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;
...
};
这个继承体系图如下:
继承关系现在考虑这些指针:
Shape* ps; //静态类型为Shape*
Shape* pc = new Circle; //静态类型为Shape*
Shape* pr = new Rectangle; //静态类型为Shape*
ps,pc和pr都被声明为pointer-to-Shape类型,所以它们都以Shape为静态类型。不论它们真正指向什么,它们的静态类型都是Shape*。
pc的动态类型是Circle,pr的动态类型是Rectangle。ps*没有动态类型,因为它尚未指向任何对象。
动态类型可在程序执行中改变(通常经由赋值动作):
ps = pc; //ps的动态类型如今是Circle*
ps = pr; //ps的动态类型如今是Rectangle*
virtual函数根据动态绑定而来,意思是调用一个virtual函数时,究竟调用哪一份实现代码,取决于发出调用那个对象的动态类型:
pc->draw(Shape::Red); //调用Circle::draw(Shape::Red)
pr->draw(Shape::Red); //调用Rectangle::draw(Shape::Red)
当然,这都是基础知识。你肯定已经掌握了。但是,上面已经提到过,virtual函数是动态绑定,而缺省参数值却是静态绑定。意思是你可能会在“调用一个定义于derived class内的virtual函数”的同时,却使用base class为它所指定的缺省参数值:
pr->draw( ); //调用Rectangle::draw(Shape::Red)
pr的动态类型是Rectangle,所以调用的是Rectangle的virtual函数,一如你所预期。Rectangle::draw函数的缺省参数值应该是GREEN,但由于pr的静态类型是Shape,所以调用的缺省参数值来自Shape class而非Rectangle class!结局是这个函数调用有着奇怪并且几乎没人预料得到的组合,由Shape class和Rectangle class的draw**声明时各出一半力。
由于书中作者给的这个例子,不是很明显,有点隐晦,可能理解上文有点吃力,但是我们换个例子代码:
class Shape {
public:
virtual void draw(int color = 0) const = 0;
};
class Rectangle: public Shape{
public:
virtual void draw(int color = 1) const
{
cout << "Rectangle class : " << color << endl;
}
};
class Circle: public Shape{
public:
virtual void draw(int color) const
{
cout << "Circle class : " << color << endl;
}
};
int main()
{
Shape* pc = new Circle; //静态类型为Shape*
Shape* pr = new Rectangle; //静态类型为Shape*
pc->draw(); //Circle class : 0
pr->draw(); //Rectangle class : 0
pc->draw(10); //Circle class : 10
pr->draw(10); //Rectangle class :10
return 0;
}
这下大家一定很清楚了吧,就不多解释了。
以上事实不只局限于“ps,pc和pr都是指针”的情况;即使把指针换成references问题仍然存在。
Circle c;
Rectangle r;
Shape &sc = c;
Shape &sr = r;
sc.draw(); //Circle class : 0
sr.draw(); //Rectangle class : 0
sc.draw(10); //Circle class : 10
sr.draw(10); //Rectangle class :10
为什么C++坚持以这种方式来运作呢?
答案在于运行期效率。如果缺省参数值是动态绑定,编译器就必须有某种方法在运行期为virtual函数决定适当的参数缺省值。这比目前实行的“在编译期决定”的机制更慢而且更复杂。为了程序的执行速度和编译器实现上的简易度,C++做了这样的取舍,其结果就是你如今所享受的执行效率。
代码重复问题
如果我们试着遵守本条规则,并且同时提供缺省参数给base和derived 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 = Red) const;
...
};
看起来是代码重复了,不会警告也不会报错,仅仅只是重复写了代码而已,更糟的是,代码重复又带着相依性:如果Shape内的缺省参数值改变了,所有“重复给定缺省参数值”的那些derived classes也必须改变,否则它们最终会导致“重复定义一个继承而来的缺省参数值”,与我们的本条规则的愿望相悖。应该怎么办呢?
当你想令virtual函数表现出你所想要的行为但却遭遇麻烦,聪明的做法是考虑替代设计。条款35列了不少virtual函数的替代设计,其中之一是NVI(non-virtual interface)手法:令base class内的一个public non-virtual函数调用private virtual函数,后者可被derived classes重新定义。这里我们可以让non-virtual函数指定缺省参数,而private virtual函数负责真正的工作:
class Shape {
public:
enum ShapeColor {Red, Green, Blue};
void draw(ShapeColor color = Red) const //如今它是non-virtual
{
doDraw(color); //调用一个virtual
}
...
private:
virtual void doDraw(ShapeColor color) const = 0; //真正的工作在此处完成
};
class Rectangle: public Shape{
public:
...
private:
virtual void doDraw(ShapeColor color) const; //注意,无须指定缺省参数值。
...
};
NOTE:
绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而virtual函数——你唯一应该覆写的东西——却是动态绑定的。
ps:本条款在《C++ Primer》第四版 中文版 第15.2.4节的第5点——“虚函数与默认实参”有讲解,P482。但是没有举例说明,所以可能不好理解,或者给人印象不深。
小问题:
#include <iostream>
#include <string>
using namespace std;
class Base {
public:
virtual ~Base(){}
virtual void display() const {
cout << "Base" << endl;
}
};
class Derive:public Base {
public:
virtual ~Derive(){}
virtual void display() {
cout << "Derive" << endl;
}
};
int main()
{
Base *pbase = new Derive();
pbase -> display();
return 0;
}
为啥输出?
Base
解答:
Effective C++这个问题其实不难,关键是你没有搞清楚 重载、隐藏、覆盖三者的区别,以及const的概念。
重载:要求函数名相同,形参列表不同,但要求两个函数必须在同一个作用域。你这道题目里的dispaly函数,一个在子类,一个在父类,显然不是在同一个作用域。故排除重载。
覆盖:发生在父子类之间,要求父类的函数有virtual修饰,同时对于返回类型是基本类型的函数来说,要构成覆盖,必须函数名、形参列表、返回值类型全部相同,你的父类里面的display后面有个一个const修饰,他修饰的是形参列表里的this指针,也就是dispaly(const this)。这样一写,你应该明白了吧,子类里的display应该是dispaly(),显然形参列表不相同,自然就构不成覆盖啊。
隐藏:发生在父子类之间,只要函数名相同就构成隐藏。
纵上所述,你的这两个函数构成的是隐藏关系,而不是覆盖,既然不是覆盖,那么何来多态呢?
所以运行结果肯定是Base。如果你想实现多态,有两种方法:
- 去掉父类display函数后面的const
- 在子类display函数后面加上const
网友评论