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

  • 条款36绝不重新定义继承而来的non-virtual函数
  • 条款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函数(你唯一应该覆写的东西)是动态绑定。
时间: 2024-12-22 07:16:09

《Effective C++》:条款36-条款37的相关文章

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

(一)首先有下面的继承体系: class B { public: void mf(); ... }; class D : public B {...}; D x; 以下行为: B* pB = &x; pB->mf(); 异于以下行为: D* pD = &x; pD->mf(); 上面两种行为产生的结果不一定相同.看下面这种情况: mf是个non-virtual函数而D定义有自己的mf版本: class D : public B { public: void mf(); ...

Effective C++读书笔记(条款35-40)

(六).继承与面向对象设计 ____________________________________________________________________________________________________________________________________ 条款35:考虑virtual函数以外的其他选择 #1.virual函数的四个替代方案: (1).使用non-virtual interface(NVI)手法,那是 Template Method 设计模式

effective c++学习笔记条款35-37

#include<iostream> using namespace std; class A { public: void asd() { pri(); } private: /*virtual*/ void pri() { cout << "基类函数" << endl; } }; class B :public A { private: void pri() /*override*/ { cout << "派生类函数&quo

effective C++ 读书笔记 条款36-37

条款36:绝不重新定义继承而来的non-virtual函数 重要点:non-virtual函数都是静态绑定 // 1241.cpp : 定义控制台应用程序的入口点. // #include "stdafx.h" #include <iostream> using namespace std; class Base { public: void func() { cout<<"Base::func()"<<endl; } }; cl

《Effective C++》:条款31:将文件间的编译依存关系降至最低

假如你在修改程序,只是修改了某个class的接口的实现,而且修改的是private部分.之后,你编译时,发现好多文件都被重新编译了.这种问题的发生,在于没有把"将接口从实现中分离".Class的定义不只是详细叙述class接口,还包括许多实现细目: class Person{ public: Person(const std::string& name, const Date& birthday, const Address& addr); std::strin

《Effective C++》:条款34:区分接口继承和实现继承

public继承的概念,由2部分构成:函数接口(function Interface)继承和函数实现(function implementation)继承.这两种继承的差异有点像函数的声明和函数的定义之间的差异. 我们在设计class时,有时希望derived class只继承函数的接口(即函数声明):有时候希望derived class继承函数接口和实现,但又覆写它们所继承的实现:又有时候希望derived class同时继承函数的接口和实现,但不覆写任何东西. 为了更好理解上述差异,用一个绘

《Effective C++》:条款48:理解力template 元编程

Template metaprogramming(TMP,模板元编程)这是写template-based C++规划.编译过程.template metaprogramming随着C++写模板程序,化的过程.也就是说,TMP程序运行后,从templates详细化出来C++源代码.不再是模板了. TMP有两个作用,一是它让某些事更easy. 比如编写STL容器,使用模板,可是存放不论什么类型元素.二是将运行在运行期的某些工作转移到了编译期.另一个结果是使用TMP的C++程序可能在其它方面更高效:较

Effective C++ 55个条款

Effective C++ 55个条款 让自己习惯C++ 视C++为一个语言联邦 C++高效编程守则视状况而变化,取决于你使用C++的哪一部分. 尽量以const,enums,inline替换#define 对于单纯常量,最好以const对象或enums替换#defines: 对于形似函数的宏(macros),最好改用inline函数替换#defines. 尽可能使用const 将某些东西声明为const可帮助编译器侦测出错误用法.const可被施加于任何作用域内的对象.函数参数.函数返回类型.

条款36: 区分接口继承和实现继承

作为类的设计者,有时希望派生类只继承成员函数的接口(声明):有时希望派生类同时继承函数的接口和实现,但允许派生类改写实现:有时则希望同时继承接口和实现,并且不允许派生类改写任何东西. class Shape { public: virtual void draw() const = 0; virtual void error(const string& msg); int objectID() const; ... }; class Rectangle: public Shape { ... }