《Effective C++》:条款36-条款37

条款36:绝不重新定义继承而来的non-virtual函数

以一个例子来展开本条款阐述内容。假设class D是class B的派生类,class B中有一个public成员函数mf:

class B{
public:
    void mf();
    ……
};

class D: public B {……};

由一下方式调用

D x;
B* pB=&x;
pB->mf();
D* pD=&x;
pD->mf();

上面的两次调用函数mf得到的行为相同吗?虽然mf是个non-virtual函数,但是如果class D中有自己定义的mf版本,那就行为真的不同。

class D: public B {
public:
    void mf();//遮掩了B::mf。见条款33
……};
pB->mf();//调用B::mf
pD->mf();//调用D::mf)。

之所以行为不一致,是因为non-virtual函数是静态绑定的(statically bound,条款 37)。pB被声明为一个pointer-to-B,通过pB调用的non-virtual函数永远是B所定义的版本。但是virtual函数是动态绑定(dynamically bound,条款 37),所以virtual函数不受这个约束,即通过指针调用,实际调用的函数是指针真正指向对象的那个函数。

如果你打算在class D中重新定义继承自class B的non-virtual函数,D对象很可能会出现行为不一致行径。更明确一点,即任何一个D对象都可能表现出B或D的行为;决定因素不在对象自身,而在于“指向该对象之指针”当初声明类型。References也会展现出和指针一样难以理解的行径。

前面已经说过,public继承是is-a 关系(条款 32)。**条款**34说过,class内声明一个non-virtual函数会为该class建立一个不变性(invariant),它凌驾其特异性(specialization)。将这两个观点施行到class B和class D上以及non-virtual函数B::mf上,那么

  • 适用于B对象的每一件事,也适用于D对象。(is-a 关系)
  • B的derived classes一定会继承mf的接口和实现,因为mf是一个non-virtual函数。

现在在D中重新定义mf,就会有矛盾。1、如果D真的有必要重新实现mf(不同于B的),那么is-a 关系就不成立,因为每个D都是B不再为真;既然这样,就不应该以public形式继承。2、如果D必须以public方式继承B,且D有需求实现不同的mf,那么久不能反映出不变性凌驾特异性;既然这样就应该声明为virtual函数。3、如果每个D是一个B为真,且mf真的可以反映出不变性凌驾特异性的性质,那么D久不需要重新定义mf了。

不论上面那个观点,结论都相同:任何情况下都不应该重新定义一个基础而来的non-virtual函数。

条款 7已经知道,base class内的析构函数应该是virtual;如果你违反了条款 7,你也就违反了本条款,因为析构函数每个class都有,即使你没有自己编写。

总结

  • 绝对不要重新定义继承而来的non-virtual函数。

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

在继承中,只能继承两种函数:virtual和non-virtual。在条款 36中我们学到,不能重新定义一个继承而来的non-virtual函数。本条款讨论的是继承virtual函数问题,再具体一点:继承一个带有缺省参数值的virtual函数。

我们应该知道,virtual函数是动态绑定(dynamically bound),缺省参数值却是静态绑定(statically bound)。

对象的静态类型(static 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;
    /*客户调用上面函数时,如果使用对象调用,必须指定参数值,因为静态绑定下这个函数不从base继承缺省值。*/
    /*如果使用指针或引用调用,可以不指定缺省参数值,动态绑定会从base继承缺省参数值*/
    ……
};

这个继承很简单。现在这样使用

Shape* ps;
Shape* pc=new Circle;
Shape* pr=new Rectangle;

这些指针类型都是pointer-to-Shape类型,都是静态类型Shape*。对象的动态类型是指“目前所指对象类型”。动态类型可以表现出一个对象将会有什么行为。pc动态类型是Circle*,pr动态类型是Rectangle*,ps没有动态类型(它没有指向任何对象)。动态类型可以在执行过程中改变,重新赋值可以改变动态类型。

virtual函数是动态绑定的,调用哪一份函数实现的代码,取决于调用的那个对象的动态类型。

pc->draw(Shape::Red);
pr->draw(Shape::Red);

这样调用无可非议,都带有参数值。但是如果不带参数值呢

pr->draw();//调用Rectangle::draw(Shape::Red)

上面调用中,pr动态类型是Rectangle*,所以调用Rectangle的virtual函数。Rectangle::draw函数缺省值是GREEN,但是pr是静态类型Shape*,所以这个调用的缺省参数值来自Shape class,不是Rectangle class。这次调用两个函数各出了一半的力。

C++之所以使用这么怪异的运作方式,是因为效率问题。如果缺省参数值动态绑定,编译器必须有某种办法在运行期为virtual函数决定适当的参数缺省值。这比目前实行的“在编译器决定”的机制更慢且更复杂。为了执行速度和编译器实现上的简易度,C++做了这样的取舍。

我们尝试遵守这个规则,给base class和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;
    ……
};

这样问题又来了,代码重复且带着相依性(with dependencies):如果Shape内缺省参数值改变了,那么derived classes的缺省参数值也要改变,否则就会导致重复定义一个继承而来的缺省参数值。

当时如果的确需要derived classes的缺省参数值,那么就需要替代方法。条款 35列出了一些virtual函数的替代方法,例如NVI手法:

class Shape{
public:
    enum ShapeColor{ Red, Green, Blue};
    void draw(ShapeColor=Red) const
    {
        doDraw(color);
    }
    ……
private:
    virtual void doDraw(ShapeColor color) const=0;//真正在这里完成工作
};
class Rectangle: public Shape{
public:
    ……
private:
    virtual void draw(ShapeColor color) const;
    ……
};

因为non-virtual函数不会被derived覆写(条款 36),这个设计很清楚的使得draw函数的color缺省参数值总是Red。

总结

  • 不要重新定义一个继承而来的缺省参数值,因为缺省参数值是静态绑定,而virtual函数(你唯一应该覆写的东西)是动态绑定。

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。