Stack::Stack(): top(0) {} // 顶部初始化为null void Stack::push(const T& object) { top = new StackNode(object, top); // 新节点放在 } // 链表头部 T Stack::pop() { StackNode *topOfStack = top; // 记住头节点 top = top->next; T data = topOfStack->data; // 记住节点数据 delete topOfStack; return data; } Stack::~Stack() // 删除堆栈中所有对象 { while (top) { StackNode *toDie = top; // 得到头节点指针 top = top->next; // 移向下一节点 delete toDie; // 删除前面的头节点 } } bool Stack::empty() const { return top == 0; } >即使对T一无所知, 还是能够写出每个成员函数; (假设可以调用T的拷贝构造, 条款45); 不管T是什么, 对构造, 销毁, 压栈, 出栈, 确定栈是否为空等操作所写的代码不会变; 除了"T的拷贝构造可以调用"之外, stack的行为不依赖于T;
Note 模板类的特点: 行为不依赖于类型;
所以将stack转化成模板就很简单:
| 1 2 3 | template<class T> class Stack { ... // 完全和上面相同 }; |
为什么猫不适合用模板 "每种猫都有各自特定的吃/睡方式"意味着必须为每种不同的猫实现不同的行为; 没法写一个函数来处理所有的猫[在模板中不适合, 会有很多switch()...if]; 可以实现的是制定一个函数接口, 所有种类的猫必须实现它---纯虚函数;
| 1 2 3 4 5 6 | class Cat { public: virtual ~Cat(); // 参见条款14 virtual void eat() = 0; // 所有的猫吃食 virtual void sleep() = 0; // 所有的猫睡觉 }; |
Cat的子类, e.g. Siamese[暹罗猫], BritishShortHaiedTabby[英国短毛], 必须重新定义继承来的eat和sleep接口:
| 1 2 3 4 5 6 7 8 9 10 11 12 | class Siamese: public Cat { public: void eat(); void sleep(); ... }; class BritishShortHairedTabby: public Cat { public: void eat(); void sleep(); ... }; |
知道了模板适合Stack类而不适合Cat类, 继承适合Cat类; 接下来的问题是为什么继承不适合Stack; 可以试着声明一个Stack层次结构的根类, 其他的堆栈类从这唯一的类继承:
| 1 2 3 4 5 6 | class Stack { // a stack of anything public: virtual void push(const object) = 0; virtual pop() = 0; ... }; |
问题很明显, 纯虚函数push和pop无法确定声明为什么类型; 每个子类必须重新声明继承而来的虚函数, 而且参数类型和返回类型都要和基类的声明完全相同; 可一个int堆栈只能压入/弹出int对象, 一个Cat堆栈只能压入/弹出Cat对象; Stack类无法做到声明纯虚函数使得用户既可以创建int堆栈又可以创建Cat堆栈; 因此继承不适合创建堆栈;
也许你认为可以使用通用指针(void*)来骗过编译器, 但事实上你无法避开这一条件: 派生类虚函数的声明永远不能和它在基类中的声明相抵触;[void* vs int* and Cat*]; 但是通用指针可以帮忙解决模板生成的类的效率的问题(条款42);
总结:
当对象的类型不影响类中函数的行为时, 要使用模板来生成这样一组类;
当对象的类型影响类中函数的行为时, 要是有继承来得到这样一组类;
条款42 明智地使用私有继承
条款35: C++将公有继承视为 IS-A 的关系; e.g. Student从Person公有继承, 编译器可以在必要时隐式地将Student转换为Person; 现在把公有继承换成私有继承:
| 1 2 3 4 5 6 7 8 | class Person { ... }; class Student: private Person { ... };// 这一次我们 // 使用私有继承 void dance(const Person& p); // 每个人会跳舞 void study(const Student& s); // 只有学生才学习 Person p; // p 是一个人 Student s; // s 是一个学生 dance(p); // 正确, p 是一个人 dance(s); // 错误!一个学生不是一个人 |
很明显私有继承的含义不是IS-A; 和公有继承相反:
1) 如果两个类之间的继承关系为私有, 编译器一般不会将派生类对象(Student)转换成基类对象(Person); 因此dance参数为s的时候失败;
2) 从私有基类继承而来的成员都成为了派生类的私有成员, 即使它们在基类中是保护或公有成员;
私有继承的含义: 用...来实现; e.g. 类D私有继承于类B, 表明是想利用类B中已存的某些代码, 而不是因为类B的对象和类D的对象之间有什么概念上的关系;
私有继承纯粹是一种实现技术, 只是继承实现, 接口会被忽略; D对象在实现中用到了B对象; 私有继承在设计过程中无意义, 只是在实现时才有用;
分层和私有继承: 分层也具有 用...来实现 的含义;
Note 尽可能使用分层, 必须时才使用私有继承; (保护成员/虚函数)
条款41提供了方法写一个Stack模板, 模板生成的类保存不同类型的对象; 模板是C++最有用的组成部分之一; 但如果实例化一个模板一百次, 就可能实例化了模板的代码一百次; e.g. Stack模板, 构成Stack
对于某些类, 可以采用指针来避免; 采用这种方法的类存储的是指针, 而不是对象; 实现步骤:
创建一个类, 存储的是对象的void*指针; 创建另外一组类, 唯一目的是用来保证类型安全, 借助第一步中的通用类来完成工作;
e.g. 类似条款41的非模板Stack类, 存储的是通用指针, 用void* 替换 T;
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | class GenericStack { public: GenericStack(); ~GenericStack(); void push(void *object); void * pop(); bool empty() const; private: struct StackNode { void *data; // 节点数据 StackNode *next; // 下一节点 StackNode(void *newData, StackNode *nextNode) : data(newData), next(nextNode) {} }; StackNode *top; // 栈顶 GenericStack(const GenericStack& rhs); // 防止拷贝和 赋值(参见条款27) Gene
|