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];
        //只保存一个\0
        *mychar = '\0';
    }
}
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 构造\n");
    }
    ~Person() {
        printf("Class Person 析构\n");
    }
    void say() {
        printf("Hello");
    }
};

class Family {
public:
    Family() {
        printf("Class Family 构造\n");
    }
    ~Family() {
        printf("Class Family 析构\n");
    }
    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 构造\n"); }
    ~Female() { printf("Class Female 析构\n"); }
};

class Girl : public Female {
public:
    Girl() { printf("Class Girl 构造\n"); }
    ~Girl() { printf("Class Girl 析构\n"); }
};
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\n");
    return 0;
}

//定义一个长方形类,public继承Shape
class Rectangle :public Shape {
public:
    Rectangle() {}
    //实现drow(),这是必须实现的
    void drow() const {
        printf("Rectangle Drow\n");
    }
    //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();

 

posted @ 2019-06-25 00:23  风间悠香  阅读(579)  评论(0编辑  收藏  举报