C++ 单例模式_c 单例模式

C++ 单例模式_c 单例模式原创文章,转载请注明出处。本文主要从单例的用处以及问题所做介绍

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全家桶1年46,售后保障稳定

原创文章,转载请注明出处。

目录

C++ 单例模式介绍

一、单例是什么

二、C++实现单例

2.1 基础要点

2.2 C++ 实现单例的几种方式


C++ 单例模式介绍

单例可能是最简单的一种设计模式,实现方法很多种;同时单例也有其局限性。

本文对C++ 单例的常见写法进行了一个总结, 包括1>懒汉式版本、2>线程安全版本智能指针加锁、3>线程安全版本Magic Static; 按照从简单到复杂,最终回归简单的的方式循序渐进地介绍,并且对各种实现方法的局限进行了简单的阐述,大量用到了C++ 11的特性如智能指针,magic static,线程锁;从头到尾理解下来,对于学习和巩固C++语言特性还是很有帮助的。

一、单例是什么

单例是设计模式里面的一种,全局有且只有一个类的static实例,在程序任何地方都能够调用到。比如游戏客户端的本地Excel的加载,我们都会格式化成json,我习惯用单例做本地数据的管理。

二、C++实现单例

2.1 一个好的单例应该具备下面4点

  • 1.全局只有一个实例:static 特性,同时禁止用户自己声明并定义实例(把构造函数设为 private)
  • 2.线程安全
  • 3.禁止赋值和拷贝
  • 4.用户通过接口获取实例:使用 static 类成员函数

2.2 C++ 实现单例的几种方式

2.2.1 有缺陷的懒汉式

懒汉式(Lazy-Initialization)的方法是直到使用时才实例化对象,也就说直到调用Instance() 方法的时候才 new 一个单例的对象, 如果不被调用就不会占用内存。如果单线程没有问题,当多线程的时候就会出现不可靠的情况。

#include <iostream>
using namespace std;

/*
*	版本1 SingletonPattern_V1 存在以下两个问题
*	
*	1. 线程不安全, 非线程安全版本
*	2. 内存泄露
*/
class SingletonPattern_V1
{
private:
	SingletonPattern_V1() {
		cout << "constructor called!" << endl;
	}
	SingletonPattern_V1(SingletonPattern_V1&) = delete;
	SingletonPattern_V1& operator=(const SingletonPattern_V1&) = delete;
	static SingletonPattern_V1* m_pInstance;

public:
	~SingletonPattern_V1() {
		cout << "destructor called!" << endl;
	}
	//在这里实例化
	static SingletonPattern_V1* Instance() {
		if (!m_pInstance) {
			m_pInstance = new SingletonPattern_V1();
		}
		return m_pInstance;
	}
	void use() const { cout << "in use" << endl; }
};

//在类外初始化静态变量
SingletonPattern_V1* SingletonPattern_V1::m_pInstance = nullptr;

//函数入口
int main()
{
    //测试
	SingletonPattern_V1* p1 = SingletonPattern_V1::Instance();
	SingletonPattern_V1* p2 = SingletonPattern_V1::Instance();

	system("pause");
	return 0;
}

Jetbrains全家桶1年46,售后保障稳定

执行结果是 constructor called!

可以看到,获取了两次类的实例,构造函数被调用一次,表明只生成了唯一实例,这是个最基础版本的单例实现,他有哪些问题呢?

  1. 线程安全的问题,当多线程获取单例时有可能引发竞态条件:第一个线程在if中判断 m_pInstance是空的,于是开始实例化单例;同时第2个线程也尝试获取单例,这个时候判断m_pInstance还是空的,于是也开始实例化单例;这样就会实例化出两个对象,这就是线程安全问题的由来; 解决办法:加锁
  2. 内存泄漏. 注意到类中只负责new出对象,却没有负责delete对象因此只有构造函数被调用,析构函数却没有被调用;因此会导致内存泄漏。解决办法1:当然我们自己手动调用delete来进行释放是可以的,但是维护在何处释放又成了问题。正确决办法: 使用共享指针;

因此,这里提出一个改进的,线程安全的、使用智能指针的实现:

2.2.2 线程安全、内存安全的懒汉式单例 (C++11Shared_ptr,C++11 mutex lock)

#include <iostream>
using namespace std;
#include <memory> // C++11 shared_ptr头文件
#include <mutex>  // C++11 mutex头文件
/*
*	版本2 SingletonPattern_V2 解决了V1中的问题
*
*	1. 通过加锁让线程安全了
*	2. 通过智能指针(shareptr 基于引用计数)内存没有泄露了
*/
class SingletonPattern_V2 
{
public:
	~SingletonPattern_V2() {
		std::cout << "destructor called!" << std::endl;
	}
	SingletonPattern_V2(SingletonPattern_V2&) = delete;
	SingletonPattern_V2& operator=(const SingletonPattern_V2&) = delete;

	//在这里实例化
	static std::shared_ptr<SingletonPattern_V2> Instance() 
	{
		//双重检查锁
		if (m_pInstance == nullptr) {
			std::lock_guard<std::mutex> lk(m_mutex);
			if (m_pInstance == nullptr) {
				m_pInstance = std::shared_ptr<SingletonPattern_V2>(new SingletonPattern_V2());
			}
		}
		return m_pInstance;
	}

private:
	SingletonPattern_V2() {
		std::cout << "constructor called!" << std::endl;
	}
	static std::shared_ptr<SingletonPattern_V2> m_pInstance;
	static std::mutex m_mutex;
};

//在类外初始化静态变量
std::shared_ptr<SingletonPattern_V2> SingletonPattern_V2::m_pInstance = nullptr;
std::mutex SingletonPattern_V2::m_mutex;

int main()
{
	std::shared_ptr<SingletonPattern_V2> p1 = SingletonPattern_V2::Instance();
	std::shared_ptr<SingletonPattern_V2> p2 = SingletonPattern_V2::Instance();

	system("pause");
	return 0;
}

执行结果是 constructor called! destructor called!

优点

  • 基于 shared_ptr,内部实现的是基于引用计数的智能指针,每次实例被赋值或者拷贝,都会引用+1,在内部的析构中判断引用计数为0的时候会调用真正的delete。(cocos2D中就是基于这个做的垃圾回收)(UE4中也有专门的智能指针,我的文章链接)用了C++比较倡导的 RAII思想,用对象管理资源,当 shared_ptr 析构的时候,new 出来的对象也会被 delete掉。以此避免内存泄漏。
  • 加了锁,使用互斥锁来达到线程安全。这里使用了两个 if判断语句的技术称为双重检测锁;好处是,只有判断指针为空的时候才加锁,避免每次调用 get_instance的方法都加锁,锁的开销毕竟还是有点大的。

缺点

  • 使用智能指针会要求外部调用也得使用智能指针,就算用个typedef也是一长串代码不好维护且不美观。非必要不应该提出这种约束; 使用锁也有开销; 同时代码量也增多了,实际上设计最简单的才是最好的。
  • 其实还有双重检测锁某种程度上也是不可靠的:具体可以看这篇文章

因此这里还有第三种基于 magic static 达到线程安全的方式

2.2.3 最推荐的懒汉式单例(magic static)——局部静态变量C++ 单例模式_c 单例模式

#include <iostream>
using namespace std;
/*
*	版本3 SingletonPattern_V3 使用局部静态变量 解决了V2中使用智能指针和锁的问题
*
*	1. 代码简洁 无智能指针调用
*	2. 也没有双重检查锁定模式的风险
*/
class SingletonPattern_V3
{
public:
	~SingletonPattern_V3() {
		std::cout << "destructor called!" << std::endl;
	}
	SingletonPattern_V3(const SingletonPattern_V3&) = delete;
	SingletonPattern_V3& operator=(const SingletonPattern_V3&) = delete;
	static SingletonPattern_V3& Instance() {
		static SingletonPattern_V3 m_pInstance;
		return m_pInstance;

	}
private:
	SingletonPattern_V3() {
		std::cout << "constructor called!" << std::endl;
	}
};

int main()
{
	SingletonPattern_V3& instance_1 = SingletonPattern_V3::Instance();
	SingletonPattern_V3& instance_2 = SingletonPattern_V3::Instance();

	system("pa

执行结果是 constructor called! destructor called!

魔法静态变量是C++11的核心语言功能特性,提案:N2660 – Dynamic Initialization and Destruction with Concurrency, 最早在GCC2.3 / Clang2.9 / MSVC19.0等编译器得到支持。

这种方法又叫做 Meyers’ SingletonMeyer’s的单例, 是著名的写出《Effective C++》系列书籍的作者 Meyers 提出的。所用到的特性是在C++11标准中的Magic Static特性:

If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization.
如果当变量在初始化的时候,并发同时进入声明语句,并发线程将会阻塞等待初始化结束。

这样保证了并发线程在获取静态局部变量的时候一定是初始化过的,所以具有线程安全性。

C++静态变量的生存期 是从声明到程序结束,这也是一种懒汉式。

这是最推荐的一种单例实现方式:

  1. 通过局部静态变量的特性保证了线程安全 (C++11, GCC > 4.3, VS2015支持该特性);
  2. 不需要使用共享指针,代码简洁;不需要使用互斥锁。
  3. 注意在使用的时候需要声明单例的引用 SingletonPattern_V3& 才能获取对象。

谢谢!创作不易,大侠请留步… 动起可爱的双手,来个赞再走呗\(^o^)/~

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/234053.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(3)
blank

相关推荐

  • mysql报错日志文件在哪_windows硬件报错日志

    mysql报错日志文件在哪_windows硬件报错日志1.cmd打开服务,找到mysql服务,查看属性中my.ini配置文件所在位置:2.打开my.ini配置文件,查看datadir配置:datadir=C:/ProgramData/MySQL/MySQLServer5.7/Data此目录存储了错误日志文件3.查看错误日志文件名SHOWVARIABLESLIKE”%error%”;…

    2022年10月13日
  • Mac用QuickTime录屏+soundflower录制屏内外声音

    Mac用QuickTime录屏+soundflower录制屏内外声音mac自带的quickTime录屏没有声音,可以安装soundflower录制屏内外声音。安装好soundflower后,开始配置。启动台搜索MIDI音频设置,左下角+号聚集设备之后是+多输出设备之后是系统偏好设置选声音,输入选聚集设备之后是输出,选多输出设备之后配置quicktime:右键,新建屏幕录制,选项,聚集设备,之后OK了,开始录制吧。录完后需要声音:输入和输出改回内置麦克风和内置扬声器,不然声音大小不能修改了。…

  • Python生成器建议收藏

    1.生成器使用yield语句,每次产生一个值,函数就会被冻结2.列表推导式可以用来创建list例:生成[1*1,2*2,3*3,4*4,5*5]的列表,即[1,4,9,16,25]

    2021年12月18日
  • ipset详解[通俗易懂]

    ipset详解[通俗易懂]ipset创建:create创建一个新的ipset集合:ipsetcreateSETNAMETYPENAMESETNAME是创建的ipset的名称,TYPENAME是ipset的类型:TYPENAME:=method:datatype[,datatype[,datatype]]method指定ipset中的entry存放的方式,随后的datatype约定了每个entry…

  • python中矩阵转置4种方法「建议收藏」

    python中矩阵转置4种方法「建议收藏」文章目录1.使用双重循环2.使用列表表达式3.使用zip函数4.使用numpy模块1.使用双重循环#python双重循环arr=[[1,2,3],[4,5,6],[7,8,9],[10,11,12]]arr2=[]#数组的第二维维度foriinrange(len(arr[0])):temp=[]#数组的第一维维度forjinrange(le

  • 避免在移动端页面中使用100vh

    避免在移动端页面中使用100vh100vh带来的问题在CSS中,视口单位(Viewportunits)听起来不错。如果要设置一个元素的样式使它占据整个屏幕的高度,那么你可以设置height:100vh,这样你就拥有一个完美的全屏元素,该元素会随着视口的变化而调整大小!可惜的是,事实并非如此。100vh在移动浏览器中以一种微妙但基本的方式被破坏,使其几乎无用。最好避免使用100vh,而应该通过javascript设置高度的方…

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号