C++57个入门知识点_44:单例的实现与理解(单例:提供唯一类的实例;静态对象的存储位置及生命周期;静态对象指针实现单例(懒汉式,较多用);静态对象引用实现单例(更安全),禁用拷贝构造)

C++57个入门知识点_42:静态成员变量理解(static int m_nStatic;实现单独写在类外;本质是带类域的全局变量;可以不用产生对象即可访问CInteger::m_nStatic=1)C++57个入门知识点_43:静态成员函数(static静态成员函数声明,实现和调用;静态函数内部只能访问静态成员变量;本质是静态成员函数没this指针;带类域的全局函数可直接利用类调用静态成员函数)中我们学习了静态成员变量和静态成员函数,好像没有看到它们俩有大的作用,只是起到了类域限制的作用,其实不然,其在特定的场合有特定的作用。本篇介绍使用静态成员实现的很常见的一种设计模式-单例模式

设计模式其实就是简单的代码复杂化,这是因为结构简单的代码在需求发生改变之后大概率就需要伤筋动骨的去修改的,使用一定的模式,就可以提高代码的复用率。大项目一般都是喜欢使用各种模式。本篇在学习完静态成员变量及函数后介绍用其实现单例模式。

总结放于前:

(1)什么是单例: 单例 Singleton 是设计模式的一种,其特点是只提供唯一一个类的实例,具有全局变量的特点,在任何位置都可以通过接口获取到那个唯一实例;

(2)单例模式典型应用:软件中生成log文件的操作,各种类调用生成log的类对象;

(3)单例实现方法:

  • 懒汉式单例(静态对象指针实现):类的构造函数放于private中,通过static创建带类域的静态成员函数作为创建对象的接口,需要使用静态对象指针在没有创建对象的情况下赋给对象指针,有的话就直接返回静态指针,这样就可以实现单例的目的。
  • 利用静态对象引用的方法实现单例模式(更安全),利用静态对象引用的方式实现单例需要禁用拷贝构造及等号运算符重载

1. static内存相关知识

要理解static关键字在C++中的所有作用,首先要明白程序所使用的不同内存区域的作用。

C++程序运行时使用三种内存,一种是static内存,还有一种是stack(栈)内存,以及heap(堆)内存(动态内存池)
(1)static内存用于local static object,class static members,以及在函数外定义的全局对象。static对象在生成后一直存在,直到程序结束。 static内存由编译器直接管理,程序无法控制。

(2)stack内存用于函数内定义的变量。stack内存中的对象只是在函数块执行期间存在。stack内存也是由编译器所管理。

(3)每个程序都会拥有一个内存池,被称为free store或者heap,用于程序在运行时动态寻址的对象,这些对象的生存周期由程序负责管理,heap内存由程序所控制。
static关键字其实主要限定了C++中的对象所使用的内存区域。下面根据static所应用对象的不同,分别从全局对象,本地静态对象,和类静态成员角度来解释static在C++中的作用。

详细请参考:C++中static用法总结(静态对象,内存)

2. 什么是单例模式

什么是单例: 单例 Singleton 是设计模式的一种,其特点是只提供唯一一个类的实例,具有全局变量的特点,在任何位置都可以通过接口获取到那个唯一实例;

3. 单例模式典型应用

软件中生成log文件的操作,各种类调用生成log的类对象

4. 单例模式的实现

4.1 普通模式下创建对象的特点(多次创建,产生多个对象)

首先我们创建一个Singleton的类。

#include <iostream>
class Singleton {
public:
	Singleton()
	{
		printf("Singleton()construct");
	}
	~Singleton()
	{
		printf("Singleton()destruct");
	}
};

int main()
{
    Singleton s1;
    Singleton s2;
	return 0;
}

用户(main函数)在创建对象时,通过Singleton s1;Singleton s2;我们是创建了s1,s2 两个对象,显然这不是我们想要实现的唯一实例的情况。

4.2 利用静态对象指针实现单例模式(懒汉式,目前项目中常用的框架)

1)思考1:
如果想要实现唯一的实例要求,我们需要 对构造函数加以限制,那如何去做呢?将构造函数从公有的变为私有的。

#include <iostream>

class Singleton {
public:

	~Singleton()
	{
		printf("Singleton()destruct");
	}
private:
	Singleton()
	{
		printf("Singleton()construct");
	}
};
int main()
{
	Singleton s1;
	Singleton s2;
	return 0;
}

运行之后:
在这里插入图片描述
用户此时已经无法利用Singleton s1;Singleton s2;通过构造函数创建对象了,但是得留个接口给用户去创建对象,该怎么去做呢?

2)思考2:
通过一个Public的成员函数创建一个对象可以吗?(这里是参考私有成员变量通过公有成员函数进行访问)

这样显然是不行的!这是因为调用一个函数的前提是先有一个对象,但是它又要作为创建对象的接口,这两个本来就是相悖的,这就像先有鸡还是先有蛋的的悖论

class Singleton {
public:

	~Singleton()
	{
		printf("Singleton()destruct");
	}
	//用户无法通过构造直接产生对象,需要为用户提供接口创建对象
	//创建对象的唯一用户接口
	Singleton* CreateObject()
	{
		return new Singleton();
	}

private:
	Singleton()
	{
		printf("Singleton()construct");
	}

};

3)思考3:
前面我们学习静态成员函数,在Singleton* CreateObject()前加static就与对象无关,变为了类域的函数,这样就可以不需要对象就可以调用。

#include <iostream>
//单例--只有一个实例
class Singleton {
public:

	~Singleton()
	{
		printf("Singleton()destruct");
	}
	//用户无法通过构造直接产生对象,需要为用户提供接口创建对象
	//创建对象的唯一用户接口
	static Singleton* CreateObject()
	{
		return new Singleton();
	}

private:
	Singleton()
	{
		printf("Singleton()construct");
	}

};

int main()
{
	Singleton* pObj1=Singleton::CreateObject();
	Singleton* pObj2 = Singleton::CreateObject();
	return 0;
}

运行之后,看看结果:
在这里插入图片描述
可以看到,创建了两个对象出来,不是我们想要的只实例化一个对象的目的,这是因为每调用一次函数就会new一个对象出来。

4)思考4(最终懒汉式代码):
进一步优化,增加一个对象指针出来Singleton* m_p0bject;,但是这样创建出来的还是与类绑定的,因此前面加static变为static Singleton* m_p0bject;m_p0bject对象指针并不与某个对象绑定),并在创建对象时判断是否已经创建对象,没有的话就创建一个对象出来赋给对象指针 m_p0bject,有的话就直接返回 m_p0bject

#include <iostream>

//单例--只有一个实例
class Singleton {
public:

	~Singleton()
	{
		printf("Singleton()destruct");
	}
	//用户无法通过构造直接产生对象,需要为用户提供接口创建对象
	//创建对象的唯一用户接口
	static Singleton* CreateObject()
	{
		//对象指针为空时创建对象
		if (m_p0bject == nullptr) {
			m_p0bject = new Singleton();
		}
		//对象指针不为空的话直接返回
		return m_p0bject;
	}

private:
	Singleton()
	{
		printf("Singleton()construct");
	}

	//创建静态静态对象指针
	static Singleton* m_p0bject;
};

//在类外进行静态对象指针的初始化
Singleton* Singleton::m_p0bject=nullptr;

int main()
{
	Singleton* pObj1=Singleton::CreateObject();
	Singleton* pObj2 = Singleton::CreateObject();
	return 0;
}

运行之后:
在这里插入图片描述
上面的代码运行出来,创建的对象都是一个地址下的,也就是一个实例化。

上面代码虽然实现了单例,但是有一个很重要的问题,那就是运行后可以看到只有构造部分,没有释放内存的部分,会造成内存泄漏的问题。只能通过用户增加delete pObj1的方式释放,而且还会有线程安全的问题(多线程中)。
在这里插入图片描述

4.3 利用静态对象引用的方式实现单例模式

1)思考1:
通过静态对象的方式就可以实现,既有构造又有析构,这是因为静态对象的生命周期是整个程序,程序关闭的时候才会释放。

#include <iostream>
//单例--只有一个实例
class Singleton {
public:
	~Singleton()
	{
		printf("Singleton()destruct");
	}
	//用户无法通过构造直接产生对象,需要为用户提供接口创建对象
	//创建对象的唯一用户接口
	static Singleton* CreateObject()
	{
		static Singleton obj;
		return &obj;
	}
private:
	Singleton()
	{
		printf("Singleton()construct");
	}
};
int main()
{
	Singleton* pObj1=Singleton::CreateObject();
	Singleton* pObj2 = Singleton::CreateObject();
	return 0;
}

在整个程序运行完后得到的结果如下:
在这里插入图片描述
2)思考2:
利用static Singleton obj; return &obj;方式创建的对象不是在堆上的,用户看到指针会自然而然的使用delete pObj1进行释放,这样就会导致程序运行出错。

代码改为如下返回一个引用,用户就无法使用delete了:

	static Singleton& CreateObject()
	{
		static Singleton obj;
		return obj;
	}
	
int main()
{
    //用引用创建对象
	Singleton& pObj1=Singleton::CreateObject();
	Singleton& pObj2 = Singleton::CreateObject();
	return 0;
}

3)思考3:
还有一种可能,用户使用Singleton pObj2 = Singleton::CreateObject();,实现了拷贝构造,这样就会突破单例模式。参考C++57个入门知识点_23_ 拷贝构造函数(利用一个对象创建另一个对象,调用的构造函数即拷贝构造函数;缺省下为完全拷贝;手写即CStudent(CStudent& obj) {…}-对象做参数)

int main()
{
    //用引用创建对象
	Singleton& pObj1=Singleton::CreateObject();
	//将Singleton::CreateObject()对象的引用强行赋给产生的pObj2对象
	//= 本质也是构造,把一个对象赋给另一个对象 也就是拷贝构造
	Singleton pObj2 = Singleton::CreateObject();
	return 0;
}

在这里插入图片描述
因此除了限制普通的构造,还需要限制拷贝构造

方法1: 拷贝构造私有

private:
//一种方法:拷贝构造私有
	Singleton(Singleton& obj)
	{
		printf("Singleton(Singleton& obj)construct");
	}

方法2: 拷贝构造本身就不该存在,因此更推荐另一种方法:Singleton(Singleton& obj) = delete;

public:
	//方法2禁用拷贝构造
	Singleton(Singleton& obj) = delete;
	//等号运算符重载禁用,防止形成拷贝构造,此处是另一种解决方案
	Singleton* operator=(Singleton& obj) = delete;

运行结果:
在这里插入图片描述

4)最终代码:

#include <iostream>

//单例--只有一个实例
class Singleton {
public:
	~Singleton()
	{
		printf("Singleton()destruct");
	}
	//用户无法通过构造直接产生对象,需要为用户提供接口创建对象
	//创建对象的唯一用户接口
	static Singleton& CreateObject()
	{
		static Singleton obj;
		return obj;
	}
	//方法2禁用拷贝构造
	Singleton(Singleton& obj) = delete;
	//等号运算符重载禁用,防止形成拷贝构造,此处应该是可以省略不写
	Singleton* operator=(Singleton& obj) = delete;
private:
	Singleton()
	{
		printf("Singleton()construct");
	}

};
int main()
{
	Singleton& pObj1=Singleton::CreateObject();
	return 0;
}

这种方法运行后还会报错,后边有时间了再研究:
在这里插入图片描述
5. 学习视频地址:C++57个入门知识点_44:单例的实现与理解

6. 参考文献:单例实现介绍