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

2023-11-17

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. 参考文献:单例实现介绍

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

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

  • 调用 McAfee 病毒扫描引擎

    我收到客户的请求 要求使用他们服务器上的 McAfee 病毒扫描将病毒扫描集成到应用程序中 我做了一些调查 发现 McScan32 dll 是主要的扫描引擎 它导出各种看起来有用的函数 我还发现提到了 McAfee Scan Engine
  • 通过引用传递 [C++]、[Qt]

    我写了这样的东西 class Storage public Storage QString key const int value const void add item QString int private QMap
  • 机器Epsilon精度差异

    我正在尝试计算 C 中双精度数和浮点数的机器 epsilon 值 作为学校作业的一部分 我在 Windows 7 64 位中使用 Cygwin 代码如下 include
  • 从经典 ASP 调用 .Net C# DLL 方法

    我正在开发一个经典的 asp 项目 该项目需要将字符串发送到 DLL DLL 会将其序列化并发送到 Zebra 热敏打印机 我已经构建了我的 DLL 并使用它注册了regasm其次是 代码库这使得 IIS 能够识别它 虽然我可以设置我的对象
  • 如何连接重叠的圆圈?

    我想在视觉上连接两个重叠的圆圈 以便 becomes 我已经有部分圆的方法 但现在我需要知道每个圆的重叠角度有多大 但我不知道该怎么做 有人有主意吗 Phi ArcTan Sqrt 4 R 2 d 2 d HTH Edit 对于两个不同的半
  • C++ 多行字符串原始文字[重复]

    这个问题在这里已经有答案了 我们可以像这样定义一个多行字符串 const char text1 part 1 part 2 part 3 part 4 const char text2 part 1 part 2 part 3 part 4
  • 重载 (c)begin/(c)end

    我试图超载 c begin c end类的函数 以便能够调用 C 11 基于范围的 for 循环 它在大多数情况下都有效 但我无法理解和解决其中一个问题 for auto const point fProjectData gt getPoi
  • 方程“a + bx = c + dy”的积分解

    在等式中a bx c dy 所有变量都是整数 a b c and d是已知的 我如何找到整体解决方案x and y 如果我的想法是正确的 将会有无限多个解 由最小公倍数分隔b and d 但我只需要一个解决方案 我可以计算其余的 这是一个例
  • 人脸 API DetectAsync 错误

    我想创建一个简单的程序来使用 Microsoft Azure Face API 和 Visual Studio 2015 检测人脸 遵循 https social technet microsoft com wiki contents ar
  • 使用 C# 中的 CsvHelper 将不同文化的 csv 解析为十进制

    C 中 CsvHelper 解析小数的问题 我创建了一个从 byte 而不是文件获取 csv 文件的类 并且它工作正常 public static List
  • 如何定义一个可结构化绑定的对象的概念?

    我想定义一个concept可以检测类型是否T can be 结构化绑定 or not template
  • C# xml序列化必填字段

    我需要将一些字段标记为需要写入 XML 文件 但没有成功 我有一个包含约 30 个属性的配置类 这就是为什么我不能像这样封装所有属性 public string SomeProp get return someProp set if som
  • C 编程:带有数组的函数

    我正在尝试编写一个函数 该函数查找行为 4 列为 4 的二维数组中的最大值 其中二维数组填充有用户输入 我知道我的主要错误是函数中的数组 但我不确定它是什么 如果有人能够找到我出错的地方而不是编写新代码 我将不胜感激 除非我刚去南方 我的尝
  • 如何在当前 Visual Studio 主机内的 Visual Studio 扩展中调试使用 Roslyn 编译的代码?

    我有一个 Visual Studio 扩展 它使用 Roslyn 获取当前打开的解决方案中的项目 编译它并从中运行方法 程序员可以修改该项目 我已从当前 VisualStudioWorkspace 成功编译了 Visual Studio 扩
  • 如何实例化 ODataQueryOptions

    我有一个工作 简化 ODataController用下面的方法 public class MyTypeController ODataController HttpGet EnableQuery ODataRoute myTypes pub
  • 在 WPF 中使用 ReactiveUI 提供长时间运行命令反馈的正确方法

    我有一个 C WPF NET 4 5 应用程序 用户将用它来打开某些文件 然后 应用程序将经历很多动作 读取文件 通过许多插件和解析器传递它 这些文件可能相当大 gt 100MB 因此这可能需要一段时间 我想让用户了解 UI 中发生的情况
  • 相当于Linux中的导入库

    在 Windows C 中 当您想要链接 DLL 时 您必须提供导入库 但是在 GNU 构建系统中 当您想要链接 so 文件 相当于 dll 时 您就不需要链接 为什么是这样 是否有等效的 Windows 导入库 注意 我不会谈论在 Win
  • C++ 中的 include 和 using 命名空间

    用于使用cout 我需要指定两者 include
  • 为什么 std::uint32_t 与 uint32_t 不同?

    我对 C 有点陌生 我有一个编码作业 很多文件已经完成 但我注意到 VS2012 似乎有以下语句的问题 typedef std uint32 t identifier 不过 似乎将其更改为 typedef uint32 t identifi
  • 使用 WGL 创建现代 OpenGL 上下文?

    我正在尝试使用 Windows 函数创建 OpenGL 上下文 现代版本 基本上代码就是 创建窗口类 注册班级 创建一个窗口 choose PIXELFORMATDESCRIPTOR并设置它 创建旧版 OpenGL 上下文 使上下文成为当前

随机推荐