条款40 通过分层来体现"有一个"或"用...来实现"
使某个类的对象成为另一个类的数据成员, 实现将一个类构筑在另一个类之上, 这个过程称为 分层Layering; e.g.
| 1 2 3 4 5 6 7 8 9 10 11 | class Address { ... }; // 某人居住之处 class PhoneNumber { ... }; class Person { public: ... private: string name; // 下层对象 Address address; // 同上 PhoneNumber voiceNumber; // 同上 PhoneNumber faxNumber; // 同上 }; |
>Person类被认为是置于string, Address和PhoneNumber类的上层, 因为他包含哪些类型的数据成员;
分层 也常被称为: 构成composition, 包含containment 或 嵌入embedding;
条款35解释了公有继承的含义是"是一个Is-a", 分层的含义是"有一个Have-a"或"用...来实现";
>Person类展示了 有一个 的关系: 有一个 名字, 地址, 电话, 传真...
Is-a和Have-a比较好区分, 比较难区分的是Is-a和 用...来实现, e.g. 假设需要一个类模板, 用来是任意对象的集合, 集合中没有重复元素; 程序设计中, 重用Resuse是件好事; 首先考虑采用标准库中的set模板; 但是set的限制不能满足程序要求: set内部的元素必须是完全有序的(升序或降序), 对许多类型来说, 这个条件容易满足, 而且对象间有序使得set在性能方面提供更多保证(条款49) ; 然而, 我们需要的是更广泛的: 一个类似set的类型, 但对象不必有序;
用C++标准术语, 他们只需要"相等可比较性": 对于同类的a和b对象, 要可以确定是否a==b; 这个需求适合表示颜色这类东西, 没有大小/多少比较, 但可以相同; 一个最简单的办法是采用链表, 标准库中的list模板;
自定义Set模板从list继承, 即Set
| 1 2 3 | // Set 中错误地使用了list template<class T> class Set: public list
|
>list对象可以包含重复元素, 如果3051这个值被添加到list
Note Set和list的关系并非是Is-a, 用公有继承是一个错误; 正确的方法是让Set对象 用list对象来实现:
| 1 2 3 4 5 6 7 8 9 10 11 | // 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
// 表示一个Set }; |
>Set的成员函数可以利用list以及标准库的功能;
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | template<class T> bool Set
const T& item) const { return find(rep.begin(), rep.end(), item) != rep.end(); } template<class T> void Set
const T& item) { if (!member(item)) rep.push_back(item); } template<class T> void Set
remove(const T& item) { list
if (it != rep.end()) rep.erase(it); } template<class T> int Set
const { return rep.size(); } |
>函数很简单, 参见条款33考虑内联; find begin end push_back是标准库基本框架的一部分, 可以对list这样的容器模板进行操作;
Set类的接口没有做到完整且最小(条款18); 完整性: 1) 不能对Set中的内容进行循环; 2) 没有遵循标准库采用的容器类常规;(条款49, M35) 会造成使用Set时更难以利用库中其他的部分;
Set和list的关系并非是Is-a, 而是"用...来实现", 通过分层来实现的关系;
Note 通过分层使两个类产生联系时, 在两个类之间建立了编译时的依赖关系; (条款34)
条款41 区分继承和模板
考虑2个设计问题:
1) 设计一个类来表示对象的堆栈; 这将需要多个不同的类, 因为每个堆栈中的元素必须是同类的; e.g. 用一个类表示int的堆栈, 另一个类表示string的堆栈, 还有一个类表示string的堆栈的堆栈... 为了设计最小的类接口, 会将对堆栈的操作限制为: 创建/销毁堆栈, 将对象压入/弹出堆栈, 检查堆栈是否为空; 不借助标准库中的类(stack), 目标是探究工作原理;
2) 设计一个类来表示猫; 同样需要多个不同的类, 每个品种的猫都会有所不同; 猫可以被创建/销毁, 猫会吃/睡, 但每只猫的吃/睡都有各自独特方式;
两个问题看起来相似, 设计起来却完全不同:
这涉及到 类的行为 和 类所操作的对象的类型 之间的关系; 对于堆栈和猫来说, 都要处理不同的类型(堆栈包含T类对象, 猫则为品种T); 如果类型T影响类的行为, 使用虚函数和继承, 如果不影响行为, 可以使用模板;
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | class Stack { public: Stack(); ~Stack(); void push(const T& object); T pop(); bool empty() const; // 堆栈为空 private: struct StackNode // 链表节点 { T data; // 此节点数据 StackNode *next; // 链表中下一节点 // StackNode 构造函数,初始化两个域 StackNode(const T& newData, StackNode *nextNode) : data(newData), next(nextNode) {} }; StackNode *top; // 堆栈顶部 Stack(const Stack& rhs); // 防止拷贝和 Stack& operator=(const Stack& rhs); // 赋值(见条款27) }; |
Stack对象将构造的数据结构: Stack对象top-->data+next-->data+next-->data+next......StackNode对象;
链表本身是由StackNode对象构成的, 但这只是Stack类的一个实现细节, 所以StackNode被声明为Stack的私有类型; StackNode有构造函数来确保所有的域都被正确地初始化; (C++特性: struct的构造)
对Stack成员函数的实现, 原型prototype的实现:
| 1 2 3 4 5 6 7 8 9 10 1 |