Effective C++ 第二版 40)分层 41)继承和模板 42)私有继承(二)

2014-11-24 03:21:04 · 作者: · 浏览: 4
1 12 13 14 15 16 17 18 19 20 21 22 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 成员函数的代码和构成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