【C++】单例模式

今天我们来介绍一种十分使用的设计模式:单例模式。

1.问题引入

在介绍我们的单例模式之前,我们先来解决有些小问题:

1.1只能在堆上创建对象的类

假设现在我们想要实现一个类,该类只能在堆上实例化出对象,我们应该如何设计类?

1.1 私有化析构函数

由于在栈上创建对象会默认调用析构函数,所以如果我把析构函数私有化,那么显然在栈上无法创建对象。但是,为了在堆上创建的对象也能够被析构,所以我们还得封装一个析构的接口DestroyObj()
在这里插入图片描述

1.2 私有化构造函数

由于栈上调用对象会调用构造函数,所以当我们把构造函数私有化之后,显然无法再在栈上创建类。

但是我们在堆上创建对象也是要有构造函数的,所以我们要专门封装一个接口CreateObj来供我们构建对象。

那么为什么我们要将其定义为静态成员函数呢?因为如果调用正常的成员函数,需要先将类实例化,但是我们这个函数就是用来实例化类的,这就陷入“死循环”了,所以只有静态成员才能跳脱对象的限制,它属于类,这样我们就可以在没有对象的情况下调用它。
在这里插入图片描述
但是此时该类还存在一个 bug,我们可以通过(系统自动生成的)默认拷贝构造构造一个类
在这里插入图片描述
所以我们做一个 防拷贝(只声明不实现)的操作。
在这里插入图片描述

1.2 只能在栈上创建对象的类

1.2.1 将构造函数私有化

在这里插入图片描述
这里我们不能把拷贝构造给封了,因为CreateObj在返回时会用到。这也导致一个漏洞:我们可以用栈上的对象拷贝构造堆上的对象。

1.2.2 屏蔽new

因为new在底层调用void* operator new(size_t size)函数,只需将该函数屏蔽掉即可.
关于这部分可以看:【C++】10分钟掌握 C++内存管理

在这里插入图片描述
不过这个方法也不是十全十美,我们可以在静态区创建对象,与题意有出入。
在这里插入图片描述


1.3不能拷贝构造的类

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

class CopyBan
{
public:
 	CopyBan(const CopyBan&)=delete;
 	CopyBan& operator=(const CopyBan&)=delete;
private:
    int _a
};

1.4 不能继承的类

1.4.1 构造函数私有化

显然,把父类构造函数私有化,派生类中调不到基类的构造函数。则无法继承。

但是这是一种不够直接的方式:这里类可以继承的(没有报错),但是子类是不能够创建对象的。
在这里插入图片描述

1.4.2 使用final关键字

相较于方法一,我更加推荐这种写法。
我们可以使用C++11中提供的final关键字,表示该类不能够继承

在这里插入图片描述


2.单例模式

2.1设计模式简介

设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。为什么会产生设计模式这样的东西呢?就像人类历史发展会产生兵法。最开始部落之间打仗时都是人拼人的对砍。后来春秋战国时期,七国之间经常打仗,就发现打仗也是有套路的,后来孙子就总结出了《孙子兵法》。孙子兵法也是类似。

使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 设计模式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。


2.2 静态与全局

在介绍单例模式之前,我们先来回顾一些要用到的知识。

2.2.1 全局变量和全局静态变量

全局变量存在于静态存储区,在编译时分配存储空间,全局变量默认具有外部链接性,作用域整个工程。

静态全局变量就是在全局变量的定义时加了static关键字,静态全局变量依旧是存储于静态存储区,在编译时分配存储空间,与全局变量不同的是:静态全局变量隐藏了其外部链接性,作用域缩小,仅限于本文件使用。

所以全局变量与全局静态全局变量的唯一差别在于 作用域不同


2.2.2 全局变量作用域的拓展与限制

1. 使用extern关键字可以对全局变量的作用域进行拓展

  1. 已知,全局变量的作用域是从变量的定义到程序文件的末尾。如果想在该全局变量之前就引用该全局变量,我们可以在引用之前使用 extern 关键字对该变量进行说明,有了此说明,就可以从说明之处起,合法地引用该变量。

  2. 若想在一个文件中(a.cpp)中引用另一个文件 (b.cpp)中已经定义的全局变量,我们可以在 a.cpp中使用extern 关键字对全局变量进行说明,在之后的编译和链接时,系统就知道该全局变量已经在其他文件中(b.cpp)中定义过了。

补充一下:在编译的时候遇到extern。系统会在本文件中(a.cpp)中查找全局变量的定义,如果找到,就在本文件中拓展作用域如果找不到,就在链接的时候在其他文件(b.cpp)中查找全局变量的定义,如果找到,就将作用域拓展到本文件(a.cpp);如果依旧找不到,则报错。

2. 使用static关键字可以对全局变量的作用域进行限制

全局变量默认是有外部链接性的,作用域是整个工程,在一个文件内定义的全局变量,在另一个文件中,通过extern对全局变量进行声明,就可以使用全局变量。

如果希望全局变量仅限本文件引用,而不能被其他文件引用,可以在定义全局变量时在前面加一个static关键字。


2.2单例模式

2.2.1 关于创建单例的一些尝试

现在想一想,有什么方法,可以使我们在一个工程中的某一个变量只有一个吗?

2.2.1.1 尝试定义成全局变量

假设我们现在拥有三个文件:

  1. 头文件 Singleton.h
  2. 源文件1 Singleton.cpp
  3. 源文件2 Test.cpp
// Singleton.h
#pragma once
#include<vector>
#include<iostream>
using namespace std;

//定义一个全局变量
vector<int>v;

void f1();
//Singleton.cpp

#include"Singleton.h"
void f1()
{
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;
}

//Test.cpp

#include"Singleton.h"
void f2()
{
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;
}

int main()
{
   f1();
   f2();
}

请问这样写可以保证只存在一个 v吗?显然不行,在头文件展开之后,造成了v的重定义。

在这里插入图片描述

所以我们要尝试一下其他的方法。

2.2.1.2 尝试使用全局静态变量

我们将全局变量定义为静态全局变量,再次实验一下:

// Singleton.h
#pragma once
#include<vector>
#include<iostream>
using namespace std;

//定义一个全局变量
static vector<int>v;

void f1();

我们发现,还是不对,这次虽然不报重定义的错误,但是由于static 隐藏了v 的外链属性,所以在每一文件中都会存在一个单独的v,这也就是为什么会有下面的运行结果。
在这里插入图片描述


2.2.1.3 尝试对v先声明不定义
// Singleton.h
#pragma once
#include<vector>
#include<iostream>
using namespace std;

//声明一个全局变量
extern vector<int>v;

void f1();
//Singleton.cpp
vector<int>v; //将v定义出来
#include"Singleton.h"
void f1()
{
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;
}

//Test.cpp

#include"Singleton.h"
void f2()
{
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;
}

int main()
{
   f1();
   f2();
}

这一次,我们终于成功了,具体的理由可以看之前 关于extern的讲解。
在这里插入图片描述
但是虽然我们达到了目的,但是其约束力不够强,这样的设计要求我们所有文件中只需要定义一次v,但是很可能就有人不小心就定义了多个。这也是其不够完善的地方。

所有的法子都穷尽了,我们现在可以引入单例模式,所谓模式,不如说是 规则,我们在该规则之下只能拥有一份对象,无论你想或不想。

单例模式 有好几种写法,这里只介绍两种常用的,分别叫做 饿汉模式和懒汉模式,我们会一一介绍。


2.2.2饿汉模式

简单来说,所谓“饿汉”,就是不管你将来使不使用,程序启动时就创建一个唯一的实例对象

  1. Singleton.h

class Singleton
{
public:
	static Singleton& GetInstance();
	vector<int>_v;
private:
	Singleton() {}
	Singleton(const Singleton&) = delete;
	Singleton& operator = (const Singleton&) = delete;
    //声明
	static Singleton _sinst;
};
  1. Singleton.cpp
#include"Singleton.h"

//定义
Singleton Singleton::_sinst;
Singleton& Singleton::GetInstance() {
	return _sinst;
}
  1. 我们先将构造函数私有化,拷贝构造,不可以随意地创建对象。
  2. 在类中声明一个static Singleton 对象 (不能定义),在 cpp定义这个对象
  3. 提供一个获取单例对象的静态成员函数。

由于我们把所有的构造都私有化了,所以只有只有单例类自己才可以实例化处对象,同时,我们只在头文件中声明,在cpp中定义,这样就可以防止重定义。


  • 饿汉模式的优缺点
  1. 优点:设计简单方便
  2. 缺点:如果单例对象的构造函数中要做很多工作,可能会导致程序启动很慢。 再者,如果多个单例类,并且它们之间有依赖关系,那么饿汉模式无法保证(顺序)。

2.2.3懒汉模式

简单来说,懒汉就是第一次调用GetInstance 的时候,才会创建初始化单例对象。相比于饿汉模式,不会存在启动慢的问题,或者是 启动时初始化的依赖关系。

  1. Singleton.h
class Singleton
{
public:
	static Singleton& GetInstance();
	vector<int>_v;
private:
	
	Singleton() {}
	Singleton(const Singleton&) = delete;
	Singleton& operator = (const Singleton&) = delete;
	
	static Singleton *_spinst;
	
};
  1. Singleton.cpp
Singleton* Singleton::_spinst = nullptr;

Singleton& Singleton::GetInstance() {
	if (_spinst == nullptr) {
		_spinst = new Singleton;
	}
	return *_spinst;
}

其实基本思路与懒汉模式相同,但是在类中我们声明的是一个单例类的指针,在cpp中定义指针为nullptr。但是只有真正调用 GetInstance 之后,才会在堆上实例化出单例对象。


但是,由于懒汉模式的设计,还会存在一些问题

当存在多个线程的时候,可能会导致有多个单例的产生:

在这里插入图片描述
比如说,现在我们有两个线程。线程先进来,它完成了1的右半边,即new 出了一个对象,但是还没有赋值给_spainst 的时候,时间片就到了。之后线程2进入,由于_spinst依旧为空,线程2也new 出一个对象并且完成了1,2 的所有步骤,并且在外部可能做了一些数据操作,此后,线程1再次进入,将之前的对象赋值给_spinst … 此时就出现了问题,线程2下的 _spinst被覆盖了,数据也丢失泄漏。

改进之后,代码如下:

  1. Singleton.h
class Singleton
{
public:
	static Singleton& GetInstance(); //获取单例对象
	static void  DelInstance();      //释放单例对象
	vector<int>_v;
private:
	
	Singleton() {}
	Singleton(const Singleton&) = delete;
	Singleton& operator = (const Singleton&) = delete;
	
	static Singleton *_spinst;
	static mutex _mtx;
};
  1. Singleton.cpp
#include"Singleton.h"

Singleton* Singleton::_spinst = nullptr;
Singleton& Singleton::GetInstance()
{
    //双重判空是为了避免无意义的加锁解锁
	if (_spinst == nullptr)
	{
		_mtx.lock();
		if(_spinst == nullptr){
			_spinst = new Singleton;
		}
		_mtx.unlock();
	}
	return *_spinst;
}

static void  Singleton::DelInstance()
{
	  if (_spinst != nullptr)
		{
			_mtx.lock();
			if(_spinst != nullptr){
				delete _spinst;
				_spinst==nullptr;
			}
			_mtx.unlock();
		}
}

由于饿汉模式是在mai函数之前创建对象,所以并不存在线程安全的问题。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ornamrr

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值