`
evasiu
  • 浏览: 165221 次
  • 性别: Icon_minigender_2
  • 来自: 广州
博客专栏
Fa47b089-e026-399c-b770-017349f619d5
TCP/IP详解卷一>阅读...
浏览量:12240
社区版块
存档分类
最新评论

effective c++ -- 继承与面向对象设计

 
阅读更多
继承与面向对象设计中,涉及了继承方式,即public、private及protected继承方式,继承体系中的屏蔽问题,成员函数virtual、non-virtual的选择以及多重继承等。

Item 32: 确定你的public继承塑模出is-a关系
以C++进行面向对象编程,最重要的一个规则是:public inheritance意味is-a的关系,于是,基类对象B可以派上用场的地方,继承类对象D一样可以派上用场。
类之间除了存在is-a关系外,还可能存在has-a和is-implementated-in-terms-of的关系。

[b]Item 33: 避免遮掩继承而来的名称
在继承体系下,C++名字查找规则如下:
(1)在函数内查找;
(2)在类内查找;
(3)在基类内查找;
(4)在命名空间内查找;
(5)在全局域内查找。
把(1)~(5)看成在不同域内查找,在每个域内,编译器首先查找所有名字匹配的变量、函数,如果是函数,还会找出其最佳匹配,只要名字在某一步中的领域内匹配了,编译器便不会再往更大的范围查找。编译器总是在匹配完后才检验其可用性,即共访问修饰符public, private等。

我们来看下面一个例子:
class Base{
  private:
    int x;
  public:
    virtual void mf1() = 0;
    virtual void mf1( int ) = 0;
    virtual void mf2();
    void mf3();
    void mf3( double );
    ...
};

class Derived : public Base{
  public:
    virtual void mf1();
    void mf3();
    void mf4();
    ...
};


下面是调用的一些情况:
Derived d;
int x;
...
d.mf1();	//OK,调用Derived::mf1
d.mf1(x);	//error, Derived::mf1()遮掩了Base::mf1
d.mf2();	//OK, 调用Base::mf2
d.mf3();	//OK, 调用Derived::mf3
d.mf3(x);	//error, Derived::mf3遮掩了Base:mf3

如果希望继承base class并加上重载函数,而又希望重新定义或覆写其中一部分,那么必须为那些原本会被遮掩的每个名称引入一个using声明式。如下:
class Derived : public Base{
  public:
    using Base::mf1;
    using Base::mf3;
    virtual void mf1();
    void mf3();
    void mf4();
    ...
};

有时候你并不想继承base class的所有函数,这是可以理解的。但是在public继承下,这绝对不可能发生。因为它违反了public继承所暗示的is-a关系。然而在private继承下它却可能是有意义的。假如Derived以private形式继承Base,而Derived唯一想继承的是那个无参数的mf1版本,可以用forwarding function(转交函数)来实现:
class Derived : private Base{
  public:
    virtual void mf1(){
      Base::mf1();	}	//函数转换,暗自成为inline函数
    void mf3();
    void mf4();
    ...
};

函数转交的另外一个用途是为那些不支持using声明式的老旧编译器另辟一条新路。

Item 34: 区别接口继承和实现继承
(1)Derived类对Base类的继承有几种方式:public、private和protected,它们只是改变其他类(包括派生类的派生类)对派生类中基类成员的访问方式。public派生类继承了基类的藏品,它具有与基类相同的接口,这种方式称为接口继承;private和protected成员相当于继承了基类的操作,但并不把它们开放给其他类用户,这种派生通常称为实现继承。
(2)声明一个pure virtual函数的目的是为了让Derived class只继承函数接口。不过,比较意外的是,我们也可以为pure virtual函数提供定义,不过调用它时需要明确指出其class名称。
(3)声明impure virtual函数的目的是让derived class继承该函数的接口和缺省的实现。
(4)声明non-virtual函数的目的是为了令derived class继承函数的接口及一份强制性实现。

Item 35: 考虑virtual函数以外的其他选择
virtual函数被用来使同一继承体系的不同对象在运行时选择适合的动作,有时候它是那么显而易见的设计方式,以至于我们忘了考虑其他的可能。这一节提供了两种设计模式,来代替可能本来显而易见的virtual函数。
(1)Non-virtual-interface手法实现Template Method模式
模板方法:在一个方法中定义一个算法的骨架,而这些步骤延迟到子类中去。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
NVI手法用public non-virtual成员函数包裹较低访问性的virtual函数。
(2)Strategy模式
一种方法是将virtual函数替换为“函数指针成员变量”,这种方法使得不同的对象实例(而不是对象)可以拥有不同的处理方法;
比较函数成员指针使用范围更广的是tr1::function,该类型产生的对象可以持有任何与其签名式兼容的可调用物。所谓兼容,是指这个可调用物的参数可被隐式转换为tr1::function指定的参数。可调用物包括函数指针,函数对象以及成员函数指针等。
古典的Strategy模式将继承体系内的virtual函数替换为另一继承体系内的virtual函数。

Item 36: 绝不重新定义继承而来的non-virtual函数
重新定义继承而来的non-virtual函数,会使我们前面提到的,public继承下,D是B的命题不再为真。

Item 37: 绝不重新定义继承而来的缺省参数值
重新定义继承而来的non-virtual函数是错误的。那么,而重新定义virtual函数是很自然的。但是,不要重新定义其缺省的参数值,因为:virtual函数是在运行期间动态绑定的,而缺省的参数值却是静态绑定的。也就是,如果基类的virtual函数有缺省参数,在以后不同派生类的该virtual函数都使用基类的缺省参数。
C++这么做的原因是在于运行期效率。如果缺省参数值是动态绑定,编译器就必须有某种方法在运行期为virtual函数决定适当的参数缺省值。这比目前实行的“在编译期决定”的机制更慢而且更复杂。

Item 38: 通过复合塑模出has-a或is-implemented-in-terms-of
当复合发生于应用域内的对象之间,表现出has-a的关系;当它发生于实现域则是表现is-implemented-in-terms-of。
下面分别是has-a跟is-implemented-in-terms-of的一个例子:
class Address { ... };
class PhoneNumber { ... };

class Person{
  public:
    ...
  private:
      std::string name;		//合成成份物,has-a的关系
      Address addr;
      PhoneNumber voiceNumber;
};

template<class T>
class Set{
  public:
    bool member( const T& item ) const;
    void insert( const T& item );
    void remove( const T& item );
    std::size_t size() const;
  private:
    std::list<T> rep;		//implement Set in terms of list
};


Item 39: 明智而审慎地使用private继承
我们知道,当使用public继承时,派生类D和基类B存在is-a的关系,在需要B类的场合中,编译器会自动将D类转换为B类。但是,这只是发生在public继承下,如果是private继承,将不会有这样的转换。
这是private继承的规则:(1)如上所述,编译器不会自动将一个D对象转换为一个B对象;(2)由private base class继承而来的所有成员,在derived class中都会变成private属性。
private继承意味着implemented-in-terms-of。如果你让D以private形式继承B,你的用意是为了采用B内已经备妥的某些特性,不是因为B对象和D对象存在有什么任何观念上的关系。private继承纯粹是一种实现技术。
而且,在需要implemented-in-terms-of时,尽可能使用复合,必要时才使用private继承。何时才是必要的?主要是当protected成员和/或virtual函数被牵进来的时候。
例如我们有一个Widget,它需要某种计时器,在一个周期到的时候执行某个动作,有这样一个计时器:
class Timer{
  public:
    explicit Timer( int tickFrequency );
    virtual void onTick() const;
    ...
};

可以以private形式继承Timer:
class Widget: private Timer{
  private:
    virtual void onTick() const;
    ...
};

但是下面的复合方式似乎更好:
class Widget{
  private:
    class WidgetTimer : public Timer{
      public:
	virtual void onTick() const;
	...
    };
    WidgetTimer timer;
    ...
};

为什么复合的方法比private继承要好呢?
首先,你可能希望让Widget有派生类,但是又不希望这个派生类重新定义onTick(),如果Widget继承自Timer,这样的想法就没办法实现。
其次,使用复合的方法使得我们可以使用pointer to implementation的方法来降低耦合度。
当一个意欲成为derived class者想访问一个意欲成为base class者的protected成分,或为了重新定义一个或多个virtual函数时,我们可能会选择private继承。
另有一种激进情况可能让我们选择使用private,那只适用于所处理的class不带任何数据。这样的class没有non-static成员变量,没有virtual函数(因为这种函数的存在会为每个对象带来一个vptr),也没有virtual base class(因为这样的base class也会招致何种上的额外开销)。于是这种empty class对象不使用任何空间,因为没有任何隶属对象的数据需要存储。但是C++裁定:凡是独立(非附属)对象都必须有非零大小,所以看下面的例子:
class Empty{ ... };	//没有数据,所以其对象不使用任何内存

class HoldsAnInt{
  private:
    int x;
    Empty e;
};

你会发现,sizeof( HoldsAnInt ) > sizeof( int )。因为Empty作为独立对象,必须有非零大小。
如果你这样实现HoldsAnInt:
class HoldsAnInt : private Empty{	//Empty是附属对象
  private:
    int x;
};

现在, sizeof( HoldsAnInt ) == sizeof( int );这就是所谓的Empty Base Optimization,EBO。
明智则审慎地使用private继承的意思是,在考虑过所有其他方案之后(如混合了public继承和复合的设计)仍然认为private继承是“表现程序内两个class之间的关系”的最佳办法,这才用它。

Item 40: 明智而审慎地使用多重继承
多重继承的意思是继承一个以上的base class,但这些class并不常在继承体系中又有更高级的base class。
有多重继承下,程序有可能从一个以上的base class继承相同名称,那会导致较多的歧义,为了解决这个歧义,你必须明白地指出你要调用哪个base class内的函数。
所谓的“钻石型多重继承”,是像下面这样的一种情况:
class File {
  public:
    ...
  private:
    std::string fileName;
    ...
};

class InputFile : public File{	... };
class OutputFile : public File{ ... };
class IOFile : public InputFile, public OutputFile{	... };

继承自File的InputFile和OutputFile将各自拥有一个成员变量fileName,那么IOFile内该有多少笔这个名称的数据呢?从某个角度说,IOFile从其每一个base class继承一份,所以其对象内应该有两份fileName成员变量;但是逻辑告诉我们,IOFile对象只该有一个fileName数据,所以它继承自两个base class而来的fileName不该重复。
默认情况下,IOFile将同时从InputFile和OutputFile继承fileName成员,也就是拥有两份fileName,除非显式声明virtual public继承。如下:
class InputFile: virtual public File { ... };
class OutputFile: virtual public File { ... };
class IOFile: public InputFile, public OutputFile { ... };

这样看来我们似乎应该总是用virtual public取代public,可是想一想为避免继承得来的成员变量重复,编译器必须提供若干幕后戏法,而其后果是:使用virtual继承的那些class所产生的对象往往比使用non-virtual继承的兄弟们大,访问virtual base class的成员变量时也比访问non-virtual base class的成员变量速度慢。
因此作者给出的关于virtual继承的忠告:(1)非必要不使用virtual base;(2)如果必须使用virtual base classes,尽可能避免在其中放置数据。
书中最后还用了一个例子来说明多重继承的正当用途,其中涉及“public继承某个interface class”和“private继承某个协助实现的class”的两相组合。

呵呵,感觉这一章总结得很潦草,其中很多想法,大概要到真正应用中,才能体会其艺术吧。
分享到:
评论

相关推荐

    Effective C++ 中文版

    当您读过《Effective C++中文版(第3版改善程序与设计的55个具体做法)》后,就获得了迅速提升自己C++功力的一个契机。  在国际上,本书所引起的反响,波及整个计算机技术出版领域,余音至今未绝。几乎在所有C++书籍...

    Effective C++

    5、继承与面向对象设计 条款35:使公有继承体现是一个的函义 条款36:区分接口继承与实现继承 条款37:绝不要重新定义继承而来的非虚函数 条款38:绝不要重新定义继承而来的缺省参数值 条款39:避免向下转换继承层次 条款...

    Effective C++中文版

    Effective C++中文版,包括C、C++内存管理,设计与声明,类和函数的实现,继承和面向对象的设计等

    Effective C++(第三版)

    6. 继承与面向对象设计 inheritance and object-oriented design 条款32:确定你的public继承塑模出is-a关系 make sure public inheritance models “is-a.” 条款33:避免遮掩继承而来的名称 avoid hiding ...

    Effective c++

    提高编程效率的50条建议 条款1:尽量用const和inline而不用#define 条款2:尽量用而不用 条款3:尽量用new和delete而不用malloc和free 内存管理的建议 设计与说明的建议 继承与面向对象的设计 杂项

    OOD启思录 高清pdf

    而《OOD启思录》被读者评价为“面向对象设计领域中的Effective C++”——正如Effective C++能帮助你迈向C++专家层面,《OOD启思录》能帮助你迈入OOD殿堂。 本书提供了改进面向对象设计的真知灼见。  全书共11章,...

    Effective.C++.中文第二版.50条款doc文档.chm

    第六章 继承和面向对象设计 条款35: 使公有继承体现 "是一个" 的含义 条款36: 区分接口继承和实现继承 条款37: 决不要重新定义继承而来的非虚函数 条款38: 决不要重新定义继承而来的缺省参数值 条款39: 避免 "向下...

    c++ 面向对象的类设计

    好像,more effective c++上说的,class的成员函数,应该是在完整的情况下保持最小化。但是,这里我们的出发点,是成员数据的完整最小化。 最小化的好处是可以保持概念最大的独立性,也意味着,可以用最小的代价来...

    C++书籍集合

    本书专注于C++面向对象程序设计的底层机制,包括结构式语意、临时性对象的生成、封装、继承,以及虚拟——虚拟函数和虚拟继承,帮助你理解程序的底层实现,以便写出更高效的代码。 《The design and evolution of ...

    程序员面试刷题的书哪个好-CPP_Learning:记录下C++语言学习

    面向对象编程 从C到C++ 封装 继承 多态 操作符重载 模版与泛型编程 输入输出 经典书籍 C++ Primer Effective三部曲 STL源码剖析 面试问题记录 from 牛客, 我现在的想法是C/C++方向,涉及到具体的知识有数据结构与...

    asp.net知识库

    .net 2.0 访问Oracle --与Sql Server的差异,注意事项,常见异常 Ado.net 与NHibernate的关系? 动态创建数据库 SQL Server数据库安全规划全攻略 .net通用数据库访问组件SQL Artisan应用简介1 在Framework1.0下...

Global site tag (gtag.js) - Google Analytics