最近开始看《Effective C++》,为了方便以后回顾,特意做了笔记。若本人对书中的知识点理解有误的话,望请指正!!!
我们可以将本条款的讨论局限于“继承一个带有默认参数的 虚函数”,而本条款成立的理由非常简单:虚函数 是动态绑定(又名后期绑定),而默认参数值却是静态绑定(又名前期绑定)。
对象的所谓静态类型,就是它在程序中被声明使所采用的类型。看下面的 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;
...
};
//静态类型都为 Shape* 。注意,不论它们真正指向什么,它们的静态类型都是 Shape*
Shape* ps;
Shape* pc = new Circle;
Shape* pr = new Rectangle;
对象的所谓动态类型,就是目前所指对象的类型。也就是说,动态类型可以表现出一个对象将会有什么行为。上述例子中,pc、pr 的动态类型分别是 Circle*
、 Rectangle*
,而 ps 没有动态类型,因为它没有指向任何对象。
动态类型如其名所示,可在程序执行过程中改变(通常是经由赋值动作)
ps = pc; //ps的动态类型如今是 Circle*
ps = pr; //ps的动态类型如今是 Rectangle*
虚函数 是动态绑定的,所以调用一个 虚函数 时,究竟调用哪个函数取决于发出调用的那个对象的 动态类型:
pc->draw(Shape::Red); //调用 Circle::draw(Shape::Red)
pr->draw(Shape::Red); //调用 Rectangle::draw(Shape::Red)
这些 虚函数 的性质我们都有所了解。但是当遇到带有默认参数值的 虚函数 时,会怎样呢?因为 虚函数 是动态绑定,而默认参数值是静态绑定,则在你调用一个定义于 派生类 的 虚函数时,使用的却是 基类 中指定的默认参数值:
pr->draw(); //调用 Rectangle::draw(Shape::Red)!但在Rectangle定义的draw的默认参数是Shape::GREEN
Rectangle::draw
函数的默认参数值应该是 GREEN,但由于 pr 的静态类型是 Shape*
,所以此一调用的默认参数值来自于基类 Shape
中 draw 的默认参数 Shape::Red
。
以上情况不只局限于 “ps,pc 和 pr 都是指针” 的情况,即使把指针换成引用,问题仍然存在。重点在于 draw
是个 虚函数 ,而它有个默认参数值在 派生类 中被重新定义了。
为什么 C++ 坚持默认参数值是静态绑定呢?答案在于运行期效率。因为当默认参数值是动态绑定时,编译器就需要在运行期为 虚函数 决定适当的参数默认值,这无疑会复杂且低效。
如果我们提供默认的参数值给 派生类 及 基类,又会怎样呢?
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
内的默认参数值改变了,则“重复规定默认参数值”的那些 派生类 也必须改变,否则它们最终会导致“重复定义一个继承而来的默认参数值”。怎么办呢?
当你想用 虚函数 表现出你期望的行为却遭遇麻烦,聪明的做法是考虑替代设计(见条款35)。例如使用 NVI 手法,令基类内的一个 public 普通函数调用 private 虚函数,后者可被 派生类 重新定义。这里我们让 普通函数 指定默认参数,private 虚函数 负责真正的工作:
class Shape {
public:
enum ShapeColor { Red, Green, Blue };
void draw(ShapeColor color = Red) const //普通函数指定默认参数值
{
doDraw(color); //调用虚函数
}
...
private:
virtual void doDraw(ShapeColor color) const = 0; //真正的工作在该函数完成
};
class Rectangle : public Shape {
public:
...
private:
virtual void doDraw(ShapeColor color) const;
...
};
因为条款36中说过,普通函数绝对不被派生类重写,所以 draw 函数的 color 默认值总是 Red。