C++设计模式_创建型模式_单件模式

本文记录单例模式。

单例模式又称为单例模式,是一种创建型模式,适用于产生一个对象的示例。

使用场景:项目中只存在一个对象,比如声音管理系统,一个配置系统,一个文件管理系统,一个日志系统,一个线程池等。

单件类的实现和特点

一个单例模式的实现。

cpp 复制代码
class GameConfig
{
private:
	GameConfig() {}
	GameConfig(const GameConfig&) {}
	GameConfig& operator=(const GameConfig&) {}

public:
	static GameConfig* getInstance()
	{
		if (_instance == NULL)
		{
			_instance = new GameConfig();
		}
		return _instance;
	}


private:
	static GameConfig* _instance;
};

GameConfig* GameConfig::_instance = nullptr;

void test()
{
	//GameConfig g1;		// 测试构造函数
	//GameConfig g2(g1);   // 测试拷贝构造函数	
	//GameConfig g3 = g1; // 测试拷贝构造函数
	//// 测试赋值运算符
	//GameConfig g4;
	//g4 = g1;
	// 测试单例
	GameConfig* g5 = GameConfig::getInstance();
	GameConfig* g6 = GameConfig::getInstance();
	if (g5 == g6)
	{
		cout << "g5 == g6" << endl;
	}
}

上面分别测试了单例类的构造函数,拷贝构造,拷贝赋值运算符。

单例模式的特点:

1 类的构造函数是私有的;

2 通过Public的静态成员函数getInstance() 创建单例类的对象;

3 将拷贝构造也设置为私有,保证对象可能被复制。

单件类在多线程中可能导致的问题

假如在多线程场景下使用单例模式:当线程1执行完if (instance == nullptr)这句话时,还未new GameConfig()对象,此时由于操作系统调度,切换到了线程2,此时线程2也会进入if (instance == nullptr),这样就可能导致了在多线程情况下,创建了两个单例类对象,这违背了开发者初衷,代码产生混乱。如何解决上面问题呢?

解决方式1:在if中加锁,代码如下所示。加锁方式解决了多个线程同时进入if条件的问题,但是随着而来的执行效率问题:一旦第一次创建成功后,后边即使多个线程也不会再执行_instance == nullptr的条件了,这种加锁的方式相当于对一个只读互斥量加锁,严重影响了程序执行效率。

cpp 复制代码
static GameConfig* getInstance()
{
	std::lock_guard<std::mutex> lock(_mutex);  // 
	if (_instance == NULL)
	{
		_instance = new GameConfig();
	}
	return _instance;
}

解决方式2:双重加锁。双重加锁代码如下,这种方式时为了提高多线程情况下效率,事实上这种方式确实减少了加锁的频率,提高的效率。这种方式在《C++并发编程实战》书上,作者很不赞同这种加锁方式,记录那个笔记时,再来看这种方式的缺点。

cpp 复制代码
static GameConfig* getInstance()
{
	if (_instance == nullptr)
	{
		std::lock_guard<std::mutex> lock(_mutex);  // 
		if (_instance == NULL)
		{
			_instance = new GameConfig();
		}
	}
	return _instance;
}

双重加锁代码的问题:内存访问重新排序,导致双重锁定失效的问题。上边的代码instance = new GameConfig(),这行代码大概分三个步骤完成:首先调用malloc分配内存,然后调用构造函数初始化这块内存,最后让instance 指向这块内存。但是,由于编译器优化等原因,上边的步骤被重新排序,执行顺序可能时132,这样麻烦就来了,因为instance指向这块new出来的内存,_instance == NULL就不成立了,表示已经new 成功了,可是这个new出来的内存并没有被初始化,当一个线程使用这个内存时,就会报错,这就是内存访问重新排序的问题。

实际使用时,先在main()中初始化,这样不必加锁也可实现线程的安全的单例模式。

书中提供的一个示例:

cpp 复制代码
class GameConfig
{
private:
	GameConfig() {};
	GameConfig(const GameConfig& tmpobj);
	GameConfig& operator = (const GameConfig& tmpobj);
	~GameConfig() {};
public:
	static GameConfig* getInstance()
	{
		GameConfig* tmp = m_instance.load(std::memory_order_relaxed);
		std::atomic_thread_fence(std::memory_order_acquire);
		if (tmp == nullptr)
		{
			std::lock_guard<std::mutex> lock(m_mutex);
			tmp = m_instance.load(std::memory_order_relaxed);
			if (tmp == nullptr)
			{
				tmp = new GameConfig();
				std::atomic_thread_fence(std::memory_order_release);
				m_instance.store(tmp, std::memory_order_relaxed);
			}
		}
		return tmp;
	}
private:
	static atomic<GameConfig*> m_instance;
	static std::mutex m_mutex;
};
std::atomic<GameConfig*> GameConfig::m_instance;
std::mutex GameConfig::m_mutex;

饿汉式与懒汉式

懒汉式单例模式在调用getInstance()时构造对象;

饿汉式单例模式在编译时候就构造对象;

饿汉式 单例模式

cpp 复制代码
// 饿汉式 单例模式
class GameConfig
{
private:
	GameConfig() {};
	GameConfig(const GameConfig& tmpobj);
	GameConfig& operator = (const GameConfig& tmpobj);
	~GameConfig() {};
public:
	static GameConfig* getInstance()
	{
		return m_instance;
	}

private:
	static GameConfig* m_instance;
private:
	class GC
	{
	public:
		~GC()
		{
			if (m_instance != nullptr)
			{
				delete m_instance;
				m_instance = nullptr;
			}
		}
	};
	static GC gc;
};
GameConfig* GameConfig::m_instance = new GameConfig();
GameConfig::GC GameConfig::gc;

懒汉式

cpp 复制代码
// 懒汉式单例模式另一种写法
class GameConfig
{
private:
	GameConfig() {};
	GameConfig(const GameConfig& tmpobj);
	GameConfig& operator = (const GameConfig& tmpobj);
	~GameConfig() {};

public:
	static GameConfig* getInstance()
	{
		return &m_instance;
	}
private:
	static GameConfig m_instance;
};
GameConfig GameConfig::m_instance;

单件类对象内存释放问题

上边的饿汉式单例模式中,在类中加了一个GC类,在GC类的析构函数中释放单例模式的对象。

单件类定义、UML图及另外一种实现方法

单件设计模式定义:保证一个类仅有一个实例存在,同时提供能对该实例访问的全局方法(getInstance成员函数)。

下面也是一种常见的单例模式写法。

cpp 复制代码
class GameConfig
{
private:
	GameConfig() {};
	GameConfig(const GameConfig& tmpobj);
	GameConfig& operator = (const GameConfig& tmpobj);
	~GameConfig() {};

public:
	static GameConfig& getInstance()
	{
		static GameConfig instance; // 局部静态变量,在运行时期初始化;
		return instance;
	}
};
void test()
{
	static int a = 100;  // 编译时期初始化

	GameConfig& g1 = GameConfig::getInstance();
}

单例模式UML

类和类之间是聚合关系。

相关推荐
青山是哪个青山5 分钟前
第一节:CMake 简介
linux·c++·cmake
晨晖222 分钟前
二叉树遍历,先中后序遍历,c++版
开发语言·c++
M__3324 分钟前
动规入门——斐波那契数列模型
数据结构·c++·学习·算法·leetcode·动态规划
wangchen_026 分钟前
C/C++时间操作(ctime、chrono)
开发语言·c++
setary03011 小时前
FastDDS之共享内存
c++
ShineSpark1 小时前
eventpp 全面教程(从入门到实战)
c++·后端
夏幻灵1 小时前
指针在 C++ 中最核心、最实用的两个作用:“避免大数据的复制” 和 “共享”。
开发语言·c++
FPGAI1 小时前
C++学习之函数
c++·学习
CC.GG1 小时前
【C++】STL----封装红黑树实现map和set
android·java·c++
violet-lz2 小时前
C++ 内存分区详解
开发语言·jvm·c++