《C++面向对象软件设计及构建》文章翻译:7.5模板及继承,7.5 Templates and Inheritance
模板,可以以多种方式与继承配套使用。为什么继承可与模板配套使用呢?因为,模板类本身也是一个类,即使它是带有参数的类,也不会改变这个事实。将两个语言特性组合起来的话,其效果就是,模板的参数化能力与继承的特化能力有机结合。在本章节中,会说明模板与继承的四种组合方式:
•.一个模板类,继承另一个模板类,
•.一个非模板类,继承某个模板类,
•.一个模板类,继承某个非模板类,以及
•.
一个模板类,使用多重继承。
对于每种组合,都会有一个示例,展示这两种特性如何有机地结合在一起。同时还会说明,在这种组合中,得到了什么好处。
在这种情况下,基类和派生类都是模板。通常的用法中,会利用继承来扩展基类模板的功能,例如,在派生类模板中添加新的方法和/或数据。这种情况下的一种常见情景就是,派生类中的模板参数也会被用作基类的模板参数。
在此,我们利用章节7.1 中定义的 队列模板 来进行扩展, 以展示模板与继承之间的这种组合用法。对于队列数据结构 的一种常见扩展做法就是,加入 一个方法, 让它能够返回队列中的第一个元素,却不从队列中删除这个元素。 这样的一个队列,称之为 “可观察队列”(InspectableQueue),具有 一个基本队列所具有的所有方法。因此 ,就可以利用继承来实现这个数据结构,使得, 在 InspectableQueue 中就不需要重复定义那些已经在Queue 模板类中实现的方法了。 同时, 与Queue模板相同的 是 , InspectableQueue 也会被定义为一个模板,这样, 它就可以针对多种不同的类进行复用了。InspectableQueue 模板 的定义,如下所示。
|
template <class QueueItem> class Queue { private: QueueItem buffer[100]; int head, tail, count; public: Queue(); void Insert(QueueItem item); QueueItem Remove(); ~Queue(); }; template <class QueueItem> class InspectableQueue : public Queue<QueueItem> { public: InspectableQueue(); QueueItem Inspect(); // 返回第一个元素而不删除它 ~InspectableQueue(); }; |
InspectableQueue模板可以被实例化,而实例化之后的模板,其对象可以按照如下方式声明:
InspectableQueue<Location> locations;
Location loc1(...);
Location loc2(...);
...
locations.Insert(loc1); // 基类方法
locations.Insert(loc2); // 基类方法
...
Location front = locations.Remove(); // 基类方法
Location newFront = locations.Inspect(); // 派生 类方法
...
在对声明语句InspectableQueue<Location>进行实例化时,编译器必须将这个模板类与Queue 模板类之间的继承关系计算在内。在InspectableQueue 模板的声明中,说明了,传递给InspectableQueue 模板的模板参数,也就是本示例中的Location,应当同时用来对基类进行实例化。换句话说,由以上代码推导出的继承层次关系,就等价于:
InspectableQueue<Location> : public Queue<Location> {...} // 仅是示例用的语法
上面这句代码,并不是正确的C++语法。不过,它表达了,派生类模板与它的基类模板之间是怎样的关系。
非模板的派生类,可以继承某个模板基类,前提就是:该模板的参数,可根据派生类的自然特性及其定义来固定下来。在这种情况下,模板参数由派生类的设计者来决定。使用该个派生类的程序猿,不需要知道,在派生类的定义中用到了某个模板。根据继承关系的一般特性,模板类中的公有方法,也会成为派生类中的公有方法。因此,使用该派生类的程序猿,可以同时使用派生类和基类中的方法来对派生类对象进行操作。对于使用该派生类的程序猿来说,在继承操作中牵涉到的数据类型,反映了该派生类的设计者在设计时对模板参数所做的决策。
Polygon 类的一个变种,展示了,某个非模板类继承了某个模板类。多边形,是由一组点来定义的。这组点的个数是未知的,并且在其生命周期中可能会发生变化,因为,可能会向其中加入新的点,也可能删除已有的点。同时,多边形对象应当知道该如何将自己绘制到画布上去,具体就是,绘制多条线段,将相邻的点连接起来。因此,Polygon类,是两种能力的组合体:维护一组点的信息的能力;以及,绘制自身的能力。我们不需要实现全套用于维护列表的方法,只需要继承List 模板即可获取到列表维护能力,具体地,List 模板会根据Location 类进行实例化。Polygon这个派生类呢,在这些继承的方法之外,再加上绘制自身的能力即可。Polygon 类的设计如下所示。本示例中,重复写出了List 模板接口的定义,以便参考。
|
template <class BaseType> class List { private: ... public: List(); void First(); // 将第一个元素设置为当前元素 void Next(); // 移动到下一个列表元素 int Done(); // 是否还存在着当前元素? BaseType& Current(); // 获取当前值 void Insert(BaseType val); // 在当前值之后插入一个值 void Delete(); // 删除当前元素 ~List(); }; class Polygon : public List<Location> // Polygon“是一个”由Location组成的列表 { public: Polygon(); void Draw(Canvas& canvas); // 在画布中绘制自身 }; |
在Polygon 类中,模板参数被固定下来了,因为,多边形(Polygon)只会是由多个位置(Location)(点)的列表(List)组成。Polygon类本身并不是模板,因为,Polygon 类中的所有数据类型都是确定的。
以下代码示例中展示了Polygon 类的用法。注意,此处的Polygon 对象,既可以被从模板基类中继承的方法(例如,Insert)来操作,也可以被Polygon 类中自身定义的方法(例如,Draw)所操作。在这个示例中,还展示了,对于用到了Polygon 类的程序猿来说,根本感受不到模板类的存在。
Polygon poly;
...
poly.Insert(Location(20,20)); // 从模板继承的方法
poly.Insert(Location(30,30)); // 从模板继承的方法
// 再插入其它位置
poly.Draw(canvas); // 派生 类中的方法
这段代码,展示了Polygon 类的设计中的一个狠重要的点。对某个Polygon 对象调用Insert 方法时,需要传入一个Location 对象作为参数。Insert 方法的参数,它的类,是由List 模板的参数决定的。由于Polygon 类继承自List<Location>,所以,Insert 方法的参数就必须是一个Location 对象。
在这种情况中,派生类是一个模板类,而基类是一个非模板类。在这种情况下,模板参数仅仅对派生类有意义,而对基类无意义。不过,跟一般的公有继承相同的一点是,派生类会在它的公有接口中同时暴露出从基类继承的所有方法以及派生类自身加入的任何其它方法。
在这种情况下,我们所使用的示例,是一个模板,它绑定到了某个接口,而那个接口是由某个抽象基类的方法定义的, 再加上一个类,它通过自己的方法实现了那个接口。 这个示例中,会重新研究6.10 章节中展示的 Clock 类。 Clock 类,只依赖具体对象中的两个方法: Next方法 和 Show方法 。相比 于将Clock 类绑定到特定的类层次关系 ( 例如绑定到以 DisplayableNumber 为基类的类层次关系中 )来说 , 更有用处的做法是, 允许一个Clock 对象被连接到任何一个具有这两个方法的对象上。 在6.10 章节中,用到了 多重继承 ,以便将两个类组合在一起,其中一个是基类,它的纯虚函数中定义了一个接口,而另一个类,实现了该个虚基类所要求的接口。此处, 我们利用模板解决了同样的问题。 这个基类,以及根据 这个基类所定义的Clock 类,展示如下。
|
class SequencedInterface { public: virtual void Show() = 0; virtual void Next() = 0; }; class Clock { private: SequencedInterface *sequenced; public: Clock() {} void ConnectTo(SequencedInterface& sq) { sequenced = &sq;} void ConnectTo(SequencedInterface* sq) { sequenced = sq;} void Notify() { sequenced->Next(); sequenced->Show(); } }; |
在这个模板中,使用了一个对象,该个对象的类,是由模板的参数来指定的,它满足了该个模板所继承的基类的要求。这个对象,以构造函数参数的形式传递给了模板,并且,对这个对象的引用,被作为模板的私有数据保留下来。在下面的代码中,模板中私有数据成员seqItem 即是引用了这个对象。当模板的Show 方法被调用时,它就简单地将这个调用转发给seqItem 对象;对于Next 方法,也是一样的。如果用来补充该个模板的类并未定义Show 或Next 方法,那么,编译器会针对这件事报告一个错误。因此,在运行时,对于程序来说,可以确认,seqItem 对象一定会提供Show 和Next 方法。
|
template class <SequencedImplementation> class Sequenced : public SequencedInterface { private: SequencedImplementation& seqItem; public: Sequenced(SequencedImplementation& item) : seqItem(item) {} void Next() { seqItem.Next();} void Show() { seqItem.Show();} }; |
对于这个模板,可按照以下代码的方式来使用。此处,会创建一个Clock 和一个Counter 对象,并且打算将 Clock 对象连接到Counter 对象。然而,在 Clock 的ConnectTo 方法中,要求,该个Clock 所要连接的那个目标对象,必须是 SequencedInterface 的派生类。然而,Counter 类,继承的是DisplayableNumber,而不是SequencedInterface。利用Counter 类来补全ImplementsSequenced 模板,就可以在Counter 类和SequencedInterface 类之间通过对Sequenced 模板的编程来建立一个间接关系。这个间接关系,是通过两个步骤来建立的:首先,使用Counter 类来实例化Sequenced 模板;然后,对于这个实例化后的模板,构造出一个对象(timedCounter),并且传入Counter 类的一个对象(time)。这两个步骤,在下面示例中是使用同一行代码来完成的,具体就是创建timedCounter 对象的那行代码:
Clock timer;
Counter time(0);
...
Sequenced <Counter> timedCounter(time);
...
timer.ConnectTo( (SequencedInterface&)timedCounter );
..
timer.Notify(); // 调用time 对象 的Next 和Show 方法
以上代码的关键之处,就是,将timedCounter 编号类型转换成 SequencedInterface 对象的地方,这种转换本身是可行的,因为,timedCounter 对象是Sequenced<Counter>类的实例,而这个类的基类就是SequencedInterface。因此,派生类对象是正确地转换成它的基类的某个对象。这样,Clock类就只需要关注这样一个事实,也就是,它所连接到的对象,满足SequencedInterface 抽象基类中定义的接口。
Sequenced模板, 可被任何一个提供了Show 和Next 方法的类所补全 且 实例化。对于 这个示例,之前在 Animation 类 (章节6.10)中演示过,那个类同时提供了这两个方法。如下所示 ,一个Animation 对象也可以被连接到某个Clock 对象。
Clock timer;
Animation movie(...);
...
Sequenced <Animation> timedAnimation(movie);
...
timer.ConnectTo( (SequencedInterface&)timedAnimation );
..
timer.Notify(); // 调用movie 对象 的Next 和 Show
这砣代码,展示了模板的价值:利用模板,可以做到,让任何一个提供了Show 和Next 方法的类的实例,被安全地转换成SequencedInterface 类型,而无论该对象的类本身是否继承了SequencedInterface 基类。这种类型转换是安全的,因为,在模板被实例化的时候,编译器会确认,所用来实例化模板的那个类,必须拥有Show 和Next 方法。此处的类型转换,充分地扮演了自己本来的角色,它使得,Clock 类无需知道自己所连接到的那个对象具体是什么类型。
多重继承,是上面所说技巧的一个变种:利用模板来将某个抽象基类定义的接口绑定到由任意类所提供的实现上去。在ImplementsSequenced 模板中,所使用的技巧是,继承抽象基类,并且保留对于具体实现类的一个引用,以创建这种绑定。这种设计的一个缺陷就是,只有那些在接口中定义好了的方法才会出现在模板中;某些时候,可能需要的是,实现类中的所有方法都能够通过模板访问到。通过多重继承,可满足这种需求。
以下示例中,展示了:Sequenced模板,它使用的是单重继承;和,SequencedObject模板,它使用的是多重继承。Sequenced模板,利用单重继承和关联来将实现和接口绑定到一起。所关联的对象,seqItem,包含了对于Next 和Show 方法的实际实现,而SequencedObject 模板呢,使用的是多重继承。与Sequenced 模板相同的是,SequencedObject 模板继承了抽象基类SequencedInterface。另外,SequencedObject也从SequencedImplementation 继承了它的方法的实现。注意,SequencedImplementation本身是模板参数。两个模板都要求相同的构造函数参数;也就是,由SequencedImplementation 定义的类的一个对象。Sequenced模板维护着与这个对象之间的关联关系,而SequencedObject模板使用这个对象来初始化它自己的SequencedImplementation 层。类似地,对于转发方法,也有类似的区别。在Sequenced 模板中,它会将方法调用转发到seqItem 对象,而SequencedObject 会将方法调用转发给它自己的基类。
|
template class <SequencedImplementation> class Sequenced : public SequencedInterface { private: SequencedImplementation& seqItem; public: Sequenced(SequencedImplementation& item) : seqItem(item) {} void Next() { seqItem.Next();} void Show() { seqItem.Show();} }; template class <SequencedImplementation> class SequencedObject : public SequencedImplementation, public SequencedInterface{ public: SequencedObject(SequencedImplementation& item) : SequencedImplementation(item) {} void Next() { SequencedImplementation::Next();} void Show() { SequencedImplementation::Show();} }; |
对于使用Sequenced 和SequenceObject 模板的程序猿来说,这两个模板极其相似。在用来实例化模板及构造对象的代码中,除了模板名字以外,其它部分完全一致。以下代码展示了这种相似性,其中,分别用两种模板构造了同一个对象。
|
|
Clock timer; Counter time(0); ... Sequenced <Counter> timedCounter(time); ... timer.ConnectTo( (SequencedInterface&)timedCounter ); ... timer.Notify(); |
Clock timer; Counter time(0); ... SequencedObject <Counter> timedCounter(time); ... timer.ConnectTo( (SequencedInterface&)timedCounter ); ... timer.Notify(); |
然而,在Sequenced 和SequencedObject 模板之间有一个主要的区别。由于Sequenced模板使用了单重继承,所以,Sequenced 模板的对象都只能被类型转换为SequenceInterface。换句话说,SequencedImplementation类的实例,只能被看作SequencedInterface 对象。与之相对的是,SequncedObject模板同时继承了SequencedImplementation 和SequencedInterface,这样的话,SequencedObject对象就可以被类型转换为SequencedInterface 对象或SequencedImplementation 对象。利用这种灵活性,程序猿就可以做到:将模板对象类型转换成一个自认为满足SequencedInterface 接口的对象,事实上它确实满足这个接口,因为它继承了这个接口;也可以将同一个模板对象类型转换成SequencedImplementation 类对象,这也是正确的,因为它也继承了SequencedImplementation。
1.设计及实现一个AppendableList 模板,它继承List 模板。在妳的AppendableList 模板中,应当加入一个方法,通过该方法,能够将一个新的元素追加到列表末尾。
2.设计并实现一个PrependableList 模板,它继承List 模板。在妳的PrependableList模板中,应当加入一个方法,通过该方法,能够将一个新的元素插入到列表的开头。
3.设计并实现一个模板,名为ActionButton,它可用来创建一个按钮(Button),那个按钮在被按下时,会做出某个动作。ActionButton模板,只有一个模板参数,OnPushAction。ActionButton模板,继承了那个非模板类,Button。模板的构造函数中,需要传入OnPushAction 类的某个对象。ActionButton模板维护着它与OnPushAction 类的这个对象之间的关联关系,并且加入一个新的方法,void OnPush(),该个方法会将调用转发给它所关联的那个对象。
4.设计并实现一个Named 模板,它的用途是,为那个用作模板参数的类,加上一个字符串名字属性。通过妳的模板,应当能够做到,按照如下所示的方式来创建一个NamedCounter 对象,并且对其进行操作:
Counter count(0);
Named<Counter> namedCounter(count, "Seconds");
...
if (namedCounter.IsNamed("Seconds")) ...
char* name = namedCounter.GetName();
namedCounter.Next();
...
Counter& ctr = (Counter&)namedCounter;
5.编写一个测试用例,用于验证,SequencedObject<Counter>类的某个对象,可被类型转换为Counter 对象,而Sequenced<Counter>类的某个对象,不可被类型转换为Counter 对象。
未知美人
未知美人
Your opinionsHxLauncher: Launch Android applications by voice commands