zoukankan      html  css  js  c++  java
  • Effective C++ 第二版 40)分层 41)继承和模板 42)私有继承

    条款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---

  • 相关阅读:
    traceroute命令
    Apache部署django项目
    Linux中变量#,#,@,0,0,1,2,2,*,$$,$?的含义
    Python正则表达式
    Python 字符串格式化 (%操作符)
    Python初学者的一些编程技巧
    Linux命令 ls -l 输出内容含义详解
    Django 前后台的数据传递示列
    hibernate基础(一)
    MySQL之多表
  • 原文地址:https://www.cnblogs.com/fuhaots2009/p/3471324.html
Copyright © 2011-2022 走看看