继承与面向对象设计中,涉及了继承方式,即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”的两相组合。
呵呵,感觉这一章总结得很潦草,其中很多想法,大概要到真正应用中,才能体会其艺术吧。
分享到:
相关推荐
- **对象和类**:C++的核心在于面向对象编程,书中强调理解对象生命周期和构造/析构函数的重要性,以及如何正确地使用拷贝构造函数和赋值运算符。 - **作用域与生命周期**:讨论了局部变量、全局变量和静态变量的...
这两本书的内容都是基于C++标准库和面向对象编程原则,适合有一定C++基础的开发者阅读。通过学习和实践书中的建议,开发者可以编写出更符合C++语言精神的高质量代码,提升自己的编程技能。《Effective C++》和《More...
1. **面向对象设计原则**:书中强调了C++作为面向对象语言的核心设计原则,如封装、继承、多态和抽象。这些原则不仅指导如何编写更易于理解和维护的代码,也帮助开发者利用C++的强大功能来构建复杂的系统。 2. **...
- **面向对象编程原则**:书中可能介绍了如何在C++中实现封装、继承和多态等面向对象的基本概念。 - **命名空间与作用域**:C++中的命名空间如何使用,以及不同作用域规则对程序设计的影响。 - **模板和泛型编程**:...
2. 类与对象:深入讨论了C++中的面向对象编程(OOP)概念,包括类的定义、构造函数、析构函数、拷贝构造函数、赋值操作符等。 3. 高级特性:涵盖了模板编程、异常处理、STL容器的使用,以及智能指针等C++的高级特性...
- **对象与类**:C++是一种面向对象的语言,对象是类的实例,类定义了对象的状态(数据成员)和行为(成员函数)。 - **封装**:C++通过类来实现封装,隐藏内部实现细节,提供公共接口供外部使用。 - **继承与...
当您读过《Effective C++中文版(第3版改善程序与设计的55个具体做法)》后,就获得了迅速提升自己C++功力的一个契机。 在国际上,本书所引起的反响,波及整个计算机技术出版领域,余音至今未绝。几乎在所有C++书籍...
两本书都强调了面向对象设计的原则,如封装、继承和多态,并提倡使用RAII(Resource Acquisition Is Initialization)原则来有效管理内存和其他资源。它们还提醒开发者注意C++中的陷阱和常见错误,帮助避免潜在的...
9. **多态与继承**:C++的继承和多态是面向对象编程的核心,但滥用会导致设计复杂和性能损失。理解何时使用虚函数、纯虚函数以及接口类,以及何时应选择组合而非继承,是提升设计质量的关键。 10. **命名空间与作用...
对于C++的面向对象特性,如封装、继承和多态性,书中的讲解深入浅出,帮助读者理解如何设计和实现高效、可维护的C++程序。 《Effective C++》则更注重实践,书中列举了一系列的编程建议和技巧,帮助开发者写出更高...
1. **面向对象设计**:C++是一种支持面向对象编程的语言,其核心思想是封装、继承和多态。书中会讨论如何有效利用类和对象,以及如何设计良好的接口来实现模块化。 2. **智能指针**:在第三版中,Meyers可能会介绍...
3. 类的设计和继承:C++支持多种编程范式,包括面向对象编程。本书应该会讨论如何设计类,以及如何合理利用继承来创建类层次结构。同时也会探讨多态、虚函数和虚析构函数等概念。 4. 模板和STL(标准模板库):模板...
1. 视C++为语言联邦:Scott Meyers提出了一个概念,即C++是由若干个相对独立的语言子集组成的,包括C语言、面向对象的C++和模板C++。理解这种多语言属性有助于更好地掌握C++的复杂性和灵活性。 2. 使用const替代#...
9. **面向对象设计原则**:书中强调了封装、继承、多态等面向对象设计原则,并给出了如何在C++中实现这些原则的实例。 10. **编译器优化**:理解编译器如何进行优化可以帮助程序员编写出更高效的代码。书中讨论了...
1. **Objective-C基础**:Objective-C是苹果平台的主要编程语言,基于C语言并扩展了面向对象的特性。书中会介绍类、对象、消息传递等基本概念,以及与C++的交互方式。 2. **协议(Protocols)**:Objective-C的协议...
- 面向对象的C++:这部分强调了类、封装、继承、多态、虚函数等面向对象编程的核心概念。 - 模板C++:模板编程允许编写独立于数据类型的代码,是C++泛型编程的核心,也是C++强大的抽象工具之一。 - STL(标准模板...
书中包含的140个条目覆盖了C++语言的多个方面,包括面向对象设计、模板、STL、内存管理等关键主题。2018年版更新了部分内容,以适应现代C++的发展。 1. **面向对象设计**:书中强调了如何利用C++的面向对象特性来...
1. **面向对象设计原则**:书中第一条建议就强调了理解并使用对象是C++的核心,包括封装、继承和多态等面向对象特性。封装有助于隐藏实现细节,增强代码的健壮性;继承则提供了代码重用和扩展机制;多态则允许通过...