使某个类的对象成为另一个类的数据成员,从而实现将一个类构筑在另一个类之上,这一过程称为 “分层”(Layering)。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class Address { ... };?????????? // 某人居住之处 class PhoneNumber { ... }; class Person { public: ... private: string name;?????????????????? // 下层对象 Address address;?????????????? // 同上 PhoneNumber voiceNumber;?????? // 同上 PhoneNumber faxNumber;???????? // 同上 }; |
本例中,Person类被认为是置于string,Address和PhoneNumber类的上层,因为它包含那些类型的数据成员。”分层” 这一术语有很多同义词,它也常被称为:构成(composition),包含(containment)或嵌入(embedding)。
使公有继承体现 “是一个” 的含义解释了公有继承的含义是 “是一个”。对应地,分层的含义是 “有一个” 或 “用…来实现”。
上面的Person类展示了 “有一个” 的关系。一个Person对象 “有一个” 名字,地址,电话号码和传真号码。你不能说,一个人 “是一个” 名字或一个人 “是一个” 地址;你得说,一个人 “有一个” 名字, “有一个” 地址,等等。大多数人对区分这些没什么困难,所以混淆 “是一个” 和 “有一个” 的情况相对来说比较少见。
稍微有点麻烦的是区分 “是一个” 和 “用…来实现”。例如,假设需要一个类模板,用来表示任意对象的集合,并且集合中没有重复元素。程序设计中,重用(Reuse)是再好不过的一件事了,而且你也许已经读过熟悉标准库中关于C++标准库的总体介绍,那么,你的第一反应一定是想采用标准库中的set模板。是啊,既然可以使用别人所写的东西,为什么还要再去写一个新的模板呢?
但是,深入研究set的帮助文档后,你会发现,set的下述限制将不能满足你的程序要求:set要求包含在它内部的元素必须是完全有序的,即,对set中的任两个元素a和b来说,一定可以确定:要么a<b,要么b<a。对许多类型来说,这个要求很容易满足,而且,对象间完全有序使得set可以在性能方面提供某些保证,这一点很吸引人。(了解标准库在性能上更多的保证)然而,你所需要的是更广泛的东西:一个类似set的类,但对象不必完全有序;用C++标准所包装的术语来说,它们只需要所谓的 “相等可比较性”:对于同种类型的a和b对象来说,要能确定是否a==b。这种要求更简单,它更适合于那些表示颜色这类东西的类型。总不能说红色比绿色更少或绿色比红色更少吧?看来,对你的程序来说,还是得需要自己来写个模板。
当然,重用还是件好事。作为数据结构专家,你知道,在实现集合的众多选择中,一个最简单的办法是采用链表。你一定猜到了什么。对,标准库中正有这么一个list模板(用来产生链表类)!所以可以重用它。
具体来说,你决定让自己的Set模板从list继承。即,Set<T>将从list<T>继承。因为,在你的实现中,Set对象实际上将是list对象。于是你这样声明Set模板:
1 2 3 |
// Set中错误地使用了list template<class T> class Set: public list<T> { ... }; |
至此,一切好象都很正确,但实际上错误不小。正如使公有继承体现 “是一个” 的含义所说明的,如果D “是一个” B,对B成立的所有事实对D也成立。但是,list对象可以包含重复元素,所以如果3051这个值被增加到list<int>中两次,list中将包含3051的两个拷贝。相反,Set不可以包含重复元素,所以如果3051被增加到Set<int>中两次,Set中将只包含这个值的一个拷贝。于是,说一个Set “是一个” list就是弥天大谎,因为如上所述,有一些在list对象中成立的事实在Set对象中不成立。
因为这两个类的关系并非 “是一个”,所以用公有继承来表示它们的关系就是一个错误。正确的方法是让Set对象 “用list对象来实现”:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// Set中使用list的正确方法 template<class T> class Set { public: bool member(const T& item) const; void insert(const T& item); void remove(const T& item); int cardinality() const; private: list<T> rep;?????????????????????? // 表示一个Set }; |
Set的成员函数可以利用list以及标准库其它部分所提供的大量功能,所以,实现代码既不难写也很易读:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
template<class T> bool Set<T>::member(const T& item) const { return find(rep.begin(), rep.end(), item) != rep.end(); } template<class T> void Set<T>::insert(const T& item) { if (!member(item)) rep.push_back(item); } template<class T> void Set<T>::remove(const T& item) { list<T>::iterator it = find(rep.begin(), rep.end(), item); if (it != rep.end()) rep.erase(it); } template<class T> int Set<T>::cardinality() const { return rep.size(); } |
这些函数很简单,所以很自然地想到将它们作为内联函数;但在做最后决定前,还是回顾一下明智地使用内联所做的讨论。(上面的代码中,find, begin, end, push_back等函数是标准库基本框架的一部分,它们可用来对list这样的容器模板进行操作。标准库框架的总体介绍参见熟悉标准库。)
值得指出的是,Set类的接口没有做到完整并且最小(争取使类的接口完整并且最小)。从完整性上来说,它最大的遗漏在于不能对Set中的内容进行循环,而这一功能对很多程序来说是必需的(标准库中的所有成员都提供了这一功能,包括set)。Set的另一个缺陷是没有遵循标准库所采用的容器类常规,从而造成使用Set时更难以利用库中其它的部分。
Set的接口尽管有这些瑕疵,但下面这一点不能被掩盖:Set在理解它和list的关系上,具有无可辩驳的正确性。这种关系并非 “是一个”(虽然初看会以为是),而是 “用…来实现”,通过分层来实现这种关系是类的设计者应该感到自豪的。
顺便说一句,当通过分层使两个类产生联系时,实际上在两个类之间建立了编译时的依赖关系。关于为什么要考虑到这一点以及如何减少这方面的麻烦,参见将文件间的编译依赖性降至最低。
You mentioned this really well. canadian pharmacies shipping to usa
Wow many of excellent facts! trazodone 50 mg
Very good posts. Cheers! 600 mg cbd oil
Cheers, Plenty of postings.
canadian pharmacies online
You have made your position extremely effectively!. hydrochloorthiazide
Incredible loads of great knowledge! Isotretinoin Capsules Price
Reliable data. With thanks. cialis tablets
This is nicely put. . kratom
Nicely put, Thanks a lot. canadian pharmacy viagra brand