• C++面向对象高级开发课程(第三周)


    一,类与类之间的关系:继承(Inheritance)、复合(Composition)、委托(Delegation)。

    二,复合:表示 is-a ,该设计思想可以参照C语言的 struct 。

    1. 例子:源自STL中queue的源代码。

     1 template <class T, class Sequence = deque<T> >
     2 class queue {
     3 ...
     4 protected:
     5     Sequence c; // 底层容器
     6 public:
     7     // 以下完全利用 c 的操作函數完成
     8     bool empty() const { return c.empty(); }
     9     size_type size() const { return c.size(); }
    10     reference front() { return c.front(); }
    11     reference back() { return c.back(); }
    12     // deque 是兩端可進出,queue 是末端進前端出(先進先出)
    13     void push(const value_type& x) { c.push_back(x); }
    14     void pop() { c.pop_front(); }
    15 };

    其中第1行代码  class Sequence = deque<T> 表示 Sequence 默认是 deque<T> 类型。

    以上的 queue 类也等价于把 deque<T> 替换进来。

    template <class T>
    class queue {
    ...
    protected:
        deque<T> c; // 底层容器
    public:
        // 以下完全利用 c 的操作函数完成
        bool empty() const { return c.empty(); }
        size_type size() const { return c.size(); }
        reference front() { return c.front(); }
        reference back() { return c.back(); }
       //
        void push(const value_type& x) { c.push_back(x); }
        void pop() { c.pop_front(); }
    };

    2. 对于这种类与类之间的关系,表示了 GoF 23 种设计模式中的 Adapter 适配器模式——类对象结构型模式。如下图表示:

    3. 其中,queue 是容器,它改装了 deque——也就是重写了一些操作的方法名。比如原来 deque 的 push_back()改写成了 push();pop_front()改写成了pop()。

    4. 复合(Composition)关系的 UML 表示法,如下图:

    尤其重要的是表示类与类之间的“复合”关系。

    5. 从内存的角度,复合关系在内存中的分布是这样的,如下图所示:

    6. 构造与析构函数的执行流程

    6.1  构造函数由内而外:Container 的构造函数首先调用 component 的 default 构造函数,然后才执行自己。

    Container::Container(...) : Component() {...} ;

       构造函数后面的单引号“:”表示委托构造函数语法,同时也是构造函数初始值列表。

      上面那段代码中红体字部分,表示是由编译器加入的,如果不满足程序员应用的新情况,程序员可以自行修改或者加入参数。 

    6.2  析构函数由外而内:Container 的析构函数首先执行自己,然后才调用 Component 的析构函数。

    Container::~Container(...) {... ~Component() };

    上面那段代码中红体字部分,表示是由编译器加入的。

    三,委托:Composition by reference

    1. Composition by reference 不能根据现实情况——用指针“组合”到一起——表示为 by pointer。是因为学术界不说 pointer 而是 reference。该课程也从来不提 by pointer。

    2. UML表示如下图:

    其中,空心的表示内部用指针而不是实体来“组合”,所以有点虚,用空心的菱形表示。

    3. 例子 String.h 代码如下

     1 // file String.hpp
     2 class StringRep;
     3 class String {
     4 public:
     5     String();
     6     String(const char* s);
     7     String(const String& s);
     8     String &operator=(const String& s);
     9     ~String();
    10 . . . .
    11 private:
    12     StringRep* rep; // pimpl
    13 };

      代码第12行:pimpl 表示 pointer to implementation 。pimpl 是一种设计模式。“左边的类”是接口,“右边的类”是具体实现。此方法也称“编译防火墙”

       文件 String.cpp 代码如下

    // file String.cpp
    #include "String.hpp"
    namespace {
    class StringRep {
        friend class String;
            StringRep(const char* s);
            ~StringRep();
            int count;
            char* rep;
        };
    }
    
    String::String(){ ... }
    ...

       “委托”关系类之间的生命周期不同步:因为类与类之间的“组合”方式是 reference ,所以声明周期不同步。

    4. String 只是对外的接口,当需要任何实现时都会去调用 StringRep 这个具体的实现类的操作。这种设计模式是 GoF 23 当中的 Bridge 桥接模式,也称 Handle and Body 模式。 

      该设计模式有利于“实现方法”的切换,很具有弹性。这样的方法也叫“编译防火墙”左边只编译一次,右边可以变换实现方式。

    5. String类 和 StringRep类 的画图表示如下图

    三个对象共享一个数据空间:Hello。此时的reference counting,也就是 n 的值是 3。

    6. 当有其中某个对象需要改变数据时,程序会分配一个新的数据空间给它让他去修改,这样的设计方式也称作“copy on write”。

    四,继承:表示 is-a。

    1. 继承与虚函数搭配最强有力。

    2. UML图表示如下

    其中空心的三角形表示继承,且父类与子类一定要保持上下位置。

    (表示继承的空心三角形) 

    (放块 T 表示该类是模版类型)

    对应UML的代码如下

    struct _List_node_base
    {
        _List_node_base* _M_next;
        _List_node_base* _M_prev;
    };
    template<typename _Tp>
    struct _List_node : public _List_node_base
    {
        _Tp _M_data;
    };

       在C++中,struct 同样具有继承的功能。

    3. 继承关系的UML图表示如下图

    4. 从内存分配的角度看继承关系

    5. bass class 的 dtor(析构函数)必须是 virtual function,否则会出现编译错误:undefined behavior。

    6. 构造与析构函数的执行流程

    6.1  构造由内而外:Derived(派生类)的构造函数首先调用 Base 的 default 构造函数,然后才执行自己。

    Derived::Derived(...) : Base() {...};

       上面那段代码中红体字部分,表示是由编译器加入的,如果不满足程序员应用的新情况,程序员可以自行修改或者加入参数。 

    6.2  析构由外而内:Derived 的析构函数首先执行自己,然后才调用 Base 的析构函数。

    Derived::~Derived(...) { ... ~Base() };

       上面那段代码中红体字部分,表示是由编译器加入的。

    7. 继承权限有三种(public、private、protected)public在C++中最常用,并且Java语言只有 public 继承权限。

    五,Inheritance with virtual functions

    1. 大部分的继承,都是继承的函数调用权。(因为数据都是放在 private 区)

    2. non-virtual 函数 : 不希望 derived class(子类)重新定义(override)覆盖它。

      virtual 函数 : 希望 derived class 重新定义(override)它,且对它已有的默认定义进行覆盖。

      pure-virtual 函数 : 希望 derived class 一定要重新定义它,且目前对它函数本身无任何默认定义。

      代码以及示例如下图:

    3. 虚函数 :可以完成子类 override 的操作。类似 PHP mvc 框架中 IndexController 继承 Controller 之后,在 IndexController::init() 中定义的“前操作”。

    4. virtual functions 的作用:延缓父类中的动作到子类中去写出来。是设计模式 Template Method 的基础。

    比如下面这段节选自 MFC 中的代码。用到的设计模式是GoF 23 中 Template Method (模版方法)——类行为型模式。

    4.1 UML图如下

    4.2 Application framework 代码(MFC官方定义的基础类)如下

    CDocument::OnFileOpen()
    {
        ...
        Serialize(); // 读取不同类型文件的方法暂时未知,所以交由子类去实现
        ...
    }

     4.3 Application 代码(程序员应用MFC基础类,继承之后的代码实现)如下

    class CMyDoc::public CDocument
    {
        virtual Serialize() 
        {
            // users do something
        }
    };

     4.4 在主程序中,对于用户自定义类的用法如下

    main()
    {
        CMyDoc myDoc;
        …
        myDoc.OnFileOpen();
    }

     4.5 程序执行流程,如下图

    第一步:初始化 myDoc 类。

    第二步:调用 myDoc.OnFileOpen() 方法,编译器会自动寻径到父类(CDocument)中的OnFileOpen() 方法。

    第三步:编译器发现 serialize() 方法是虚函数,所以会自动向“下”寻径(CMyDoc),去用子类的 serialize() 来覆盖父类中的 serialize()。

    4.6 具体的代码实现如下图

    六,Inheritance + Composition 关系下的构造和析构

    1.情况一:复合发生在子类中

    1.1 UML图如下

    1.2 内存分布如下图

    1.3 代码实现

    class Component
    {
    public:
        Component() { std::cout << "component" << std::endl;  }
    };
    
    class Base
    {
    public:
        Base() { std::cout << "base"<< std::endl; }
    };
    
    class Derived : public Base
    {
    public:
        Derived() { std::cout << "derived" << std::endl; }
    private:
        Component _component;
    };
    
    int main()
    {
        Derived d;
        return 0;
    }

     1.4 运行结果,如下图

    1.5 结论:在 inheritance + composition 设计模式下,当复合发生在子类时,编译器的执行流程是 base->component->derived。

    Derived::Derived(…): Base(),Component() { … }; // 红色字体表示编译器自动生成的

    2.情况二:复合发生在父类中

    2.1 UML,如下图

    2.2 内存分布,如下图

    2.3 代码实现

    class Component
    {
    public:
        Component() { std::cout << "component" << std::endl;  }
    };
    
    class Base
    {
    public:
        Base() { std::cout << "base"<< std::endl; }
    private:
        Component _component;
    };
    
    class Derived : public Base
    {
    public:
        Derived() { std::cout << "derived" << std::endl; }
    };
    
    int main()
    {
        Derived d;
        return 0;
    }

     2.4 运行结果

    2.5 结论:在 inheritance + composition 设计模式下,当复合发生在父类时,编译器的执行流程是 component->base->derived。

    Derived::~Derived(…){ … ~Component(), ~Base() }; // 红色表示编译器自动生成的

    至此,类与类之间的三种关系已经交待完毕,下面是实际应用的部分。

    七,Delegation(委托) + Inheritance(继承)

    1. 这种设计模式在 UI 程序设计中最常用到,功能也是最强大的。

    2. GoF 23 observer 观察者模式

    2.1实例代码 Subject类 

    class Subject
    {
        int m_value;
        vector<Observer*> m_views;
    public:
        void attach(Observer* obs)
        {
            m_views.push_back(obs);
        }
        void set_val(int value)
        {
            m_value = value;
            notify();
        }
        void notify()
        {
            for (int i = 0; i < m_views.size(); ++i)
                m_views[i]->update(this, m_value);
        }
    };

     Observer类

    class Observer
    {
    public:
        virtual void update(Subject* sub, int value) = 0;
    };

     2.2. UML图如下

    2.3. 窗口(window)就是 observer 类。

    2.4 实际代码如下图

    2.5 对 2.4 中代码的解释

    2.5.1 注册的动作

    void attach(Observer* obs)

       把观察者注册到本体(被观察者)中。

    2.5.2 通知观察者的动作

    void notify()

       通知观察者,进行更新数据。

       以上就是OOD(面向对象设计) 

    至此,已经学习了 Adapter、Handle-Body、Template Method、Observer四中设计模式。还将学习Composition、Prototype两种设计模式。

    八,委托相关设计

    1. 设计一个文件系统。采用 GoF 23 当中的 Composite(组成)模式——对象结构型模式。

        其中  primitive  adj.基本的  表示文件

          composite  n.合金   表示容器

    1.1 Composite 设计模式目的:容器中既能放置自己(composite),又能放置 primitive。

    1.2 UML

    1.3 代码

    Component类

    class Component
    {
        int value;
    public:
        Component(int val) { value = val; }
        virtual void add( Component* ) { }
    };

     primitive类

    class Primitive: public Component
    {
    public:
        Primitive(int val): Component(val) {}
    };

     composite类

    class Composite: public Component
    {
        vector <Component*> c;
    public:
        Composite(int val): Component(val) { }
        void add(Component* elem) 
        {
            c.push_back(elem);
        }
        …
    };

     1.4 在C++的容器中(比如vector)只能存储每一个占用内存大小都是一样的类型,所以composite类中容器存储的是指针,尤其对于用户自定义的类型,更要存储指针。 

    2. 设计一个框架。采用 GoF 23 当中的 Prototype(原型) 模式——对象创建型模式。 

    2.1 Prototype 设计模式的目的:在不可能知道子类名称的时候,创建子类对象。

      C++原型模式中子类创建了一个自己,就是所谓的原型

      该设计模式突破了C++不知道类名而无法创建该类对象的束缚。

      比如如下php代码可以动态地创建一个类。

    $obj = new $className();

    2.2 UML

    2.2.1 UML图中,带有下划线的表示是静态成员。

       (成员函数、成员变量)标识符:类型名

        “-”负号表示 private

        “#”井号表示protected

    2.3 代码

    Image.h

    enum imageType
    {
        LSAT, SPOT
    };
    class Image
    {
    public:
        virtual void draw() = 0;
        static Image *findAndClone(imageType);
    protected:
        virtual imageType returnType() = 0;
        virtual Image *clone() = 0;
        // 每一个声明过的Image的子类,注册它自己的原型 
        static void addPrototype(Image *image)
        {
            _prototypes[_nextSlot++] = image;
        }
    private:
        // addPrototype()函数在这里存储每一个注册过的原型 
        static Image *_prototypes[10];
        static int _nextSlot;
    };
    Image *Image::_prototypes[];
    int Image::_nextSlot;

     Image.cpp

    // 客户端需要客户端调用这个public static方法,当它需要Image的子类的实例时
    Image *Image::findAndClone(imageType type)
    {
        for (int i = 0; i < _nextSlot; i++)
            if (_prototypes[i]->returnType() == type)
                return _prototypes[i]->clone();
    }

     LandSatImagine.h

    class LandSatImage : public Image
    {
    public:
        imageType returnType() {
            return LSAT;
        }
        void draw() {
            cout << "LandSatImage::draw " << _id << endl;
        }
        // 当clone()函数被调用时,它调用带有一个无用参数的构造函数
        Image *clone() {
            return new LandSatImage(1);
        }
    protected:
        // 这个构造函数只能被clone()函数调用
        LandSatImage(int dummy) {
            _id = _count++;
        }
    private:
        // 原理:初始化Image子类将会引起注册过的子类原型的默认构造函数被调用
        static LandSatImage _landSatImage;
        // 只有private static数据成员被初始化后,才会被调用
        LandSatImage() {
            addPrototype(this);
        }
        // 名词性的“state”是每一个实例的机制(Nominal "state" per instance mechanism)
        int _id;
        static int _count;
    };
    // 注册子类原型 
    LandSatImage LandSatImage::_landSatImage;
    // 初始化“state”是每一个实例的机制(Initialize the "state" per instance mechanism)
    int LandSatImage::_count = 1;

     SpotImage.h

    class SpotImage : public Image
    {
    public:
        imageType returnType() {
            return SPOT;
        }
        void draw() {
            cout << "SpotImage::draw " << _id << endl;
        }
        Image *clone() {
            return new SpotImage(1);
        }
    protected:
        SpotImage(int dummy) {
            _id = _count++;
        }
    private:
        SpotImage() {
            addPrototype(this);
        }
        static SpotImage _spotImage;
        int _id;
        static int _count;
    };
    SpotImage SpotImage::_spotImage;
    int SpotImage::_count = 1;

     main.cpp

    // 模仿创建请求的流 (Simulated stream of creation requests)
    const int NUM_IMAGES = 8;
    imageType input[NUM_IMAGES] =
    {
        LSAT, LSAT, LSAT, SPOT, LSAT, SPOT, SPOT, LSAT
    };
    
    int main()
    {
        Image *images[NUM_IMAGES];
        // 给予一个image类型,找到与之对应的原型,然后返回一个clone对象
        for (int i = 0; i < NUM_IMAGES; i++)
            images[i] = Image::findAndClone(input[i]);
        // 示例:正确的image对像被clone
        for (i = 0; i < NUM_IMAGES; i++)
            images[i]->draw();
        // 释放动态内存
        for (i = 0; i < NUM_IMAGES; i++)
            delete images[i];
    }

     其中 LSAT 就是 LastSatImage。

    3. clone() 等于 拷贝。

  • 相关阅读:
    第123讲:Hadoop集群管理之Namenode目录元数据结构详解学习笔记
    看待类和对象/C++的访问修饰符的作用
    c++之 reference vs point转
    关于 《C++网络编程+卷1+运用ACE和模式消除复杂性》的源码及例子
    C++之 new转
    第二次作业案例分析
    第一次作业四则运算
    【博客观后感】
    hello
    hlt指令
  • 原文地址:https://www.cnblogs.com/fengyubo/p/4806294.html
Copyright © 2020-2023  润新知