【C++】单例模式【两种实现方式】

news2024/10/6 18:22:53

 

目录

 一、了解单例模式前的基础题

1、设计一个类,不能被拷贝

2、设计一个类,只能在堆上创建对象

3、设计一个类,只能在栈上创建对象

4、设计一个类,不能被继承

二、单例模式

1、单例模式的概念

2、单例模式的两种实现方式 

2.1 懒汉模式实现单例模式

2.2 饿汉模式实现单例模式


 一、了解单例模式前的基础题

1、设计一个类,不能被拷贝

拷贝只会发生在两个场景中:拷贝构造函数以及赋值运算符重载,因此 想要让一个类禁止拷贝, 只需让该类不能调用拷贝构造函数以及赋值运算符重载即可
C++98
将拷贝构造函数与赋值运算符重载只声明不定义,并且将其访问权限设置为私有即可。
class CopyBan
{
    // ...
    
private:
    CopyBan(const CopyBan&);
    CopyBan& operator=(const CopyBan&);
    //...
};
原因:
1. 设置成私有:如果只声明没有设置成 private ,用户自己如果在类外定义了,就可以不
能禁止拷贝了
2. 只声明不定义:不定义是因为该函数根本不会调用,定义了其实也没有什么意义,不写
反而还简单,而且如果定义了就不会防止成员函数内部拷贝了。
C++11
 C++11 扩展 delete 的用法, delete 除了释放 new 申请的资源外,如果在默认成员函数后跟上
=delete ,表示让编译器删除掉该默认成员函数
class CopyBan
{
    // ...
    CopyBan(const CopyBan&)=delete;
    CopyBan& operator=(const CopyBan&)=delete;
    //...
};

2、设计一个类,只能在堆上创建对象

思路:

创建对象一定调用构造函数(或拷贝构造:两种禁用方式),故先禁掉构造函数(私有化构造函数,对于拷贝构造下面有两种方式),则放在类中的私有下,但是怎么在堆上创建对象?用一个成员函数GetObj来在堆上创建对象(因为类内能访问私有成员中的构造函数,类外不可以),那为什么要用static修饰GetObj?这样就可以用类名::GetObj来访问了,而不用创建一个对象才能访问GetObj(因为你调用GetObj之前创建的对象一定是在栈上的),此外拷贝构造要设置为私有或直接=delete,即不能使用,因为存在你拷贝构造一个栈上的对象的场景(即用拷贝构造来创建对象)

步骤:
1. 将类的构造函数私有拷贝构造声明为delete,防止别人调用拷贝在栈上生成对象。
2. 提供一个静态的成员函数,在该静态成员函数中完成堆对象的创建
class HeapOnly
{
public:
	static HeapOnly* GetObj()
	{//专门用来在堆上创建对象
		return new HeapOnly;
    //注意这里是返回HeapOnly指针,那只是指针的拷贝,
    //而不是对象的拷贝,故不会调用构造函数
	}

	//C++11:防拷贝:拷贝构造函数声明成delete
	HeapOnly(const HeapOnly&) = delete;
private:
	//构造函数私有化
	HeapOnly()
	{}

	//C++98防拷贝:拷贝构造函数声明为私有
	//HeapOnly(const HeapOnly&);
};

int main()
{
	//HeapOnly hp; //在栈上创建,失败
	HeapOnly* p = HeapOnly::GetObj();//成功【创建一个在堆上的对象】

	std::shared_ptr<HeapOnly> sp1(HeapOnly::GetObj());
	std::shared_ptr<HeapOnly> sp2(HeapOnly::GetObj());

	//HeapOnly copy(*sp1);//用栈上的对象来拷贝构造copy是不行的,故要禁掉拷贝构造

	return 0;
}

3、设计一个类,只能在栈上创建对象

和只能在堆上创建对象思路的唯一区别在于:创建的栈的对象传值返回,对象的拷贝要调用拷贝构造函数,所以不能禁掉拷贝构造函数

class StackOnly
{
public:
	static StackOnly CreateObj()
	{
	//因为返回个匿名对象,传值返回,会调用拷贝构造
	//故不能禁掉拷贝构造
		return StackOnly();
	}

private:
	StackOnly()
	{}
};

int main()
{
	 StackOnly obj = StackOnly::CreateObj();
	 //StackOnly* ptr3 = new StackOnly; //失败
}

下面是有缺陷的代码:

下面的代码只是禁掉了在堆区创建数据,但是在静态区创建的数据还是无法阻止

//该方案存在一定程度缺陷,无法阻止在数据段(静态区)创建对象
class StackOnly
{
public:
	// 禁掉operator new可以把下面用new 调用拷贝构造申请对象给禁掉
	void* operator new(size_t size) = delete;
};

int main()
{
	StackOnly so;
	//new分为operator new + 构造函数
	//StackOnly* ptr3 = new StackOnly(obj); //失败
	static StackOnly sso;//在静态区上开辟成功

	return 0;
}

4、设计一个类,不能被继承

C++98方式
// C++98中构造函数私有化,派生类中调不到基类的构造函数。则无法继承
class NonInherit
{
public:
 static NonInherit GetInstance()
 {
 return NonInherit();
 }
private:
 NonInherit()
 {}
};
C++11方式
 final 关键字, final 修饰类,表示该类不能被继承。
class A  final
{
    // ....
};

二、单例模式

1、单例模式的概念

 设计模式:
设计模式( Design Pattern )是一套 被反复使用、多数人知晓的、经过分类的、代码设计经验的 总结。为什么会产生设计模式这样的东西呢?就像人类历史发展会产生兵法。最开始部落之间打仗时都是人拼人的对砍。后来春秋战国时期,七国之间经常打仗,就发现打仗也是有 套路的,后来孙子就总结出了《孙子兵法》。孙子兵法也是类似。
使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 设计模式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。
单例模式:
一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。

单例模式有两种实现模式:懒汉模式和饿汉模式

2、单例模式的两种实现方式 

设计一个类,只能创建一个对象(单例模式) 

那么题目的意思就是保证全局只有一个实例对象

①、单例模式大体结构(有缺陷)


class Singleton
{
public:
	static Singleton* GetInstance()
	{
		if (_pinst == nullptr)
		{//因为是静态成员变量,除了第一次为nullptr
	   	//再进来不是nullptr了,直接返回_pinst即可
			_pinst = new Singleton;
		}

		return _pinst;
	}

private:
	Singleton()
	{}

	Singleton(const Singleton& s) = delete;

	static Singleton* _pinst;//静态成员的声明
};

Singleton* Singleton::_pinst = nullptr;//静态成员的定义

int main()
{
	//Singleton s1; //失败
	//保证获取的对象每次都是同一个
	cout << Singleton::GetInstance() << endl;
	cout << Singleton::GetInstance() << endl;
	cout << Singleton::GetInstance() << endl;

	//Singleton copy(*Singleton::GetInstance());//无法调用拷贝构造

    return 0;
}

运行结果:

上面代码缺陷是线程安全问题:如果两个线程同时要new一个对象(即_pinst = new Singleton),这时就发生错误了,我们要的是只有一个对象,故引出锁解决

 那我们先看看这种错误出现的场景:

为了防止线程跑太快而达不到我们想看到错误情况的效果,我们用sleep睡眠来辅助

为解决上面的问题,我们用

2.1 懒汉模式实现单例模式

①、错误代码1

//懒汉模式:第一次获取对象时,再创建对象
class Singleton
{
public:
	static Singleton* GetInstance()
	{
		_mtx.lock();

		if (_pinst == nullptr)
		{//因为是静态成员变量,除了第一次为nullptr
		 //再进来不是nullptr了,直接返回_pinst即可
		    _pinst = new Singleton;
		}

		_mtx.unlock();

		return _pinst;
	}

	Singleton(const Singleton& s) = delete;

private:
	Singleton()
	{}

	static Singleton* _pinst;//静态成员的声明
	static mutex _mtx;
};

Singleton* Singleton::_pinst = nullptr;//静态成员的定义
mutex Singleton::_mtx;

上面代码意义是创建对象保证只有一个线程在访问,解决了不会同时创建对象的问题,但是如果new失败了要抛异常怎么办?此时正在访问的线程都没有解锁,其他线程也无法访问了,故要用unique_lock:也会帮你锁,且不管你是否主动unlock解锁,都会在出了作用域后解锁

②、用unique_lock来改进

③、只需第一次加锁

只要_pinst指向已经new出来的实例对象,就无须加锁了

④、析构单例模式的对象

一般单例模式下的new出来的这个全局唯一对象是不需要释放的,因为这种单例模式下的对象,整个程序只有一个,它是一直在用的,没必要释放。
如果你就想要释放的话,两种方式:

①、静态函数

②、静态变量的生命周期


#include<vector>
#include<thread>
#include<mutex>

namespace lazy_man
{
	//懒汉模式:第一次获取对象时,再创建对象
	class Singleton
	{
	public:
		static Singleton* GetInstance()
		{
			//_mtx.lock();

			unique_lock会锁,锁完之后不管你是否解锁,出了作用域他都会自动解锁
			而你现在就这一个地方需要锁,故再加个{}作用域
			//{
			//	unique_lock<mutex> lock(_mtx);
			//	if (_pinst == nullptr)
			//	{//因为是静态成员变量,除了第一次为nullptr
			//	 //再进来不是nullptr了,直接返回_pinst即可
			//		_pinst = new Singleton;
			//	}
			//}

			//双检查:
			if (_pinst == nullptr)
			{
				//加锁只是为了保护第一次
				{
					unique_lock<mutex> lock(_mtx);
					if (_pinst == nullptr)
					{//因为是静态成员变量,除了第一次为nullptr
					//再进来不是nullptr了,直接返回_pinst即可
						_pinst = new Singleton;
					//只要_pinst指向已经new出来的实例对象,就无须加锁了
					}
				}

			}

			//_mtx.unlock();

			return _pinst;
		}

		//如果你就想释放这个对象的话,自己写个静态函数即可,手动调
		static void DelInstance()
		{
			unique_lock<mutex> lock(_mtx);
			delete _pinst;
			_pinst = nullptr;
		}

		Singleton(const Singleton& s) = delete;

	private:
		Singleton()
		{}


		static Singleton* _pinst;//静态成员的声明
		static mutex _mtx;
	};

	Singleton* Singleton::_pinst = nullptr;//静态成员的定义
	mutex Singleton::_mtx;

	//1、如果要手动释放单例对象,可以调用DelInstance
	//2、如果需要程序结束时,正常释放单例对象,可以加入下面的设计
	class GC
	{
	public:
		~GC()
		{
			Singleton::DelInstance();
		}

	};

	static GC gc;//main函数结束就会调用它的析构函数,进而释放_pinst

	void x()
	{
		Singleton s1; //失败
		保证获取的对象每次都是同一个
		//cout << Singleton::GetInstance() << endl;
		//cout << Singleton::GetInstance() << endl;
		//cout << Singleton::GetInstance() << endl;

		Singleton copy(*Singleton::GetInstance());//无法调用拷贝构造

		//代码中存在线程问题:若多个线程同时获取一个对象呢?
		vector<std::thread> vthreads;
		int n = 4;
		for (size_t i = 0; i < n; ++i)
		{
			vthreads.push_back(std::thread([]()
				{
					//cout << std::this_thread::get_id() << ":";
					cout << Singleton::GetInstance() << endl;
				}));//线程对象里面用了一个lambda表达式
		}

		for (auto& t : vthreads)
		{
			t.join();
		}
	}
}

int main()
{
	lazy_man::x();

	return 0;
}

运行结果:

2.2 饿汉模式实现单例模式

饿汉模式有个静态成员变量,静态变量在程序运行前创建,在程序的整个运行期间始终存在,他始终保持原先的值,除非给他赋予一个不同的值或程序结束。正因为程序前创建,那此时只有主线程,不存在线程安全问题。

namespace hungry_man
{
	//饿汉模式 --main函数之前就创建对象
	class Singleton
	{
	public:
		static Singleton* GetInstance()
		{
			return &_inst;
		}

		Singleton(const Singleton& s) = delete;

	private: 
		Singleton()
		{}

		static Singleton _inst;
	};

	//static对象是在main函数之前创建的,这时只有主线程,故不存在线程安全问题
	Singleton Singleton::_inst; 

	void x()
	{
		Singleton s1; //失败
		保证获取的对象每次都是同一个
		//cout << Singleton::GetInstance() << endl;
		//cout << Singleton::GetInstance() << endl;
		//cout << Singleton::GetInstance() << endl;

		Singleton copy(*Singleton::GetInstance());//无法调用拷贝构造

		//代码中存在线程问题:若多个线程同时获取一个对象呢?
		vector<std::thread> vthreads;
		int n = 4;
		for (size_t i = 0; i < n; ++i)
		{
			vthreads.push_back(std::thread([]()
				{
					//cout << std::this_thread::get_id() << ":";
					cout << Singleton::GetInstance() << endl;
				}));//线程对象里面用了一个lambda表达式
		}

		for (auto& t : vthreads)
		{
			t.join();
		}
	}
}

int main()
{
	hungry_man::x();

	return 0;
}


2.3懒汉和饿汉模式的区别


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1188994.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

低代码平台,业务开发的“银弹”

目录 一、为什么需要低代码平台 二、低代码平台的搭建能力 三、低代码其他能力 四、写在最后 随着互联网和信息技术的快速发展&#xff0c;各行各业都在积极拥抱数字化转型。在这个过程中&#xff0c;软件开发成为企业实现数字化转型的关键环节。然而&#xff0c;传统的软件开发…

交流信号继电器 DX-31BJ/AC220V JOSEF约瑟 电压启动 面板嵌入式安装

DX系列信号继电器由矩形脉冲激磁&#xff0c;磁钢保持。本继电器为双绕组。工作线圈可为电压型&#xff0c;亦可为电流型。复归线圈为电压型。继电器的工作电流或工作电压为长脉冲&#xff0c;亦可为脉冲不小于20mS的短脉冲。 系列型号 DX-31B信号继电器DX-31BJ信号继电器 D…

【VS2019 Qt5 VTK9.2】临时解决配置相关问题的简单方法

配置报错 编译报错提示&#xff08;LNK2019或LNK2001&#xff09; 严重性 代码 说明 项目 文件 行 禁止显示状态 错误 LNK2019 无法解析的外部符号 “__declspec(dllimport) public: __cdecl QVTKOpenGLNativeWidget::QVTKOpenGLNativeWidget(class QWidget *,class QFlags)(_i…

华为gre over ipsec配置案例

除了物理口加入安全域zone外&#xff0c;tunnel也得加入到安全域 一定记得tunnel也得加入zone&#xff0c;这个总爱忘记。 [fw1]firewall zone dmz [fw1-zone-dmz]add interface Tunnel 1 [fw2]firewall zone dmz [fw2-zone-dmz]add interface Tunnel 1

前端Vue 页面滑动监听 拿到滑动的坐标值

前言 前端Vue 页面滑动监听 拿到滑动的坐标值 实现 Vue2写法 mounted() {// 监听页面滚动事件window.addEventListener("scroll", this.scrolling);}, methods: { scrolling() {// 滚动条距文档顶部的距离let scrollTop window.pageYOffset ||document.documentE…

Python爬虫-获取汽车之家车家号

前言 本文是该专栏的第9篇,后面会持续分享python爬虫案例干货,记得关注。 地址:aHR0cHM6Ly9jaGVqaWFoYW8uYXV0b2hvbWUuY29tLmNuL0F1dGhvcnMjcHZhcmVhaWQ9MjgwODEwNA== 需求:获取汽车之家车家号数据 笔者将在正文中介绍详细的思路以及采集方法,废话不多说,跟着笔者直接往…

Spring Boot自动配置原理、实战、手撕自动装配源码

Spring Boot自动配置原理 相比较于传统的 Spring 应用&#xff0c;搭建一个 SpringBoot 应用&#xff0c;我们只需要引入一个注解 SpringBootApplication&#xff0c;就可以成功运行。 前面四个不用说&#xff0c;是定义一个注解所必须的&#xff0c;关键就在于后面三个注解&a…

竞赛选题 深度学习猫狗分类 - python opencv cnn

文章目录 0 前言1 课题背景2 使用CNN进行猫狗分类3 数据集处理4 神经网络的编写5 Tensorflow计算图的构建6 模型的训练和测试7 预测效果8 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; **基于深度学习猫狗分类 ** 该项目较为新颖&a…

系统的讲解 - PHP 接口签名验证

概览 工作中&#xff0c;我们时刻都会和接口打交道&#xff0c;有的是调取他人的接口&#xff0c;有的是为他人提供接口&#xff0c;在这过程中肯定都离不开签名验证。 在设计签名验证的时候&#xff0c;一定要满足以下几点&#xff1a; 可变性&#xff1a;每次的签名必须是不…

Linux环境下安装人大金仓数据库

人大金仓产品简介 金仓数据库管理系统[简称:KingbaseES]是北京人大金仓信息技术股份有限公司&#xff08;简称人大金仓&#xff09;自主研发的、具有自主知识产权的商用关系型数据库管理系统&#xff08;DBMS&#xff09;。该产品面向事务处理类应用&#xff0c;兼顾各类数据分…

JAVA反射机制中获取字节码文件对象的三种方式

1&#xff0c;&#xff08;常用&#xff09;源代码阶段&#xff0c;Class.forName("全类名") 2&#xff0c;&#xff08;传参&#xff09;加载阶段 类名.class 3&#xff0c;&#xff08;前提有对象&#xff09;运行阶段 对象.getClass()

FPGA UDP RGMII 千兆以太网(1)

1 RGMII 接口 PHY 的 MII 接口有很多种, 例如 MII、 GMII、 RGMII、 SGMII、 XGMII、 TBI、 RTBI 等。其中 RGMII的主要优势在于,它可同时适用于 1000M、 100M、 10M 三种速率,而且接口占用引脚数较少。但也存在缺点,其一, PCB 布线时需要尽可能对数据、控制和时钟线迚行…

Banana Pi BPI-M6(Raspberry Pi 5 替代品)初始设置及固件烧录

Banana Pi BPI-M6&#xff1a;初始设置和镜像烧录 Banana Pi BPI-M6 的首次测试 在上一篇文章中&#xff0c;我比较了Banana Pi BPI-M6和Raspberry Pi 5的硬件特性。两者都拥有出色的硬件技术&#xff0c;在性能方面应该不会有太大的问题。 今天我想测试一下 Banana Pi。作为…

微信小程序使用阿里巴巴矢量图标

一&#xff0c;介绍 微信小程序使用图标有两种方式&#xff0c;一种是在线获取&#xff0c;一种是下载到本地使用&#xff0c; 第一种在线获取的有个缺点就是图标是灰色的&#xff0c;不能显示彩色图标&#xff0c;而且第一种是每次请求资源的&#xff0c;虽然很快&#xff0…

Git 分支管理流程探讨

为了确保项目稳定性&#xff0c;满足项目迭代与项目开发人员的增长&#xff0c;需要尽快制定一个规范的 Git 分支管理流程。此分支管理流程是在 Git-Flow 的基础上做了一些改变。 环境区分 环境分为以下四种&#xff1a; 测试 1 服&#xff08;开发自测&#xff0c;查看效果等…

使用c++17std库varaint替代varaint开源库报错处理

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、pandas是什么&#xff1f;二、使用步骤 1.引入库2.读入数据总结 前言 提示&#xff1a;这里可以添加本文要记录的大概内容&#xff1a; 例如&#xff1a;…

GCC + Vscode 搭建 nRF52xxx 开发环境

在 Windows 下使用 GCC Vscode 搭建 nRF52xxx 开发环境 ...... by 矜辰所致前言 最近有遇到项目需求&#xff0c;需要使用到 Nordic 的 nRF52xxx 芯片&#xff0c;还记得当初刚开始写博文的时候的写的 nRF52832 学习笔记&#xff0c;现在看当时笔记毫无逻辑可言&#xff0c…

【教学类-40-03】A4骰子纸模制作3.0(6.5CM嵌套+记录表)

作品展示 背景需求 骰子2.0&#xff08;7字形&#xff09;存在幼儿不会“包边”的问题&#xff0c;求助老师帮忙示范&#xff0c;最后累的还是老师 1.0版本&#xff0c;边缘折线多&#xff0c;幼儿剪起来费力。 2.0版本&#xff0c;边缘折线多&#xff0c;幼儿剪起来费力。&a…

基于开源项目OCR做一个探究(chineseocr_lite)

背景&#xff1a;基于图片识别的技术有很多&#xff0c;应用与各行各业&#xff0c;我们公司围绕电子身份证识别自动录入需求开展&#xff0c;以下是我的研究心得 技术栈&#xff1a;python3.6&#xff0c;chineseocr_lite的onnx推理 环境部署&#xff1a;直接上截图&#xff…

什么是ADC测试,能进行自动化测试吗?

ADC测试是一种电子测试方法&#xff0c;用于评估模拟-数字转换器的性能。ADC(模数转换器)是一种将模拟信号转换为数字信号的电子器件。在ADC测试中&#xff0c;会施加一个已知的模拟信号到ADC的输入端&#xff0c;然后测量其数字输出&#xff0c;通过比较输入信号和输出信号之间…