Effective C++条款37:继承与面向对象——绝不重新定义继承而来的缺省参数值

前言

  • 在条款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;

};

Effective C++条款37:继承与面向对象——绝不重新定义继承而来的缺省参数值

  • 现在我们定义下面的代码,它们都被声明为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;

};

  1. 低效率的原因:
    • ①代码重复
    • ②依赖性太高,如果基类中的缺省参数值改变了,那么需要将派生类中的缺省参数值都修改一遍

以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函数——你唯一应该覆盖的东西——却是动态绑定
上一篇:python 图像处理(11):基本图形的绘制


下一篇:小程序canvas生成图片压缩和失真问题draw失效问题汇总