条款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<T>将从list<T>继承:

1
2
3
// Set 中错误地使用了list
template<class T>
class Set: public list<T> { ... };

>list对象可以包含重复元素, 如果3051这个值被添加到list<int>中两次, list中会包含3051两个拷贝; 相反, Set不可以包含重复元素, 就算添加2次, 也只会包含一个拷贝; 所以有一些在list对象中成立的事情在Set中不成立;

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<T> rep; // 表示一个Set
};

>Set的成员函数可以利用list以及标准库的功能;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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(); }

>函数很简单, 参见条款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
11
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<int>成员函数的代码和构成Stack<double>成员函数的代码是完全分开的; 有时这是不可避免的, 即使模板函数实际上可以共享代码, 这种代码重复还是可能存在; 这种目标代码体积的增加叫做: 模板导致的"代码膨胀";

对于某些类, 可以采用指针来避免; 采用这种方法的类存储的是指针, 而不是对象; 实现步骤:

创建一个类, 存储的是对象的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)
    GenericStack& operator=(const GenericStack& rhs);
};

因为类存储的是指针, 可能会出现一个对象被多个堆栈指向的情况(被压入到多个堆栈); 很重要的一点是, pop和类的析构函数销毁任何StackNode对象时, 都不能删除data指针; StackNode对象是在GenericStack类内部分配的, 所以还是得在类的内部释放;

仅仅有GenericStack类是没用的, 很多人容易误用它; e.g. 对于一个保存int的堆栈, 用户会错误地将一个指向Cat对象的指针压入这个堆栈中, 编译不会报错; 因为对于void*参数, 指针类型就可以通过;

为了类型安全, 要为GenericStack创建接口类 interface class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class IntStack { // int 接口类
public:
    void push(int *intPtr) { s.push(intPtr); }
    int * pop() { return static_cast<int*>(s.pop()); }
    bool empty() const return s.empty(); }
private:
    GenericStack s; // 实现
};
class CatStack { // cat 接口类
public:
    void push(Cat *catPtr) { s.push(catPtr); }
    Cat * pop() { return static_cast<Cat*>(s.pop()); }
    bool empty() const return s.empty(); }
private:
    GenericStack s; // 实现
};

>IntStack和CatStack只是适用于特定类型; 只有int/Cat指针可以被压入或弹出IntStack/CatStack; IntStack和CatStack都通过GenericStack类来实现, 这种关系是通过分层来体现的; IntStack和CatStack将共享GenericStack中真正实现它们行为的函数代码; IntStack和CatStack所有成员函数是(隐式)内联函数, 意味着使用这些接口类带来的开销几乎是零;

但是如果有些用户错误地认为使用GenericStack更高效, 或者轻率地认为类型安全不重要, 怎样才能阻止他们绕过IntStack和CatStack而直接使用GenericStack? (设计C++特别要避免类型错误);

要表示 用...来实现,  可以选择私有继承; 通过它可以告诉别人: GenericStack使用起来不安全, 只能用来实现其他的类;

e.g. 将GenericStack的成员函数声明为保护类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class GenericStack {
protected:
    GenericStack();
    ~GenericStack();
    void push(void *object);
    void * pop();
    bool empty() const;
    private:
... // 同上
};
GenericStack s; // 错误! 构造函数被保护
 
class IntStack: private GenericStack {
public:
    void push(int *intPtr) { GenericStack::push(intPtr); }
    int * pop() { return static_cast<int*>(GenericStack::pop()); }
    bool empty() const return GenericStack::empty(); }
};
class CatStack: private GenericStack {
public:
    void push(Cat *catPtr) { GenericStack::push(catPtr); }
    Cat * pop() { return static_cast<Cat*>(GenericStack::pop()); }
    bool empty() const return GenericStack::empty(); }
};
IntStack is; // 正确
CatStack cs; // 也正确

>和分层的方法一样, 私有继承的实现避免代码重复, 这样的类型安全的接口类只包含有对GenreicStack函数的内联调用;

在GenericStack类上构筑类型安全的接口是个trick的技巧, 手工写所有的接口类是很麻烦的, 可以使用模板来自动生成它们;

e.g. 模板通过私有继承来生成类型安全的堆栈接口;

1
2
3
4
5
6
7
template<class T>
class Stack: private GenericStack {
public:
    void push(T *objectPtr) { GenericStack::push(objectPtr); }
    T * pop() { return static_cast<T*>(GenericStack::pop()); }
    bool empty() const return GenericStack::empty(); }
};

>编译器会通过这个模板, 根据你的需要自动生成所有的接口类;

因为这些类是类型安全的, 用户类型错误编译器就能发现;

因为GenericStack的成员函数是保护类型, 而且接口把GenericStack作为私有基类, 用户无法绕过接口类;

因为每个接口类成员函数被(隐式)声明为inline, 使用这些类型安全的类时不会带来运行开销; 生成的代码就像用户直接使用GenericStack来编写的一样;

因为GenericStack使用了void*指针, 操作堆栈的代码就只需要一份, 不管程序中使用了多少不同类项的堆栈;

这个设计使得代码达到高效和强力的类型安全;

Note C++的各种特性是以非凡的方式相互作用的;

从这个例子可以发现, 如果使用分层, 达不到这样的效果, 只有继承才能访问保护成员, 只有继承才使得虚函数可以重新被定义; (条款43 虚函数引发私有继承的使用); 因为存储虚函数和保护成员, 有时候私有继承是表达类之间"用...来实现"关系的唯一有效途径; 但从广泛意义上来说, 分层是应该优先采用的技术;

---YC---

Effective C++ 第二版 40)分层 41)继承和模板 42)私有继承的更多相关文章

  1. Effective Java 第二版 Enum

    /** * Effective Java 第二版 * 第30条:用enum代替int常量 */ import java.util.HashMap;import java.util.Map; publi ...

  2. 《Effective Java第二版》总结

    第1条:考虑用静态工厂方法代替构造器 通常我们会使用 构造方法 来实例化一个对象,例如: // 对象定义 public class Student{ // 姓名 private String name ...

  3. 《Effective Java 第二版》读书笔记

    想成为更优秀,更高效程序员,请阅读此书.总计78个条目,每个对应一个规则. 第二章 创建和销毁对象 一,考虑用静态工厂方法代替构造器 二, 遇到多个构造器参数时要考虑用builder模式 /** * ...

  4. Effective C++ 第二版 17)operator=检查自己 18)接口完整 19)成员和友元函数

    条款17 在operator=中检查给自己赋值的情况 1 2 3 class  X { ... }; X a; a = a;  // a 赋值给自己 >赋值给自己make no sense, 但 ...

  5. Effective C++ 第二版 10) 写operator delete

    条款10 写了operator new就要同时写operator delete 写operator new和operator delete是为了提高效率; default的operator new和o ...

  6. Effective C++ 第二版 8) 写operator new 和operator delete 9) 避免隐藏标准形式的new

    条款8 写operator new 和operator delete 时要遵循常规 重写operator new时, 函数提供的行为要和系统缺省的operator new一致: 1)正确的返回值; 2 ...

  7. Effective C++ 第二版 5)new和delete形式 6) 析构函数里的delete

    内存管理 1)正确得到: 正确调用内存分配和释放程序; 2)有效使用: 写特定版本的内存分配和释放程序; C中用mallco分配的内存没有用free返回, 就会产生内存泄漏, C++中则是new和de ...

  8. Effective C++ 第二版 1)const和inline 2)iostream

    条款1 尽量用const和inline而不用#define >"尽量用编译器而不用预处理" Ex. #define ASPECT_R 1.653    编译器永远不会看到AS ...

  9. Effective C++ 第二版 31)局部对象引用和函数内new的指针 32)推迟变量定义

    条款31 千万不要返回局部对象的引用, 不要返回函数内部用new初始化的指针的引用 第一种情况: 返回局部对象的引用; 局部对象--仅仅是局部的, 在定义时创建, 在离开生命空间时被销毁; 所谓生命空 ...

随机推荐

  1. HTML DOM节点

    在 DOM 树中,基本上一切都是节点.每个元素在最底层上都是 DOM 树中的节点.每个属性都是节点.每段文本都是节点.甚至注释.特殊字符(如版权符号 ©).DOCTYPE 声明(如果 HTML 或者 ...

  2. Java编程思想-基于注解的单元测试

    Junit的测试方法命名不一定以test开头 上面介绍的atunit已经很老了,现在junit测试框架已经基本注解了

  3. WebStorm shortcuts.

  4. 抓取锁的sql语句-第三次修改

    CREATE OR REPLACE PROCEDURE SOLVE_LOCK AS V_SQL VARCHAR2(3000); --定义 v_sql 接受抓取锁的sql语句CUR_LOCK SYS_R ...

  5. TCP/IP-IP

    A contented mind is a perpetual feast. "知足长乐" 参考资料:TCP/IP入门经典 (第五版) TCP/IP详解 卷一:协议 一.简介 IP ...

  6. python密码处理(可用于生产模式)

    import os from hashlib import sha256 from hmac import HMAC def encrypt_password(password, salt=None) ...

  7. Websocket 与代理服务器如何交互? How HTML5 Web Sockets Interact With Proxy Servers

    How HTML5 Web Sockets Interact With Proxy Servers Posted by Peter Lubberson Mar 16, 2010 With the re ...

  8. CAFFE安装 CentOS无GPU

    前记 由于是在一台用了很久的机器上安装caffe,过程比较复杂,网上说再干净的机器上装比较简单.如果能有干净的机器,就不用再过这么多坑了,希望大家好运!介绍这里就不说了,直接进入正题: Caffe 主 ...

  9. Egret 入门

    居然使用 TyptScript... 先贴手册地址:http://www.typescriptlang.org/docs/tutorial.html. 先要接受一个诡异的写法: private loa ...

  10. Scut:缓存管理

    Scut 的缓存管理看起来还是蛮复杂的.   redis 本身就有内存缓存+持久化的作用,Scut还是自己封装了一层内存缓存+Redis缓存+持久化. . 这是一个缩略版本的结构图. 1. 上半部分是 ...