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

 

目录

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

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懒汉和饿汉模式的区别


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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

第二章《补基础:不怕学不懂线性代数》笔记

2.1 直观理解向量 2.1.1 理解向量加法与数乘 维度相同的向量之间才可以进行加法运算&#xff0c;向 量进行加法运算时只要将相同位置上的元素相加即可&#xff0c;结果向量的维度保持不变。 向量进行数乘运算时将标量与向量的每个元素 分别相乘即可得到结果向量。 2.1.2 理…

SpringCloud 微服务全栈体系(十三)

第十一章 分布式搜索引擎 elasticsearch 二、索引库操作 索引库就类似数据库表&#xff0c;mapping 映射就类似表的结构。 我们要向 es 中存储数据&#xff0c;必须先创建“库”和“表”。 1. mapping 映射属性 mapping 是对索引库中文档的约束&#xff0c;常见的 mapping …

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

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

【笔记】结合P02项目——maven继承与聚合

maven的继承关系 P02项目大概是这个样子&#xff0c;下图展示的是其父工程 父工程配置了parent依赖springb-boot-starter-parent&#xff0c;子工程配置其parant为父工程 子工程引用common子工程 maven的版本锁定 管理子工程的版本号问题 父工程添加dependencyManageMent…

【修车案例】一波形一案例(8)

背景介绍&#xff1a;有客户问到如果气缸盖垫片失效&#xff0c;冷却液压力应该会有明显上升&#xff0c;用虹科Pico示波器怎么做这个诊断&#xff1f;我们找到一辆气缸盖垫片和冷却套坏了的丰田AD发动机进行测试分析。 示波器诊断&#xff1a; A通道 - WPS500X压力传感器测冷…

主流超融合多副本机制缺陷与 SmartX 的临时副本策略

多副本机制是超融合软件常用的数据保护方式&#xff0c;可以为存储数据提供冗余保护——即使一个或部分副本异常&#xff0c;系统仍可通过健康副本进行副本恢复。但是&#xff0c;主流实现方式下&#xff0c;这一机制依旧无法避免“副本降级”期间带来的风险&#xff1a;在副本…

视频集中存储EasyCVR平台播放一段时间后出现黑屏是什么原因?该如何解决?

安防视频监控/视频集中存储/云存储/磁盘阵列EasyCVR平台可拓展性强、视频能力灵活、部署轻快&#xff0c;可支持的主流标准协议有国标GB28181、RTSP/Onvif、RTMP等&#xff0c;以及支持厂家私有协议与SDK接入&#xff0c;包括海康Ehome、海大宇等设备的SDK等。平台既具备传统安…

Ubuntu开机无法进入系统,文件根系统目录空间不足导致?

前言&#xff1a; 自己电脑上装的是Win11和Ubuntu20双系统&#xff0c;平时就是切换着用。 偶然有次&#xff0c;Ubuntu提示文件根系统目录空间不足&#xff0c;自己没在意。 结果下次开机进入Ubuntu时候&#xff0c;芭比Q了。。进不了系统 这样的事情发生很多次了&#xff0c;…

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

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

超好用!在线即可制作电子产品图册

​电子产品图册是展示产品特点、功能和外观的重要方式之一。通过图册&#xff0c;可以让客户更好地了解产品&#xff0c;增强信任感&#xff0c;从而促进销售。同时&#xff0c;对于企业来说&#xff0c;制作精美的电子产品图册也是展示企业文化和品牌形象的重要手段之一。 一、…

自然语言处理(一):RNN

「循环神经网络」&#xff08;Recurrent Neural Network&#xff0c;RNN&#xff09;是一个非常经典的面向序列的模型&#xff0c;可以对自然语言句子或是其他时序信号进行建模。进一步讲&#xff0c;它只有一个物理RNN单元&#xff0c;但是这个RNN单元可以按照时间步骤进行展开…

大语言模型(LLM)综述(六):大型语言模型的基准和评估

A Survey of Large Language Models 前言7 CAPACITY AND EVALUATION7.1 基本能力7.1.1 语言生成7.1.2 知识利用7.1.3 复杂推理 7.2 高级能力7.2.1 人类对齐7.2.2 与外部环境的交互7.2.3 工具操作 7.3 基准和评估方法7.3.1 综合评价基准7.3.2 评估方法 7.4 实证评估7.4.1 实验设…

HarmonyOS开发:UI开展前的阶段总结

前言 关于HarmonyOS&#xff0c;陆陆续续总结了有14篇的文章&#xff0c;大家可以发现&#xff0c;没有一篇是关于UI相关的&#xff0c;不是自己没有分享的打算&#xff0c;而是对于这些UI而言&#xff0c;官方都有着一系列的文档输出&#xff0c;如果我再一一的分享&#xff0…

Redis之Java操作连接操作Redis

前言 Java是一种强大的编程语言&#xff0c;而Redis是一个快速且具有高可扩展性的开源键值数据存储系统。使用Java操作Redis可以提高效率和性能&#xff0c;并且能够满足大规模数据存储和处理的需求。在本篇文章中&#xff0c;我们将介绍如何使用Java连接Redis&#xff0c;以及…

Day24力扣打卡

打卡记录 寻找峰值&#xff08;二分法&#xff09; class Solution { public:int findPeakElement(vector<int> &nums) {int left -1, right nums.size() - 1; // 开区间 (-1, n-1)while (left 1 < right) { // 开区间不为空int mid left (right - left) / …

键盘打字盲打练习系列之认识键盘——0

一.欢迎来到我的酒馆 盲打&#xff0c;yyds&#xff01; 目录 一.欢迎来到我的酒馆二.键盘规格三.键盘分区 二.键盘规格 经常看视频&#xff0c;看到别人在键盘上一通干净利索的操作&#xff0c;就打出想要的文字。心里突然来一句&#xff1a;卧槽&#xff0c;打字贼快啊&#…

Go 接口:nil接口为什么不等于nil?

本文主要内容:深入了解接口类型的运行时表示层。 文章目录 一、Go 接口的地位二、接口的静态特性与动态特性2.1 接口的静态特性与动态特性介绍2.2 “动静皆备”的特性的好处 三、nil error 值 ! nil四、接口类型变量的内部表示第一种&#xff1a;nil 接口变量第二种&#xff1a…

刷题笔记day14-二叉树01

前序遍历 前序遍历的方式是&#xff1a;根节点、左节点、右节点。 /*** Definition for a binary tree node.* type TreeNode struct {* Val int* Left *TreeNode* Right *TreeNode* }*/ import "container/list"func preorderTraversal(root *TreeNode…

VR全景如何应用在房产行业,VR看房有哪些优势

导语&#xff1a; 在如今的数字时代&#xff0c;虚拟现实&#xff08;VR&#xff09;技术的迅猛发展为许多行业带来了福音&#xff0c;特别是在房产楼盘行业中。通过利用VR全景技术&#xff0c;开发商和销售人员可以为客户提供沉浸式的楼盘浏览体验&#xff0c;从而带来诸多优…

「Verilog学习笔记」使用子模块实现三输入数的大小比较

专栏前言 本专栏的内容主要是记录本人学习Verilog过程中的一些知识点&#xff0c;刷题网站用的是牛客网 分析 题目要求编写子模块实现两个输入数的大小比较并输出较小值&#xff0c;可以使用if-else语句实现。同时要求在主模块中实现三个输入数值的大小比较&#xff0c;假设三个…