您的位置:首页 > 其它

Item 37:绝不重新定义继承而来的缺省参数值

2016-04-08 10:01 471 查看


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*


pspcpr都被声明为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*,所以调用的是Rectanglevirtual函数,一如你所预期。Rectangle::draw函数的缺省参数值应该是GREEN,但由于pr的静态类型是Shape*,所以调用的缺省参数值来自Shape class而非Rectangle class!结局是这个函数调用有着奇怪并且几乎没人预料得到的组合,由Shape classRectangle classdraw声明时各出一半力。

由于书中作者给的这个例子,不是很明显,有点隐晦,可能理解上文有点吃力,但是我们换个例子代码:

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;
}


这下大家一定很清楚了吧,就不多解释了。

以上事实不只局限于“pspcpr都是指针”的情况;即使把指针换成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函数的替代设计,其中之一是NVInon-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

解答

这个问题其实不难,关键是你没有搞清楚 重载、隐藏、覆盖三者的区别,以及const的概念。

重载:要求函数名相同,形参列表不同,但要求两个函数必须在同一个作用域。你这道题目里的dispaly函数,一个在子类,一个在父类,显然不是在同一个作用域。故排除重载。

覆盖:发生在父子类之间,要求父类的函数有virtual修饰,同时对于返回类型是基本类型的函数来说,要构成覆盖,必须函数名、形参列表、返回值类型全部相同,你的父类里面的display后面有个一个const修饰,他修饰的是形参列表里的this指针,也就是dispaly(const this)。这样一写,你应该明白了吧,子类里的display应该是dispaly(),显然形参列表不相同,自然就构不成覆盖啊。

隐藏:发生在父子类之间,只要函数名相同就构成隐藏。

纵上所述,你的这两个函数构成的是隐藏关系,而不是覆盖,既然不是覆盖,那么何来多态呢?

所以运行结果肯定是Base。

如果你想实现多态,有两种方法:

- 去掉父类display函数后面的const

- 在子类display函数后面加上const

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: