【C++】 设计模式(单例模式、工厂模式)

文章目录

    • 设计模式
      • 概念
      • 单例模式
        • 懒汉式
          • 方法一
          • 方法二
          • 总结
        • 饿汉式
        • 单例模式的优点
      • 工厂模式
        • 概念
        • 简单工厂
        • 工厂方法
        • 抽象工厂
        • 三种工厂方法的总结

设计模式

概念

设计模式是由先人总结的一些经验规则,被我们反复使用后、被多数人知晓认可的、然后经过分类编排,形成了一套针对代码设计经验的总结,设计模式主要是为了解决某类重复出现的问题而出现的一套成功或有效的解决方案,设计模式的出现提高代码可复用性、扩展性、可维护性、灵活性、稳健性以及安全可靠性的解决方案。

设计模式一般分为3大类创建型模式结构型模式行为型模式,共计23种。

设计模式的本质是面向对象设计原则的实际运用,是对类的封装性、继承性和多态性以及类的纵、横关系的充分理解。

值得注意的是:设计模式只是一个程序设计指导思想,在具体的程序设计开发中,必须根据设计的应用系统的特点和要求来适当的选择,而不是一味地追求设计模式,否则可能过犹不及。

单例模式

单例模式(Singleton Pattern):属于创建型模式,3个要点:

  1. 当前的类最多只能创建一个对象(实例)
  2. 这个唯一的实例,由当前类创建
  3. 需要提供公共的接口,在整个系统中都可以访问。

写一个单例:

先把类名写出来

#include
using namespace std;

class CSingleton {

};
int main(){

return 0;
}

单例分为两种:懒汉式:现用现创建 饿汉式:提前创建

懒汉式

方法一

先写懒汉式

首先我们要满足单例模式的第一个要点,只能创建一个对象,那么如果我们不加以限制,我们想创建多少个对象都可以,所以要怎么去限制呢

因为我们在创建对象的时候自动调用默认的无参构造函数(public),所以如果将构造函数改为私有的,那么就不能在类外随意创建对象了,只能在类内进行创建

之后我们要提供一个公共的接口

class CSingleton {

	CSingleton(): m_a(0){}
public:
	static CSingleton* GetSingleton() {
		return new CSingleton;
	}
};

现在我们可以保证只能在类内创建对象,然后公共接口也有了

但是这样写还是有问题的,我们第一次调用这个接口会创建一个对象,然后再调用还会创建对象,所以现在是在类外控制住不能多次创建对象了,但是在类内还没有限制住

我们的目标是第一次调用这个函数会创建一个对象,而再去调用这个函数是要返回第一次创建的对象,所以就要用指针去接一下这个对象方便之后操作

在类中创建一个私有的当前类对象的指针

CSingleton* m_psin;

然后在接口函数中判断如果这个指针为空,那就创建一个对象,如果不为空就返回这个指针

CSingleton* GetSingleton() {
		if (!m_psin)
			m_psin = new CSingleton;
		return m_psin;
	}

此时单例模式的三点都已经满足

调试一下,为了方便查看,我们在类内加一个int型成员属性,然后在主函数获取这个接口

那么问题来了,我们没法在类外创建对象,那么怎么去获取这个接口呢

我们想到可以将接口变成静态函数,因为静态函数中只能使用静态成员属性,所以类对象指针也要变成静态的,然后通过类名作用域去调用

【C++】 设计模式(单例模式、工厂模式)_第1张图片

这样就可以在类外去获取这个接口了

	CSingleton* psin1 = CSingleton::GetSingleton();
	CSingleton* psin2 = CSingleton::GetSingleton();

然后测试一下获取到的对象是否是同一个对象,看一下两个指针指向的地址,然后再看一下两个指针指向的对象成员是不是同一个

	cout << psin1 << "   " << psin2 << endl;

	psin1->m_a = 11;
	cout << psin2->m_a << endl;

【C++】 设计模式(单例模式、工厂模式)_第2张图片

结果很明显,地址是一样的,并且一个指针更改了对象成员,另一个指针指向的对象中的成员也会改变,所以可以确定当前的类中只有一个对象。

但是这样写真的就能完全保证只创建一个对象了吗,如果我们用一个对象给另一个对象做初始化,那么还是会通过拷贝构造产生新的对象

那么我们可以直接将拷贝构造函数弃用

	CSingleton(const CSingleton&) = delete;

之后我们在用完这个对象我们还要将指针释放回收,我们可以在类外进行回收,但是更理想的办法是和构造函数一样我们在类内创建接口,然后回收的时候调用

所以我们在私有位置创建一个析构函数,那么在类外就不能随意释放指向对象的指针了

然后创建一个接口函数,参数为一个指针的引用,因为这样我们就可以知道在类外是用哪根指针来回收对象的了,如果指向对象的指针不为空,那么回收这跟指针,然后将指向对象的指针和回收时传入的指针都赋空

	static void DestorySingleton(CSingleton*& psin) {
		if (m_psin) {
			delete m_psin;
		}
		psin = m_psin = nullptr;
	}

调用回收接口,并判断指针是否为空

	CSingleton::DestorySingleton(psin1);
	if (!psin1) {
		cout << "回收单例后,这个指针为空了" << endl;
	}

【C++】 设计模式(单例模式、工厂模式)_第3张图片

那么还有个问题,就是如果我们一直使用这个单例,但最后忘了回收,那就无法释放这块空间了,我们能不能有个什么方法用来保底,最后一定会释放掉这块空间

我们在类中嵌套定义一个静态的类,并对他命名,作为单例类的成员属性,因为是静态的,所以在类外进行定义

	static class Destory {} m_destory;

因为这个类是单例类里面的,并且成员属性也是单例类里面的,所以在定义的时候要这么去定义:

CSingleton::Destory CSingleton::m_destory;//类外定义

我们就利用这个类的静态全局性,在这个类里面的析构函数回收掉单例类创建的对象空间

	static class Destory {
    public:
		~Destory() {
			if (m_psin) {
				delete m_psin;
			}
			m_psin = nullptr;
		}
	} m_destory;

这样就算我们不去手动回收单例,在程序结束时也会自动回收了 ,原理就是类中静态的对象最后会由编译器去回收,然后会默认调用这个嵌套类中的析构,然后我们在这个类的里面回收外层类中对象的空间

那么我们这个懒汉式单例写到这里就基本结束了,还有一点小小的问题现在还解决不了,就是在判断new创建单例的时候,在单线程下绝对是没有问题的,不过如果是多线程就不一定了,所谓多线程就是同时多个人去走这个代码,或者说是走这个判断,再去创建对象,就有可能创建出来多个对象,这个问题在以后学多线程的时候再去解决

解决方法就是加锁和解锁

【C++】 设计模式(单例模式、工厂模式)_第4张图片

方法二

第二种方法就是在创建对象的时候不去创建空间用指针接,而是直接创建一个静态局部对象,然后将地址返回,那么既然没有创建额外空间,就不用析构函数释放了

静态局部对象的特点就是只有第一次创建的时候会真正创建,后期在创建的时候就是直接使用第一次创建的对象

class CSingleton {

	CSingleton() : m_a(0) { /*cout << __FUNCTION__ << endl;*/ }
	CSingleton(const CSingleton&) = delete;  //弃用 拷贝构造函数
	~CSingleton() {}
public:
	//问题:在多线程下 可能会创建出来多个对象
	static CSingleton* GetSingleton() {
		static CSingleton sin;  //静态局部对象
		return &sin;
	}

	int m_a;
};
总结

这两种方法各有各的优缺点,

第二种方法的优点很明显,首先就是不需要考虑回收,而且不会有多线程下可能创建多个对象的问题

第一种方法的优点在于因为他的对象是new出来的,所以支持在运行过程中随时去回收,而第二种方法只能等到程序退出时资源才会被系统回收

饿汉式

就是将获取对象接口中的创建对象提取出来,让他只是获取对象,然后将静态局部对象尝试放在类中,作为一个类成员,而不是放在类中的函数中

不能舍弃静态直接在类内创建类的对象,这就像函数自己调用自己,会进入到死循环,计算空间的时候没有截至条件;可以定义静态对象是因为静态对象的属性不管是当前类的还是自己类的都不是属于类对象的,所以计算空间的时候不用算它,就可以计算出来具体空间大小

【C++】 设计模式(单例模式、工厂模式)_第5张图片

这样就做到了提前创建

做一个测试看构造函数的调用在主函数之前还是之后,所以主函数中先输出一条分割线,然后构造函数中输出对应的函数名,看函数名在分割线的前后即可

【C++】 设计模式(单例模式、工厂模式)_第6张图片

确实在主函数之前就调用构造函数了

这种方式也不会出现多线程创建多个对象的错误,并且不用创建析构回收额外空间

单例模式的优点

我们可以在任务管理器中看到单例模式的影子,就是一个程序的exe如果不通过特殊手段,只会打开一个,那可以打开多个文件夹管理器就有种多线程的感觉

从单例的单字可以看出来,严格保证了这个东西只能有一个,相对来说会节省空间,再就是他的创建和销毁相对来说没那么随意,比较严格,只能用提供的方法,所以跟一般的实例来比安全性比较高

那么优点就是:

  1. 单例模式提供了严格的对唯一实例的创建、访问和销毁,安全性高。
  2. 单例模式的实现可以节省系统资源。

工厂模式

概念

工厂模式(Factory Pattern):也是创建模式,主要用来集中创建对象的,如果在任何使用的地方创建对象那就造成了类或方法之间的耦合,如果要更换对象那么在所有使用到的地方都要修改一遍,不利于后期的维护,也违背了开闭设计原则,如果使用工厂模式来创建对象,那么就彻底解耦合了,如果要修改只需要修改工厂类即可。工厂模式最大的优势:解耦。

耦合的意思就是我直接用你的,你有改动我这边也会有改动,在一个函数中直接通过名字去调用另一个函数,这其实就是一个直接的耦合,如果通过传参传一个函数指针,这样就是解耦

简单工厂

这里要通过一些例子说明

车和发动机之间,现在有产品发动机类作为父类,然后子类有2L发动机和2T发动机,然后创建一个汽车类,因为汽车和发动机为包含关系,汽车也不能离开发动机,发动机也不能离开汽车,所以他们的关系为组合,所以在汽车类里面添加一个发动机类的对象,然后创建出构造函数,为了使对象更灵活一些,要再创建一个带参构造,参数为一个标识,用以区分判断发动机类别,然后创建一个汽车运行的方法,并且写出构造析构函数用来回收成员属性的额外空间。

class CEngine {
public:
	virtual void working() = 0;
};

class CEngine2L:public CEngine{
	virtual void working() {
		cout << "2.0自然吸气发动机正在工作" << endl;
	}
};

class CEngine2T :public CEngine {
	virtual void working() {
		cout << "2.0涡轮增压发动机正在工作" << endl;
	}
};

class CCar {
public:
	CEngine* m_pEngine;

	CCar():m_pEngine(new CEngine2L){
		
	}
	CCar(string & type) {
		if (type == "2.0L") {
			m_pEngine = new CEngine2L;
		}
		else if(type == "2.0T"){
			m_pEngine = new CEngine2T;
		}
		else {
			m_pEngine = nullptr;
		}
	}
    
	void drive() {
		if (m_pEngine) {
			m_pEngine->working();
			cout << "我的小汽车正在呜呜的跑~~~~" << endl;
		}
	}
	~CCar() {
		if (m_pEngine)
			delete m_pEngine;
		    m_pEngine = nullptr;
	}
};

最后在主函数中创建车的对象,然后调用驾驶的方法。如果没有学过工厂模式的话,面对这样的需求我们就是这么写的

那么现在知道了耦合解耦的思想之后,我们会发现这个代码不太好,如果发动机产生改动的话(比如类名做修改,或者构造函数增加参数了),那么在汽车类中给成员属性创建空间对象的时候也要做修改

接下来我们去解耦,把工厂引入进来

我们创建一个工厂类,然后在工厂里面创建发动机,创建的方法可以参考车类中的有参构造,传入一个类型,然后返回一个发动机类指针,然后在函数中做判断

最后在车类中再写一个带参构造,参数为工厂类指针和发动机标识,然后如果有工厂,那么工厂去创建一个发动机返回给发动机类指针,如果没有工厂,返回空即可

class CFactoryEngine {
public:
	CEngine* CreateEngine(string& type) {
		if (type == "2.0L") {
			return new CEngine2L;
		}
		else if (type == "2.0T") {
			return new CEngine2T;
		}
		else {
			return nullptr;
		}
	}
};
	CCar(CFactoryEngine* pFac, string& type) :m_pEngine(pFac ? pFac->CreateEngine(type) : nullptr) {

	}

这样我们发现,我们并没有在汽车类里面直接使用发动机类,而是通过工厂创建发动机,那么如果发动机类有什么变动,这里也不用有什么改动

接着我们去主函数中测试一下,先要创建一个工厂对象,然后将工厂的地址和发动机标识在创建车子的时候传入进去,注意在传参的时候不能直接传入字符串,我们写的是一个引用,要在外面先把对象定义出来

	CFactoryEngine fac;
	string s = "2.0T";
	CCar benz(&fac,s);
	benz.drive();
	
	string s1 = "2.0L";
	CCar bmw(&fac, s1);
	bmw.drive();

这就是一个简单工厂

工厂方法

简单工厂写的代码虽然没什么毛病,但是会有一些功能上的问题,就是如果产品的种类有增加或减少,工厂里面的逻辑就要有修改,那么工厂类里面的代码就会增加减少,违背了开闭原则(扩展支持,修改关闭)

所以在这个缺点的基础上,提出了工厂方法,把工厂再分出来几个子工厂,用具体工厂来创建具体产品

这样如果增加新的产品,总工厂不用做任何改动,只需要增加子工厂即可

class CFactoryEngine {
public:
	virtual CEngine* CreateEngine() = 0;
};

class CFactory2L :public CFactoryEngine {
	virtual CEngine* CreateEngine() {
		return new CEngine2L;
	}
};

class CFactory2T :public CFactoryEngine {
	virtual CEngine* CreateEngine() {
		return new CEngine2T;
	}
};

在车子类里的构造函数就不用再传递发动机标识了,可以直接根据传递的工厂对象不同来决定创建发动机的种类

	CCar(CFactoryEngine* pFac) :m_pEngine(pFac ? pFac->CreateEngine() : nullptr) {}

主函数中实现:

	CFactoryEngine* pFac2T = new CFactory2T;
	CFactoryEngine* pFac2L = new CFactory2L;

	CCar benz(pFac2T);
	benz.drive();

	CCar benz2(pFac2L);
	benz2.drive();

模型图:

【C++】 设计模式(单例模式、工厂模式)_第7张图片

类图:

【C++】 设计模式(单例模式、工厂模式)_第8张图片

工厂方法模式每个子类工厂对应一个具体类型的对象,比如CFactoryEngine2L对应CEngine2L对象,遵循了开闭原则,提高了开闭原则,提高了扩展性(如果增加了新的类型的发动机那么原有的发动机工厂不用改动),如果对象种类较多,那么一个都要对应一个工厂,需要大量的工厂得不偿失。

抽象工厂

抽象工厂(Abstract Factory Pattern)和工厂方法的模式基本一样,区别在于,工厂方法是生产一个具体的产品,而抽象工厂可以用来生产一组相同,有相对关系的产品,重点在于一组,一批,一系列。

模型图:

【C++】 设计模式(单例模式、工厂模式)_第9张图片

类图:

【C++】 设计模式(单例模式、工厂模式)_第10张图片

一系列产品,如:发动机和变速箱:

class CEngine {
public:
	virtual void working() = 0;
};

class CEngine2L:public CEngine{
	virtual void working() {
		cout << "2.0自然吸气发动机正在工作" << endl;
	}
};

class CEngine2T :public CEngine {
	virtual void working() {
		cout << "2.0涡轮增压发动机正在工作" << endl;
	}
};

class CGearbox {
public:
	virtual void working() = 0;
};

class CGearboxManual :public CGearbox {
	virtual void working() {
		cout << "手动变速箱正在工作" << endl;
	}
};

class CGearboxAuto :public CGearbox {
	virtual void working() {
		cout << "自动变速箱正在工作" << endl;
	}
};

抽象工厂类:

class CFactory {
public:

	virtual CGearbox* CreateGearbox() = 0;

	virtual CEngine* CreateEngine() = 0;
};

class CFactory2TManual :public CFactory {
	virtual CGearbox* CreateGearbox() {
		return new CGearboxManual;
	}

	virtual CEngine* CreateEngine() {
		return new CEngine2T;
	}
};

在汽车类中要创建发动机与变速箱的对象,然后在构造函数中设置参数为工厂类指针,然后给发动机和变速箱初始化的时候就用这跟指针调用相应的创建函数

汽车行驶函数中要先判断发动机与变速箱是否存在,然后他们去调用各自的运行函数,最终在析构函数里将两个对象回收

class CCar {
public:
	CEngine* m_pEngine;
	CGearbox* m_pGearbox;

	CCar(CFactory* pFac) :m_pGearbox(pFac ? pFac->CreateGearbox() : nullptr),
		m_pEngine(pFac ? pFac->CreateEngine() : nullptr) {

	}

	void drive() {
		if (m_pEngine&&m_pGearbox) {
			m_pEngine->working();
			m_pGearbox->working();
			cout << "我的小汽车正在呜呜的跑~~~~" << endl;
		}
	}
	~CCar() {
		if (m_pGearbox)
			delete m_pGearbox;
		m_pGearbox = nullptr;

		if (m_pEngine)
			delete m_pEngine;
		m_pEngine = nullptr;
	}

};

在主函数中使用的时候就先将发动机与变速箱的组合对象用工厂类指针指向,然后在创建汽车对象的时候将这个指针传入参数,汽车就可以根据想要的组合创建出来了,然后可以调用行驶函数

	CFactory* pFac = new CFactory2TManual;
	CCar benz(pFac);
	benz.drive();

对于具体工厂类可以根据需求自由组合,如需求增加CEngine2L和CGearboxAuto的组合,则新建工厂类CFactory2LAuto即可。

三种工厂方法的总结

  1. 对于简单工厂和工厂方法来说,两者的使用方式实际上是一样的,如果对于产品的分类和名称是确定的,数量是相对固定的,推荐使用简单工厂模式。
  2. 抽象工厂用来解决相对复杂的问题,适用于一些列、大批量的对象生产。

你可能感兴趣的:(C++进阶之路,单例模式,c++,设计模式)