C++单例模式介绍

1.C++中的设计模式

设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。设计模式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。

2.单例模式

2.1什么是单例模式

一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。

优点:
(1)由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建、销毁时,而且创建或销毁时性能又无法优化,单例模式的优势就非常明显
(2)减少了系统的性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决
(3)避免对资源的多重占用。如避免对同一个资源文件的同时写操作
(4)单例模式可以在系统设置全局的访问点,优化和共享资源访问

缺点:
单例模式一般没有接口,扩展困难。不利于测试

2.2C++中单例模式的实现

单例模式是只能有一个实例化的对象的类。这个类就要禁止别人new出来,或者通过直接定义。在C++中,类对象被创建时需要操作系统为对象分配内存空间,并自动调用构造函数初始化对象。所以我们需要把类的构造函数私有化,禁止生成其他的实例化对象。构造函数被私有化后,就只能被类内部的成员函数调用,所以我们还需要一个公有函数供类外部调用。然后这个函数返回一个对象。为了保证多次调用这个函数返回的是一个对象,我们可以把类内部要返回的对象设置为静态变量。且应该把这个静态成员设置为 null,在共有函数里去判断,只有在静态实例成员为 null时,也就是没有被初始化的时候,才去初始化它且只被初始化一次

单例模式的特征总结:
1、一个类只有一个实例
2、提供一个全局访问点
3、禁止拷贝

实现步骤:
1、实现只有一个实例,将构造函数声明为私有
2、提供一个全局访问点,类中创建静态成员和静态成员方法
3、禁止拷贝,把拷贝构造函数声明为私有,并且不提供实现,将赋值运算符声明为私有,防止对象的赋值。

2.3懒汉模式

懒汉模式,故名思意,一个人很懒不愿意主动做事,只有你催他他才会动起来。即只有在主动调用静态成员函数的时候才会实例化的对象,以时间换空间。
优点:简单
缺点:可能会导致进程启动慢,且如果有多个单例类对象实例启动顺序不确定

#include<iostream>
using namespace std;
class Singleton
{
public:
	static Singleton* GetInstance()
	{
	//第一次调用获取实例的函数时,静态类的变量指针空,所以会创建一个对象出来,第二次调用就不是空了,直接返回第一次的对象指针(地址)
		if (instance == nullptr)
		{
			instance = new Singleton();
		}
		return instance;
	}

private:
	Singleton()//构造函数私有化
	{
		cout << "实例化了" << count << "个对象" << endl;
		count++;
	}
	// C++98防拷贝
	//Singleton(Singleton const&);     
	//Singleton& operator=(Singleton const&);    
	// C++11防拷贝  
 	Singleton(Singleton const&) = delete;     
	Singleton& operator=(Singleton const&) = delete; 
	
	int count = 1;
	static Singleton* instance;//全局访问点
};
Singleton* Singleton::instance = nullptr;
int main()
{
 	//只有我们调用GetInstance时才会生成对象(懒汉)
	Singleton* p=Singleton::GetInstance();
	cout << p << endl;
	Singleton* p2=Singleton::GetInstance();
	cout << p2 << endl;
	Singleton* p3=Singleton::GetInstance();
	cout << p3 << endl;
	system("pause");
	return 0;
}

三个地址一样,证明我们的单例类的正确的。
在这里插入图片描述

2.3.1多线程下的懒汉模式

上述代码在单线程的情况下,运行正常,但是遇到了多线程就出问题,假设有两个线程同时运行了这个单例类,同时运行到了判断 if 语句,并且当时instance 为空,那么两个线程都会去运行并创建初始化实例,此时就不满足单例类的要求了。这种情况我们需要通过加一个锁🔒去解决问题。

这里我们使用的时双重检查锁(double-check),能够保证效率和安全。

#include <iostream>
#include <mutex>
#include <thread>
using namespace std;
class Singleton
{
public:    
	static Singleton* GetInstance() 
	{
		// 注意这里一定要使用Double-Check的方式加锁,才能保证效率和线程安全
		//双重检测机制,提高了单例模式在多线程下的效率,因为这样的代码,只需要在第一次创建实例的时候,需要加锁,
		//其他的时候,线程无需排队等待加锁之后,再去判断了,比较高效。
		if (instance==nullptr) {
			m_mtx.lock();
			if (instance==nullptr) {
				instance = new   Singleton();
			}
			m_mtx.unlock();
		}
		return instance;
	}
	class CGarbo //实现一个内部类来完成析构 防止内存泄漏	
	{ 
	public:
		~CGarbo() { 
			if (Singleton::instance)
				delete Singleton::instance; 
		} 
	};
private:    
	// 构造函数私有    
	Singleton()
	{
		cout << "实例化了" << count << "个对象" << endl;
		count++;
	}    
	int count = 1;
	// 防拷贝    
	Singleton(Singleton const&);    
	Singleton& operator=(Singleton const&);    
	static Singleton* instance; // 单例对象指针    
	static mutex m_mtx;            //互斥锁
	//单例类中声明一个触发垃圾回收类的静态成员变量,它的唯一工作就是在析构函数中删除单例类的实例,
	static CGarbo Garbo; //程序运行结束时,系统会调用Singleton的静态成员garbage的析构函数,该析构函数会删除单例的唯一实例
};
Singleton* Singleton::instance = nullptr; //静态成员类外声明
Singleton::CGarbo Singleton::Garbo;  
mutex Singleton::m_mtx; 
void func(int n) { 
	cout << Singleton::GetInstance() << endl; 
}
// 多线程环境下演示上面GetInstance()加锁和不加锁的区别。
int main()
{
	thread t1(func, 10);//thread创建线程t1 调用GetInstance()
	thread t2(func, 10);//thread创建线程t2 调用GetInstance()
	t1.join();//join()等待模式,执行完再执行下一个
	t2.join();
	cout << Singleton::GetInstance() << endl;//
	cout << Singleton::GetInstance() << endl;
	system("pause");
	return 0;
}

地址一样,证明我们的单例类的正确的。
在这里插入图片描述

有两个线程同时到达,即同时调用 GetInstanc(),此时Singleton==nullptr,所以两个线程都可以通过第一重if语句。进入第一重 if 语句后,由于存在锁机制,所以会有一个线程进入 lock 语句并进入第二重singleton = = null ,而另外的一个线程则会在 lock 语句的外面等待。
当第一个线程执行new Singleton()语句后,便会退出锁定区域,此时,第二个线程便可以进入 lock 语句块,此时,如果没有第二重 Singleton = = nullptr 的话,那么第二个线程还是可以调用 new Singleton()语句,这样第二个线程也会创建一个 Singleton 实例,这样也还是违背了单例模式的初衷的。所以这里必须要使用双重检查锁定。

考虑在没有第一重 if(Singleton = = nullptr)的情况下,当有两个线程同时到达,此时,由于 lock 机制的存在,第一个线程会进入 lock 语句块,并且可以顺利执行 new Singleton(),当第一个线程退出 lock 语句块时, Singleton 这个静态变量已不为 null 了,所以当第二个线程进入 lock 时,还是会被第二重singleton = = nullptr 挡在外面,而无法执行 new Singleton()。
所以在没有第一重 singleton = = nullptr的情况下,也是可以实现单例模式的。那么为什么需要第一重 singleton = = nullptr 呢?
这里就涉及一个性能问题了,因为对于单例模式的话,new Singleton()只需要执行一次就可以了,而如果没有第一重 singleton = = nullptr 的话,每一次有线程进入 GetInstance()时,均会执行锁定操作来实现线程同步,这是非常耗费性能的,而如果我加上第一重 Singleton = = nullptr 的话,那么就只有在第一次,也就是 Singleton = =nullptr 成立时的情况下执行一次锁定以实现线程同步,而以后的话,便只要直接返回 Singleton 实例就 OK 了而根本无需再进入 lock 语句块了,这样就可以解决由线程同步带来的性能问题了。

2.4饿汉模式

饿汉模式,故名思意:人饿了就要吃东西来填饱肚子,所以需要提前准备好食物。即在初始化静态成员的时候进行实例化对象,以空间换时间。
由于要进行线程同步,所以在访问量比较大,或者可能访问的线程比较多时,采用饿汉实现,可以实现更好的性能。

class Singleton
{
private:
    Singleton(){}//构造函数私有
    static Singleton *instance;//提供全局访问点 静态成员
    // C++98防拷贝
	//Singleton(Singleton const&);     
	//Singleton& operator=(Singleton const&);    
	// C++11防拷贝  
 	Singleton(Singleton const&) = delete;     
	Singleton& operator=(Singleton const&) = delete; 
public:
    static Singleton* GetInstance() {
        return instance;
    }
};
Singleton* Singleton::instance = new Singleton();//饿汉模式的关键:初始化即实例化

类中的静态变量在外部声明的时候就可以new一个对象出来,因为instance是Singleton的成员,它是可以调用构造函数。锁也不用加了,因为我们调用Singleton::GetInstance()之前这个类就已经被实例化了,属于线程安全。我们调用这个函数的目地只是为了得到这个对象的地址。以空间换时间。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C++ 单例模式是一种常用的设计模式,它的主要目的是确保一个类只有一个实例,并提供全局访问该实例的方法。它通常被用来管理全局资源,例如日志记录器、配置文件、数据库连接等。 实现单例模式的关键在于确保类只有一个实例,并提供全局访问该实例的方法。在 C++ ,可以通过静态成员变量和静态成员函数来实现这一点。 例如,下面是一个简单的 C++ 单例模式的实现: ```cpp class Singleton { private: static Singleton* instance; // 静态成员变量,用于保存单例实例 Singleton() {} // 构造函数私有化,保证外部无法直接创建实例 public: static Singleton* getInstance() { // 静态成员函数,用于获取单例实例 if (instance == nullptr) { instance = new Singleton(); // 如果实例不存在,就创建一个新的实例 } return instance; // 返回单例实例 } }; Singleton* Singleton::instance = nullptr; // 初始化静态成员变量 ``` 在上面的例子,我们将构造函数私有化,这样就可以防止外部直接创建实例。同时,我们使用静态成员变量 `instance` 来保存单例实例,并使用静态成员函数 `getInstance` 来获取单例实例。在 `getInstance` 函数,我们首先检查实例是否已经存在,如果不存在就创建一个新的实例。最后,我们返回单例实例。 使用单例模式时需要注意线程安全问题,可以使用线程安全的实现方式来避免多线程问题。此外,单例模式也有一些缺点,例如可能会导致代码耦合性增加、难以进行单元测试等。因此,在使用单例模式时需要谨慎考虑。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值