• C++程序设计1(侯捷video 7-13)(big three、stack heap、new delete、内存分配、static、模板、namespace、复合、委托、虚函数、重载覆盖改写、设计模式)


    一、Big three(拷贝构造、拷贝赋值、析构函数)(video7)

    Big three指三个特殊函数,分别是拷贝构造函数、拷贝赋值和析构函数。

    什么时候需要拷贝构造、拷贝赋值、析构函数:

      当类中的数据是指针时,例如string类中保存字符串使用char *,那么就不能直接使用编译器给的默认Big three。因为默认的函数是按字节拷贝的,这样拷贝后的对象中的指针指向的位置和被拷贝的对象一样,这样不是真正的拷贝。

    class mystring {
    public:
        //普通构造函数
        mystring(const char* chr = 0);
        //拷贝构造函数
        mystring(const mystring& mstr);
        //析构函数
        ~mystring();
    
        //拷贝赋值
        mystring& operator = (const mystring& mstr) {
            //检测是否为自我赋值
            if (this == &mstr) {
                return *this;
            }
            //先将已有的空间释放,否则会内存泄漏
            delete[] mychar;
            //创建新的空间,大小和mstr.mychar指向空间一样大
            mychar = new char[strlen(mstr.mychar) + 1];
            //赋值内容
            strcpy(mychar, mstr.mychar);
            //返回等号左边的mystring对象,以防连续赋值
            return *this;
        }
    private:
        //变量是指针,必须实现拷贝赋值和拷贝构造
        char * mychar;
    
    };
    
    inline mystring::mystring(const char* chr) {
        //判断传入的指针是否为空(或默认为空)
        if (chr) {
            //如果不为空,则按chr的大小分配空间,并让mychar指向该空间
            mychar = new char[strlen(chr) + 1];
            //将chr的数据复制到mychar指向的空间中
            strcpy(mychar, chr);
        //如果指针为空
        }else {
            //创建一个大小为1的空间
            mychar = new char[1];
            //只保存一个
            *mychar = '';
        }
    }
    inline mystring::mystring(const mystring& mstr) {
        //分配一个和mstr.mychar字符串一样大的空间,并让mychar指向该空间
        mychar = new char[strlen(mstr.mychar) + 1];
        //将内容复制到mychar指向的空间中
        strcpy(mychar, mstr.mychar);
    }
    inline mystring::~mystring() {
        //当对象生命周期要结束的时候,必须清理内存(堆空间),否则会内存泄漏
        delete[] mychar;
    }

    在上述代码中,拷贝构造函数做的事情实际上是深拷贝,而默认的拷贝构造函数做的事情是浅拷贝(只复制mstr.mychar指针的4byte到mychar中)。如下图所示:

    代码中,以下部分非常重要:

    //检测是否为自我赋值
    if (this == &mstr) {
        return *this;
    }

    如果this和mstr是同一个对象,那么如果没有自我赋值的检测,可能会导致程序出错。

    因为我们在拷贝数据之前,第一步就是先delete[] mychar,那么也就是删除了mstr.mychar指向的空间。

    第二步我们要参照mstr.mychar指向空间的大小来开辟空间就会出问题,更别提复制其中的内容。

    所以,自我赋值检测非常重要。

    二、为mystring类添加<<重载函数(video7)

    //定义成员方法,获取mychar指针,不修改数据,加上const
    inline char * mystring::get_c_str() const {
        return this->mychar;
    }
    //重载操作符<<,使可以直接cout<<mystring_obj;
    inline ostream& operator << (ostream& os, const mystring& mstr) {
        os << mstr.get_c_str();
        return os;
    }

    三、stack和heap(video8)

    Stack栈:存在于某个作用域(scope)的一块内存空间。例如当调用函数,函数本身即会形成一个stack用来放置它所接受的参数,以及返回地址。

    Heap堆:是指由操作系统提供的一块global内存空间,程序可动态分配从中获得若干块(blocks)。当使用完毕后,有责任主动去释放这块内存。

    //存在一个类(忽略定义)
    class Complex {};
    
    //Global Object,存在于全局作用域,直到程序结束
    Complex c4(1.0, 2.0);
    //某个作用域,例如某个函数内部
    {
        //Stack Object,出作用域时自动清理,也叫Auto Object
        Complex c1(1.0,2.0);
        //c2指向的对象存在于heap堆中,叫Heap Object
        Complex * c2 = new Complex(1.0, 2.0);
        //static Object,出作用域该对象仍然存在,直到程序结束
        static Complex c3(1.0, 2.0);
    
        delete c2;
    }

    上述代码中:

    c1对象是放在stack中的(c1中如果存在指针指向的空间,那么那块空间还是在堆里,但在c1生命周期完结的时候,有析构函数去释放他们)。

    c2指针指向的对象是放在堆空间的,new关键字表示在堆空间中分配地址存在对象。该作用域的栈中只存放了c2这个指针(4 bytes)。记得使用完后在作用域内使用delete p;释放空间,否则会内存泄漏。

    c3叫静态对象,该对象存在直到程序结束。

    c4叫全局对象,该对象是在所有local scope外定义(全局作用域),存在直到程序结束。

    四、new和delete关键字的工作流程(video8)

    new的流程:

    Complex * px = new Complex(1, 2);

    编译器自动转化为以下操作:

    Complex * px; //栈中分配一个指针空间
    
    void *mem = operator new (sizeof(Complex)); //堆中分配空间,底层使用的是C语言的malloc(n)
    pc = static_cast<Complex*>(mem); //将void*转型为Complex*
    pc->Complex::Complex(1, 2); //调用构造函数相当于Complex::Complex(pc,1,2)  pc表示this指针

    简单介绍static_cast:static_cast<new_type>(expression)和C语言的类型强转是一样的。他们的区别一句话总结:static_cast在编译时会进行类型检查,而强制转换不会。

    delete的流程:

    mystring *ps = new mystring("hello");
    delete ps;

    编译器自动转化为以下操作:

    mystring::~mystring(); //先调用析构函数,将对象中指针变量指向的空间释放掉
    operator delete(ps); //然后再释放自己所占的堆空间,底层调用的是C的free(ps)

    使用delete释放空间时遵循层次释放,如果对象里存在指向其他对象的指针(甚至多层嵌套),那么delete必须从最里层开始调,逐级释放内存。

    五、VC当中的内存分配(video8)

    假设我们要new一个Complex对象,该对象大小为8bytes。在Debug和Release模式下,内存分配是不同的。

    注:仅限new的情况下。

    Debug(左)Release(右)模式下:

            

    浅绿色:Complex对象所占实际空间,大小为8bytes。

    上下砖红色:各4bytes,一共8bytes。是cookie,用来保存总分配内存大小,以及标志是给出去还是收回来。例如00000041,该数为16进制,4表示64,即总分配内存大小为64,1表示给出去(0表示收回来)。

    灰色:Debug模式下使用的额外空间,前面32bytes,后面1bytes,一共36bytes。

    深绿色:内存分配大小必须是16的倍数(这样砖红色部分里的数字最后都是0,可以用来借位表示给出去还是收回来),所以用了12byte的填充(padding)。

    同样,String对象的空间分配,如图:(左Debug,右Release)

            

    六、数组的内存分配(video8)

    //使用new分配数组空间
    char * m_data = new char[strlen(cstr) + 1];
    //使用delete[]来释放数据空间
    delete[] m_data;

    对数组的空间分配,new叫做Array new,delete[]叫做Array delete。这两个要搭配使用,否则会出错。

    注:仅限new的情况下。

    Debug(左)Release(右)模式下,数组空间的分配:

            

    灰色:即3个Complex对象的大小,每个是8bytes,一共24bytes。

    深绿色:填充为16的倍数。

    前后白色:51表示80bytes,“给出去”。

    黄色:Debug模式额外占用空间。

    中间白色:用一个整数表示数组中对象个数。

    Array new必须搭配Array delete使用,不然会有以下后果(内存泄漏):

    使用Array delete,操作系统才知道,我要释放的是一个数组,那么会根据数组中元素的个数分别调用元素对象的析构函数,确保所有元素对象内部所指向的内存空间完全释放。然后再通过free来释放数组本身。

    七、static静态关键字(video10)

    前面所实例中,没有涉及static关键字。对象的属性会存放在每个对象相应的位置,也就是说,有几个对象,数据就有几份。但是类中的成员方法只有一份,那么不同的对象在调用一个成员方法的时候,是通过以下步骤来分别处理自己的数据的:

    Complex c1, c2, c3;
    //c1,c2,c3分别调用real()来返回实部的值
    cout << c1.real() << endl;
    cout << c2.real() << endl;
    cout << c3.real() << endl;

    相当于以下伪代码:

    Complex c1, c2, c3;
    cout << Complex::real(&c1); //this == &c1
    cout << Complex::real(&c2); //this == &c2
    cout << Complex::real(&c3); //this == &c3

    即,谁调用real()成员方法,谁就是this指针指向的对象。以此来分别处理自己的数据。

    当成员属性或成员方法前面加上static关键字,那么该属性或方法就和对象脱离了。

    静态属性和一般的成员属性不同,一般的成员属性是每个对象各有一份,而静态数据一共只有一份,例如一个类叫账户,有100W个人来开户,每个人是一个账户对象,里面的金额等是普通属性,每个账户中都有这个属性。但是利率则应该是静态属性,全局只有一份,因为每个人的存款利率应该是一致的。

    静态方法和一般成员方法是一样的,也只有一份。但是静态方法的不同点是,静态方法没有this指针。静态方法只能操作静态属性。

    class Account{
    public:
        //m_rate的声明
        static double m_rate;
        static void set_rate(const double r) {
            m_rate = r;
        }
    };
    //m_rate的定义
    double Account::m_rate = 0.89;

    使用:

    //第一种调用方式,使用类名直接调用
    cout << Account::m_rate << endl;
    //第二种调用方式,使用对象来调用
    Account a;
    a.set_rate(0.50);
    cout << a.m_rate << endl;

    八、利用static实现单例模式(Singleton)(video10)

    class A {
    public:
        //对外接口,获取实例(该实例是唯一的)
        static A& getInstance();
        //任意函数,供单例对象调用
        void setup() { cout << "here is A.setup" << endl; }
    private:
        //构造函数放在private下,外部无法直接使用构造函数实例化对象
        A(){}
        //用于保存那唯一的一份对象
        static A a;
    };
    
    A& A::getInstance() {
        //第一次调用时,产生静态的成员属性A a
        static A a;
        //返回
        return a;
    }

    使用:

    A& p = A::getInstance();
    p.setup();

    九、模板(video10)

    类模板:

    //创建模板T
    template<typename T>
    class Complex {
    public:
        Complex(T r=0,T i=0):re(r),im(i){}
        
        Complex& operator += (const Complex&);
        T real() const { return re; }
        T imag() const { return im; }
        
    private:
        T re;
        T im;
    
        friend Complex& __doapl(Complex*, const Complex&);
    };

    使用:

    {
        //当使用double类型时,编译器会将Complex类定义中的T全部替换为double,产生一份代码
        Complex<double> c1(1.0, 2.2);
        //当使用int时,全部替换为int,又产生一份代码
        Complex<int> c2(4, 5);
    }

    但是,为不同的类型产生不同的代码是必要的,并不是缺点。因为如果手工去按类型定义类的话,同样是多份代码。

    函数模板:

    当我们设计一个函数可以对多种类型的数据使用时,例如:

    template<class T>
    inline T& min(T& a,T& b){
        return a < b ? a : b;
    }

    函数模板在调用时无需使用<type>来指定,因为要使用函数模板,一定是调用函数,那么就会传实参,编译器就会进行实参推导。

    函数模板和类模板是相同的,template中使用的typename和class也是相通的,可以替换使用。

    十、命名空间namespace(video10)

    namespace主要用来解决对人协调工作时的命名冲突问题。相当于把自己的代码包装一层,别人使用的时候可以以以下三种方式使用,我们以std为例:

    //将命名空间std全部打开
    using namespace std;
    int main() {
        //直接使用其中的方法
        cin << ...;
        cout << ...;
    }
    //按需打开,指定拿出的方法
    using std::cout;
    int main() {
        //未指定的需要使用std::来使用
        std::cin << ...;
        cout << ...;
    }
    //完全使用std::func_name来使用
    int main() {
        std::cin << ...;
        std::cout << ...;
    }

    如何创建namespace:

    //直接将代码包含在namespace leo内部
    namespace leo {
    
        class Account {
        public:
            //m_rate的声明
            static double m_rate;
            static void set_rate(const double r) {
                m_rate = r;
            }
        };
        //m_rate的定义
        double Account::m_rate = 0.89;
    }

    面向对象编程、面向对象设计

    类与类之间的三种关系:复合、集成、委托。

    十一、复合Composition(video11)

    复合表示has-a。即A类里有B类的对象(非指针)。

    复合的图形表示,及构造和析构顺序:

    如下代码所示:

    class Person {
    public:
        Person() {
            printf("Class Person 构造
    ");
        }
        ~Person() {
            printf("Class Person 析构
    ");
        }
        void say() {
            printf("Hello");
        }
    };
    
    class Family {
    public:
        Family() {
            printf("Class Family 构造
    ");
        }
        ~Family() {
            printf("Class Family 析构
    ");
        }
        Person& getPerson() {
            return p;
        }
    private:
        //Family中存在Person对象p
        Person p;
    };

    Family类中,存在一个Person对象。那么在初始化Family对象的时候,会根据上图里的先后顺序来构建对象。

    void test() {
        Family f;
    }
    
    int main() {
        test();
        return 0;
    }

    打印的内容:

    Class Person 构造
    Class Family 构造
    Class Family 析构
    Class Person 析构

    所以,我们可以看出,在定义Family对象f时,先调用的是Person的构造函数,然后再调用Family的构造函数,是一个由内而外的过程。

    而在test()函数结束时,f对象的生命周期完结,是先调用了Family的析构函数,然后再调用Person的析构函数,是一个由外而内的过程。

    十二、委托Delegation(video11)

    委托表示A类里有B类的对象的指针。委托也可以叫 “ 基于引用的复合(Composition by reference)”。

    当A拥有B对象的指针,那么在任何时间都可以通过指针来调用B对象做事情,所以叫做委托。

    设计模式:Handle/Body

    委托可以引申出一种非常出名的设计模式,叫Handle/Body,或叫Pointer to Implementation。即A为对外接口,B为具体实现,A中接口的操作全部调用B来完成,这样的好处就是A可以一直不变,B可以随意改变,甚至可以有多个不同的B实现方式。

    委托的图形表示为:

    一个典型的例子:

    上图中有abc三个A类(假设是String类)的对象,三个对象中的rep指针可以指向同一个B对象(实际存放字符串的类对象),假设abc是互相复制得来的,那么abc中存放的字符串应该是一样的,那么我们就可以在B中实现reference counting,即引用计数,只需要一个B对象,就可以支撑abc三个A的对象,但前提是都不对字符串内容给进行修改。这样就可以节省内存空间。

    当其中一个对象,例如a,要对字符串进行修改,那么可以单独再创建一个B对象给a单独修改,然后先前的B对象就从abc三个对象共享变为bc两个对象共享。这种操作叫做“Copy on write”

    十三、继承Inheritance(video11)

    继承的图形表示为:(T表示使用了模板,未使用则去掉T)

    继承(public继承)传达的逻辑是is-a,表示“是一种”。例如B继承A,则说明B是A的一种。

    代码如下:

    struct _List_node_base
    {
        _List_node_base* _M_next;
        _List_node_base* _M_prev;
    };
    
    template<typename _Tp>
    //_List_node继承_List_node_base(public方式继承)
    struct _List_node : public _List_node_base
    {
        _Tp _M_data
    };

     

    继承,相当于子类中包含了父类的成分(具体包含了多少,主要看继承的方式,public、protected、private)。

    class Female {
    public:
        Female() { printf("Class Female 构造
    "); }
        ~Female() { printf("Class Female 析构
    "); }
    };
    
    class Girl : public Female {
    public:
        Girl() { printf("Class Girl 构造
    "); }
        ~Girl() { printf("Class Girl 析构
    "); }
    };
    void test() {
        Girl();
    }
    
    int main() {
        test();
        return 0;
    }

    打印结果:

    Class Female 构造
    Class Girl 构造
    Class Girl 析构
    Class Female 析构

    在构建子类对象时,要先构建其内部的父类成分,所以要先调用父类的default构造函数,再调用自己的构造函数,由内而外。

    在销毁子类对象时,要先析构自己,再析构父类对象,由外而内。

    注意:父类的析构函数必须是virtual的。否则会出现undefined behavior。所以,在设计类的时候,如果一个类设计为父类,则析构函数就设计为虚拟函数,或者一个类以后有可能成为父类,那么也可以设计为虚拟析构函数。

    十四、虚函数 Virtual function(video12)

    子类继承父类的时候,在public继承方式下,子类继承了所有父类的数据(成员属性),而且还继承了所有父类的函数调用权(只是调用的权利,函数还是只有父类的那一份)。

    父类方法分为三类:

    1.non-virtual函数:不希望子类(派生类derived class)重新定义(override,覆盖)它。override这个概念不能乱用,只有当子类重新定义基类同名虚函数时,才叫override。

    2.virtual函数:希望派生类去重新定义这个函数,而且已经对其有一个默认定义(默认实现)。

    3.pure virtual函数:希望派生类一定要重新定义这个函数,因为现在完全没有定义它(无默认定义)。

    class Shape {
    public:
        //纯虚函数,用virtual function = 0的形式
        virtual void drow() const = 0;
        //虚函数,应该有定义(可以是inline的,也可以在外部定义)
        virtual void error(const std::string& msg);
        //非虚函数,即普通函数
        int objectID() const;
    
        //Todo...
    };
    
    inline void Shape::error(const std::string& msg) {
        cout << msg << endl;
    }
    inline int Shape::objectID() const {
        printf("Function objectID
    ");
        return 0;
    }
    
    //定义一个长方形类,public继承Shape
    class Rectangle :public Shape {
    public:
        Rectangle() {}
        //实现drow(),这是必须实现的
        void drow() const {
            printf("Rectangle Drow
    ");
        }
        //override error(),实现子类的个性化操作
        void error(const string& msg) {
            cout << "Rect " << msg << endl;
        }
    };

    其中Shape类不能直接实例化对象,因为其中包含drow()方法,这是一个纯虚函数,必须由子类来override。

    所以我们要使用这个类的话,只能创建一个子类来继承他,然后覆写(override)他的所有纯虚方法,一般的虚函数(非纯虚)可以根据需求决定是否覆盖。

    十五*、overload override overwrite的区别(video12)

    参考来自:https://www.cnblogs.com/kuliuheng/p/4107012.html 感谢VictoKu

    1. Overload(重载)

     重载的概念最好理解,在同一个类声明范围中,定义了多个名称完全相同、参数(类型或者个数)不相同的函数,就称之为Overload(重载)。重载的特征如下:

    (1)相同的范围(在同一个类中);
    (2)函数名字相同;
    (3)参数不同;
    (4)virtual 关键字可有可无。

    2. Override(覆盖)

     覆盖的概念其实是用来实现C++多态性的,即子类重新改写父类声明为virtual的函数。Override(覆盖)的特征如下:

    (1)不同的范围(分别位于派生类与基类);
    (2)函数名字相同;
    (3)参数列表完全相同;
    (4)基类函数必须有virtual 关键字。

    3. Overwrite(改写)

     改写是指派生类的函数屏蔽(或者称之为“隐藏”)了与其同名的基类函数。正是这个C++的隐藏规则使得问题的复杂性陡然增加,这里面分为两种情况讨论:

    (1)如果派生类的函数与基类的函数同名,但是参数不同。那么此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
    (2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。那么此时,基类的函数被隐藏(注意别与覆盖混淆)。

    十六*、设计模式Template Method(video12)

    用虚函数和继承,实现一个非常有名的设计模式,叫Template Method。

    假设,我们要打开文件,并读取里面的内容,那么对于不同的文件,我们其实有很多步骤是相同的,例如找到文件目录,选中文件,打开文件,关闭文件等。但是可能我们有一个动作可能是各不相同的,例如读取其中的内容(各种文件内容格式不同,需要的处理也不同)。那么我们可以以下图中的做法:

    步骤:

    1.在父类CDocument中,实现共同的方法,例如OpenFile、CloseFile等。

    2.CDocument中,将读文件内容的方法Serialize设计为虚函数或纯虚函数。

    3.CMyDoc继承CDocument,实现Serialize()。

    4.使用子类CMyDoc调用父类方法OnFileOpen(),按图中灰色曲线的顺序来调用内部函数。

    这样就实现了关键功能的延迟实现,对于做应用架构(Application framework)的人来说,可以在1年前写好CDocument类,将这个架构卖给其他人,然后再由购买方自己来实现CMyDoc类。这就是典型的Template Method。

    为什么会有灰色曲线的调用过程:

    1.当子类myDoc调用OnFileOpen()的时候,实际上对于编译器是CDocument::OnFileOpen(&myDoc);因为谁调用,this指针就指向谁,所以调用这个函数,myDoc的地址被传进去了。

    2.当OnFileOpen()函数运行到Serilize()的时候,实际上是运行的this->Serialize();由于this指向的是myDoc,所以调用的是子类的Serilize()函数。

    代码示例:

    class CDocument {
    public:
        void OnFileOpen() {
            cout << "dialog..." << endl;
            cout << "check file status..." << endl;
            cout << "open file..." << endl;
            Serialize();
            cout << "close file..." << endl;
            cout << "update status..." << endl;
        }
        //父类的虚函数,当然这里是纯虚函数也是可以的,virtual void Serialize() = 0
        virtual void Serialize() {}
    };
    
    class CMyDoc :public CDocument {
    public:
        //这里实现了父类的虚函数Serialize()
        virtual void Serialize() {
            cout << "MyDoc Serialize..." << endl;
        }
    };

    调用:

    #include "CDocument.h"
    
    int main() {
        CMyDoc mc;
        mc.OnFileOpen();
        return 0;
    }

    输出:

    dialog...
    check file status...
    open file...
    MyDoc Serialize...
    close file...
    update status...

    十七*、继承+委托的模式(video12)

    假设我们要做一个类似PPT的多窗口功能,即多个窗口观察同一份数据,例如多窗口画图。也可以是不同类型的窗口(例如,数字、图标、饼图、直方图等)来观察同一份数据。

    我们的数据设计在类Subject中,窗口(观察者)设计为Observer,这是一个父类,可以被继承(即可以支持派生出不同类型的观察者)。

    用如下代码来实现:

    //数据类
    class Subject {
    private:
        int m_value;
        //保存观察者指针(相当于开的窗口指针)
        vector<Observer*> m_views;
    public:
        //添加观察者
        void attach(Observer* obs) {
            m_views.push_back(obs);
        }
        void set_value(int value) {
            m_value = value;
            notify();
        }
        void notify() {
            //通知所有的观察者,数据有更新
            for (int i = 0;i < m_views.size();++i) {
                m_views[i]->update(this, m_value);
            }
        }
    };
    //观察者基类
    class Observer {
    public:
        //纯虚函数,提供给不同的实际观察者类来实现不同的特性
        virtual void update(Subject*, int value) = 0;
    };

    用图形来描述:

    十八*、Composite设计模式(组合模式)(video13)

      在计算机文件系统中,有文件夹的概念,文件夹里面既可以放入文件也可以放入文件夹,但是文件中却不能放入任何东西。文件夹和文件构成了一种递归结构和容器结构。
      虽然文件夹和文件是不同的对象,但是他们都可以被放入到文件夹里,所以一定意义上,文件夹和文件又可以看作是同一种类型的对象,所以我们可以把文件夹和文件统称为目录条目(directory entry)。在这个视角下,文件和文件夹是同一种对象。
      所以,我们可以将文件夹和文件都看作是目录的条目,将容器和内容作为同一种东西看待,可以方便我们递归的处理问题,在容器中既可以放入容器,又可以放入内容,然后在小容器中,又可以继续放入容器和内容,这样就构成了容器结构和递归结构。
      这就引出了composite模式,也就是组合模式,组合模式就是用于创造出这样的容器结构的。是容器和内容具有一致性,可以进行递归操作。

    图中Primitive代表基本的东西,即文件。Composite代表合成物,即文件夹。Component表示目录条目。

    Primitive和Composite都是一种Component,而Composite中可以存放其他的Composite和Primitive,所以Composite中的Vector存放的类型时Component指针,也就包含了Primitive和Composite两种对象的指针。

    代码框架如下:

    //一个比较抽象的类,相当于目录条目
    class Component {
    
        int value;
    public:
        Component(int val) :value(val){}
        virtual void add(Component*) {}
    };
    //相当于 文件类
    class Primitive {
    public:
        Primitive(int val):Component(val){}
    };
    //相当于 文件夹类
    class Composite {
        vector<Component*> c;
    public:
        Composite(int val) :Component(val){}
    
        void add(Component* elem) {
            c.push_back(elem);
        }
    };

    具体关于文件文件夹实例可以参照:https://www.jianshu.com/p/685dd6299d96 感谢:六尺帐篷

    十九*、Prototype模式 原型模式(video13)

    设计应用架构时,并不知道以后实现的子类名称,但有要提供给Client调用子类的功能怎么办?

    例如十年前设计的架构,子类在十年后继承父类并实现功能。Client只能调用架构中的父类,如何通过父类调用到不知道名字的子类对象。使用Prototype模式:

    #include <iostream>
    #include <vector>
    using namespace std;
    
    //可能是十年前写的框架,我们不知道子类的名字,但又希望通过该基类来产生子类对象
    class Prototype {
        //用于保存子类对象的指针(让子类自己上报)
        static vector<Prototype *> vec;
    public:
        //纯虚函数clone,让以后继承的子类来实现,也是获取子类对象的关键
        virtual Prototype* clone() = 0;
        //子类上报自己模板用的方法
        static void addPrototype(Prototype* se) {
            vec.push_back(se);
        }
        //利用该基类在vec中查找子类模板,并且通过模板来克隆更多的子类对象
        static Prototype* findAndClone(int idx) {
            return vec[idx]->clone();
        }
    
        //子类实现自己操作的函数,hello()只是个例子
        virtual void hello() const = 0;
    };
    //定义静态vector,很重要,class定义中只是声明
    vector<Prototype *> Prototype::vec;
    
    //十年后实现的子类,继承了Prototype
    class ConcreatePrototype : public Prototype{
    public:
        //用于在Prototype.findAndClone()中克隆子类对象用
        Prototype * clone() {
            //使用另一个构造函数,为了区分创建静态对象的构造函数,添加了一个无用的int参数
            return new ConcreatePrototype(1);
        }
    
        //子类实现的具体操作
        void hello() const {
            cout << "hello" << endl;
        }
    
    private:
        //静态属性,自己创建自己,并上报给父类Prototype
        static ConcreatePrototype se;
        //上报静态属性给父类
        ConcreatePrototype() {
            addPrototype(this);
        }
        //clone时用的构造方法,参数a无用,只是用来区分两个构造方法
        ConcreatePrototype(int a) {}
    };
    //定义静态属性,很重要,有了这句,才会创建静态子类对象se
    ConcreatePrototype ConcreatePrototype::se;

    步骤:

    1.子类继承Prototype父类,定义静态属性的时候,自己创建一个自己的对象,此时调用的是无参数的构造函数。并将创建好的自己的指针通过addPrototype(this)上传给基类的vector容器保存。

    2.基类定义好的纯虚函数clone(),由子类实现,并在其中通过另一个构造函数产生对象并返回。

    3.在Client端,使用基类的findAndClone(),获取vector中的子类对象模板的指针,来调用子类对象的clone功能,返回一个新的子类对象,调用多次则可创建多个对象供用户使用。

    4.创建出的子类对象可以调用在子类中实现的hello()方法,进行想要的操作。

    Prototype* p = Prototype::findAndClone(0);
    p->hello();
  • 相关阅读:
    吴裕雄 Bootstrap 前端框架开发——Bootstrap 辅助类:强制元素显示
    吴裕雄 Bootstrap 前端框架开发——Bootstrap 辅助类:清除浮动
    吴裕雄 Bootstrap 前端框架开发——Bootstrap 辅助类:设置元素为 display:block 并居中显示
    吴裕雄 Bootstrap 前端框架开发——Bootstrap 辅助类:元素浮动到右边
    吴裕雄 Bootstrap 前端框架开发——Bootstrap 辅助类:元素浮动到左边
    吴裕雄 Bootstrap 前端框架开发——Bootstrap 辅助类:表格单元格使用了 "bg-danger" 类
    C语言常见命名规范
    Method and apparatus for establishing IEEE 1588 clock synchronization across a network element comprising first and second cooperating smart interface converters wrapping the network element
    MVC5 Controller构造方法获取User为空解决方法
    C#中AssemblyInfo.cs文件详解
  • 原文地址:https://www.cnblogs.com/leokale-zz/p/11080516.html
Copyright © 2020-2023  润新知