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

目录

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

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

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

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

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

二、单例模式

1、单例模式的概念

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

[2.1 懒汉模式实现单例模式](#2.1 懒汉模式实现单例模式)

[2.2 饿汉模式实现单例模式](#2.2 饿汉模式实现单例模式)


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

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

拷贝只会发生在两个场景中:拷贝构造函数以及赋值运算符重载,因此 想要让一个类禁止拷贝, 只需让该类不能调用拷贝构造函数以及赋值运算符重载即可
C++98

将拷贝构造函数与赋值运算符重载只声明不定义 ,并且将其访问权限设置为私有即可。

cpp 复制代码
class CopyBan
{
    // ...
    
private:
    CopyBan(const CopyBan&);
    CopyBan& operator=(const CopyBan&);
    //...
};

原因:

  1. 设置成私有:如果只声明没有设置成 private ,用户自己如果在类外定义了,就可以不
    能禁止拷贝了
  2. 只声明不定义:不定义是因为该函数根本不会调用,定义了其实也没有什么意义,不写
    反而还简单,而且如果定义了就不会防止成员函数内部拷贝了。
    C++11
    C++11 扩展 delete 的用法, delete 除了释放 new 申请的资源外,如果在默认成员函数后跟上
    =delete ,表示让编译器删除掉该默认成员函数
cpp 复制代码
class CopyBan
{
    // ...
    CopyBan(const CopyBan&)=delete;
    CopyBan& operator=(const CopyBan&)=delete;
    //...
};

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

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

  1. 将类的构造函数私有拷贝构造声明为delete,防止别人调用拷贝在栈上生成对象。
  1. 提供一个静态的成员函数,在该静态成员函数中完成堆对象的创建
cpp 复制代码
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、设计一个类,只能在栈上创建对象

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

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

private:
	StackOnly()
	{}
};

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

下面是有缺陷的代码:

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

cpp 复制代码
//该方案存在一定程度缺陷,无法阻止在数据段(静态区)创建对象
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方式

cpp 复制代码
// C++98中构造函数私有化,派生类中调不到基类的构造函数。则无法继承
class NonInherit
{
public:
 static NonInherit GetInstance()
 {
 return NonInherit();
 }
private:
 NonInherit()
 {}
};

C++11方式

final 关键字, final 修饰类,表示该类不能被继承。

cpp 复制代码
class A  final
{
    // ....
};

二、单例模式

1、单例模式的概念

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

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

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

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

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

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

cpp 复制代码
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

cpp 复制代码
//懒汉模式:第一次获取对象时,再创建对象
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出来的这个全局唯一对象是不需要释放的,因为这种单例模式下的对象,整个程序只有一个,它是一直在用的,没必要释放。

如果你就想要释放的话,两种方式:

①、静态函数

②、静态变量的生命周期

cpp 复制代码
#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 饿汉模式实现单例模式

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

cpp 复制代码
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懒汉和饿汉模式的区别


相关推荐
van叶~6 分钟前
算法妙妙屋-------1.递归的深邃回响:二叉树的奇妙剪枝
c++·算法
knighthood200117 分钟前
解决:ros进行gazebo仿真,rviz没有显示传感器数据
c++·ubuntu·ros
半盏茶香1 小时前
【C语言】分支和循环详解(下)猜数字游戏
c语言·开发语言·c++·算法·游戏
小堇不是码农1 小时前
在VScode中配置C_C++环境
c语言·c++·vscode
Jack黄从零学c++1 小时前
C++ 的异常处理详解
c++·经验分享
捕鲸叉6 小时前
创建线程时传递参数给线程
开发语言·c++·算法
A charmer7 小时前
【C++】vector 类深度解析:探索动态数组的奥秘
开发语言·c++·算法
Peter_chq7 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
青花瓷8 小时前
C++__XCode工程中Debug版本库向Release版本库的切换
c++·xcode
幺零九零零9 小时前
【C++】socket套接字编程
linux·服务器·网络·c++