Effective C++读书笔记(24)

来源:互联网 发布:java飞机大战源码 编辑:程序博客网 时间:2024/05/21 22:25

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

Never redefine an inherited non-virtualfunction

有如下代码:

class B {
public:
    void mf();
    ...
};
class D: public B { ... };

虽然我们对B,D或mf一无所知,但面对一个类型为D的对象x:

D x;

B *pB =&x; // 获得一个指针指向x
pB->mf(); // 经由该指针调用mf

D *pD =&x; // 获得一个指针指向x
pD->mf(); // 经由该指针调用mf

两种情况中,pB和pD都调用了对象x中的成员函数mf。因为同样的函数和同样的对象,它们的行为应该相同。是的,但是也可能不。更明确地说,如果 mf 是non-virtual而 D 定义了它自己的mf版本:

class D:public B {
public:
    void mf(); // hides B::mf
   ...
};

pB->mf();// calls B::mf
pD->mf(); // calls D::mf

造成这一两面行为的原因是,non-virtual函数如B::mf 和D::mf都是staticallybound(静态绑定)的。这就意味着因为 pB被声明为pointer-to-B类型,所以通过pB调用的non-virtual函数永远是B所定义的版本,即使pB指向一个类型为B的派生类的对象,一如本例。

在另一方面,virtual函数是 dynamically bound(动态绑定)的,所以它们不会发生这个问题。如果 mf是一个 virtual函数,无论通过pB还是pD调用mf都将导致D::mf的调用,因为 pB 和 pD真正指向的都是一个类型为D的对象。

如果你在编写class D且重定义了一个从class B继承到的non-virtual函数mf,D 的对象将很可能表现出不一致的行为。更明确地说,当mf被调用时,任何一个D对象的行为既可能像B也可能像D,而且决定因素与对象本身无关,但是和指向它的指针声明类型有关。引用也会展现和指针一样难以理解的行为。

再从理论上分析,public inheritance意味着 is-a,在类中声明一个non-virtual函数是为这个类建立起一个不变性,凌驾于特异性。因此:

l   每一件适用于B对象的事情也适用于D对象,因为每一个D对象都 is-a(是一个)D对象;

l   B的派生类一定会继承mf的接口和实现,因为mf是B的non-virtual函数。

如果你要重新定义继承而来的non-virtual函数,则原本基类中的函数可以定义为virtual以求得不同的实现;而既然你是以public方式继承了基类,且那个函数不是virtual,就说明那个“不变性凌驾于特异性”的性质,即派生类可以直接使用基类中的那个non-virtual函数,且这样做是符合需求的设计。

我们以前已经解释了为什么多态基类中的析构函数应该是virtual。如果你违反了那个准则(如在一个多态基类中声明一个non-virtual析构函数),你也同时违反了本条款,因为派生类绝不应该重新定义一个继承而来的non-virtual函数(此处指基类的析构函数)。甚至对于没有声明析构函数的派生类,这也是成立的,因为如果你没有定义你自己的析构函数,编译器就会为你生成一个。

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

 

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

Never redefine a function’s inheriteddefault parameter value

因为重新定义一个继承而来的non-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;//缺省参数值与基类不同!
    ...
};

classCircle: public Shape {
public:
    virtual void draw(ShapeColor color)const;
    /*以对象调用此函数一定要指定参数值(静态绑定下此函数不从基类继承缺省参数值),                 但以指针或引用调用此函数可以不指定参数值(动态绑定下此函数会从基类继承缺省参数值)*/
    ...
};

现在考虑这些指针:

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

在本例中,ps,pc 和 pr全被声明为 pointer-to-Shape类型,所以它们全都以此作为它们的静态类型,无论它们真正指向什么。

对象的所谓动态类型,是指目前所指对象的类型。也就是说,动态类型可以表现出一个对象将会有什么行为。在上面的例子中,pc的动态类型是 Circle*,pr的动态类型是Rectangle*。至于ps,它没有一个实际的动态类型,因为它尚未指向任何对象。动态类型,就像它的名字所暗示的,能在程序执行过程中改变(通常是经由赋值动作):

ps = pc;// ps的动态类型如今是Circle*
ps = pr; // ps的动态类型如今是Rectangle*

virtual函数系动态绑定而来,意味着当调用一个virtual函数时,究竟调用哪一份函数实现代码,取决于发出调用的那个对象的动态类型:

pc->draw(Shape::Red);// calls Circle::draw(Shape::Red)
pr->draw(Shape::Red); // calls Rectangle::draw(Shape::Red)

 

当考虑带有缺省参数值的virtual函数时,因为virtual函数是动态绑定的,但缺省参数是静态绑定的。这就意味着你可能在调用一个定义于派生类内的virtual函数的同时(动态),却使用了基类为它指定的缺省参数值(动态)。

pr->draw();// calls Rectangle::draw(Shape::Red)!

此例中,pr的动态类型是Rectangle*,Rectangle的virtual函数被调用,在 Rectangle::draw中缺省参数值是Green。然而pr的静态类型是 Shape*,这个函数调用的缺省参数值是从Shape中取得的,而不是Rectangle!结果就是一个调用由Shape和 Rectangle两个类中的draw声明式混合组成。

如果是引用,问题依然会存在。重点在于draw是一个virtual函数,而它的一个缺省参数值在派生类中被重定义。

C++坚持这种乖张的方式来运作的原因是为了运行时效率。如果缺省参数值是动态绑定的,编译器就必须提供一种方法在运行时确定适当的virtual函数参数缺省值,这比目前在编译期确定它们的机制更慢而且更复杂。最终的决定偏向了速度和实现简单这一边,而造成的结果就是享受高效运行的乐趣。

如果试着遵循本规则,为基类和派生类的用户提供同样的缺省参数值,又会造成代码重复。更糟的是,代码重复带来相依性:如果Shape 中的缺省参数值发生变化,所有重复给定缺省参数值的派生类必须同时变化。

当你要一个virtual函数按照你希望的方式运行有困难的时候,考虑可选的替代设计是很明智的。这里我们选择NVI(non-virtual interface idiom手法:令基类内的一个public non-virtual函数调用private virtual函数,后者可被派生类重新定义。这里我们用non-virtual函数指定缺省参数,而private virtual函数做实际的工作:

classShape {
public:
    enum ShapeColor { Red, Green, Blue };
    void draw(ShapeColor color = Red) const// now non-virtual
    { doDraw(color); } // call virtual
    ...
private:
    virtual void doDraw(ShapeColor color) const =0; // 真正工作在此处完成
};

classRectangle: public Shape {
public:
    ...
private:
    virtual void doDraw(ShapeColor color) const; // 不需指定缺省参数值
    ...
};

因为non-virtual函数绝不应该被派生类覆写,这个设计很清楚地使得draw的color缺省参数值总是Red。

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