C++单例模式详解

目录

0. 前言

1. 懒汉式单例模式

1.1 最简单的单例模式

1.2 防止内存泄漏

1.2.1 智能指针的方法

1.2.2 静态嵌套的方法

1.3 保证线程安全

1.4 C++11版本的优雅解决方案

2. 饿汉式单例模式


0. 前言

起因是在程序中重复声明了一个单例模式的变量,后来程序怎么调都不对,最后发现变量是用单例模式,修改是全局的,所以决定好好梳理一下单例模式。

首先,为什么要用单例模式,就是因为我们希望一个类只有唯一一个实例,并且提供一个全局的访问点。从这个描述不难看出,这个实例应该是要static来修饰的。实际情况中,比如我们想申请一个内存池,程序都用这一块内存池,那么就可以单例模式来实现

1. 懒汉式单例模式

1.1 最简单的单例模式

先来看看最简单的单例模式怎么写,然后分析一下有什么问题。

#include <iostream>
using namespace std;// 最简单的单例模式
class Singleton{
public:// 获取实例的接口static Singleton* getInstance() {if (instance == nullptr) {instance = new Singleton(); // 使用new来创建一个新的实例对象} else {cout << "重复创建,返回已创建的实例。" << endl;}return instance;}
private:// 静态私有对象static Singleton* instance;// 构造函数一定要私有,外部无法直接访问构造函数Singleton() {cout << "运行构造函数" << endl;};~Singleton() {cout << "运行析构函数" << endl;};
};
// 要在类外进行初始化!!!
Singleton* Singleton::instance = nullptr;int main(){Singleton* s1 = Singleton::getInstance();Singleton* s2 = Singleton::getInstance();return 0;
}

这里有两个点需要特别的注意:

  • 单例模式的类需要有一个静态私有的对象,是这个类的实例,且必须在类外进行初始化。
  • 获取实例的接口getInstance()函数式可以被访问和调用的,但是必须返回static类型的变量,实际上就是返回这个类的唯一实例
  • 补充下析构函数私有化的原因:保证只能在堆上new一个新的类对象。因为C++是一个静态绑定的语言。在编译过程中,所有的非虚函数调用都必须分析完成,即使是虚函数也需检查可访问性。当在栈上生成对象时,对象会自动析构,也就说析构函数必须可以访问。而堆上生成对象,由于析构时机由程序员控制,所以不一定需要析构函数。

按照上面的写法,基本上满足了单例模式的初衷,要一个只有一个实例的类。但是存在两个问题,一个是因为使用到了new进行创建,就需要人为进行delete的释放操作,否则就会造成内存泄漏。第二个是程序乍一看只会创建一块内存空间,但是如果考虑多线程,那么就有可能多个线程分别创建了多块内存空间的实例,与我们设计单例模式的初衷相违背。

1.2 防止内存泄漏

1.2.1 智能指针的方法

运行1.1的程序,结果为:

运行构造函数
重复创建,返回已创建的实例。

可以发现,并没有调用析构函数。这里,补充一下析构函数的作用:释放对象的使用资源,并销毁对象的非static数据成员。而我们定义的instance成员变量static的,所以无法直接使用析构函数进行释放。虽然事例的简单程序在运行完之后static变量会自动释放,但是在很多复杂的程序中,使用完instance却不释放是非常致命的会导致内存泄漏的问题。这里采用智能指针的方法,并借用智能指针的reset函数,定义一个销毁的成员函数,通过这个成员函数调用delete来释放我们创建的new内存,达到析构的目的。看看下面的实现。

#include <iostream>
#include <memory>
using namespace std;class Singleton{
public:// 公有接口获取唯一实例static shared_ptr<Singleton> getInstance() {if (instance == nullptr) {instance.reset(new Singleton(), destoryInstance);}else {cout << "重复创建,返回异创建的实例。" << endl;}return instance;}// 定义销毁的实例static void destoryInstance(Singleton* x) {cout << "自定义释放实例" << endl;delete x;}
private:Singleton() {cout << "运行构造函数。" << endl;};~Singleton() {cout << "运行析构函数。" << endl;};
private:// 静态私有对象static shared_ptr<Singleton> instance;
};// 初始化
shared_ptr<Singleton> Singleton::instance;int main(){shared_ptr<Singleton> s1 = Singleton::getInstance();shared_ptr<Singleton> s2 = Singleton::getInstance();return 0;
}

运行结果为:

运行构造函数。
重复创建,返回异创建的实例。
自定义释放实例
运行析构函数。

可以看到,我们通过智能指针,在使用完instance资源后调用了自定义的释放函数,即delete了new出来的空间,达到了运行析构函数的目的,防止了内存泄漏。

1.2.2 静态嵌套的方法

解决内存的泄漏的方法,总之是要把释放的过程先写好,不能靠用户每次自己释放。对于本次分享的例子,就是要把delete放进代码里。除了利用智能指针的释放函数来调用delete之外,也可以显式的调用delete函数,要单独嵌套一个类,把这个delete函数放进嵌套类的公有析构函数中。实现过程如下:

#include <iostream>
using namespace std;
class Singleton{
public:// 公有接口获取唯一实例static Singleton* getInstance() {if (instance == nullptr) {if (instance == nullptr) {instance = new Singleton();}}else {cout << "重复创建,返回已创建的实例。" << endl;}return instance;}
private:Singleton() {cout << "运行构造函数。" << endl;};~Singleton() {cout << "运行析构函数。" << endl;};// 定义一个删除器class Deleter {public:Deleter() {};~Deleter() {if (instance != nullptr) {cout << "删除器启动。" << endl;delete instance;instance = nullptr;}}};static Deleter deleter; // 删除器也是静态成员变量
private:// 静态私有对象static Singleton* instance;
};// 初始化
Singleton* Singleton::instance = nullptr;
Singleton::Deleter Singleton::deleter;int main()
{Singleton* s1 = Singleton::getInstance();Singleton* s2 = Singleton::getInstance();return 0;
}

运行结果为:

运行构造函数。
重复创建,返回已创建的实例。
删除器启动。
运行析构函数。

1.3 保证线程安全

首先修改一下1.1中的程序,主要是增加一些打印,然后用多个线程创建Singleton的实例,看看是否每个线程都是访问的同一个内存地址。

#include <iostream>
#include <thread>
using namespace std;class Singleton{
public:// 获取实例的接口static Singleton* getInstance() {if (instance == nullptr) {instance = new Singleton(); // 使用new来创建一个新的实例对象cout << "创建地址为:" << instance << endl;} else {cout << "重复创建,返回已创建的实例。" << endl;}return instance;}
private:// 静态私有对象static Singleton* instance;// 构造函数一定要私有,外部无法直接访问构造函数Singleton() {cout << "运行构造函数" << endl;};~Singleton() {cout << "运行析构函数" << endl;};
};
// 要在类外进行初始化!!!
Singleton* Singleton::instance = nullptr;int main(){// Singleton* s1 = Singleton::getInstance();// Singleton* s2 = Singleton::getInstance();thread t1([] {Singleton* s1 = Singleton::getInstance();});thread t2([] {Singleton* s2 = Singleton::getInstance();});t1.join();t2.join();return 0;
}

运行结果如下:

运行构造函数
创建地址为:0x7f0988000b60
运行构造函数
创建地址为:0x7f0980000b60

可以发现,两个线程分别new出了一段内存空间(有一定几率是同一段,会报重复创建)。显然,这违背了我们单例模式的初衷。

解决方法是进行加锁,让一个线程先执行完,另一个线程才能获得new的权限。代码如下:

#include <iostream>
#include <mutex>
#include <thread>
using namespace std;class Singleton{
public:static Singleton* getInstance() {if (instance == nullptr) {lock_guard<mutex> l(mutex1); // 加锁保证线程安全if (instance == nullptr) {instance = new Singleton();cout << "创建地址为:" << instance << endl;}}else {cout << "重复创建,返回已创建的实例。" << endl;}return instance;}
private:static mutex mutex1;// 锁static Singleton* instance;Singleton() {cout << "运行构造函数" << endl;};~Singleton() {cout << "运行析构函数" << endl;};
};// 初始化
Singleton* Singleton::instance = nullptr;
mutex Singleton::mutex1;int main(){thread t1([](){Singleton* s1 = Singleton::getInstance();});thread t2([](){Singleton* s2 = Singleton::getInstance();});t1.join();t2.join();return 0;
}

运行结果为:

运行构造函数
创建地址为:0x7f90d4000b60
重复创建,返回已创建的实例。

加锁后,即使是多个线程,也只会申请一块内存空间。

1.4 C++11版本的优雅解决方案

上面只是为了将这个问题表述清楚,在C++11中,static变量是可以保证线程安全的,同时直接用static变量而不用new,就可以获得线程安全的且无内存泄漏的优雅写法,如下:

#include <iostream>
#include <thread>
using namespace std;
class Singleton{
public:// 公有接口获取唯一实例static Singleton* getInstance() {static Singleton instance;cout << "地址为:" << &instance << endl;return &instance;}private:Singleton() {cout << "运行构造函数" << endl;};~Singleton() {cout << "运行析构函数" << endl;};
};int main()
{thread t1([] {Singleton* s1 = Singleton::getInstance();});thread t2([] {Singleton* s2 = Singleton::getInstance();});t1.join();t2.join();return 0;
}

运行结果如下:

运行构造函数
地址为:0x55b7df269152
地址为:0x55b7df269152
运行析构函数

可以看到访问的内存地址一样,析构函数也正常的运行了。


2. 饿汉式单例模式

饿汉式和懒汉式的差别是,饿汉式提前进行了创建,而如果提前创建static变量,那么在程序开始前这个变量就创建好了,因此不存在线程不安全的问题,只需要保证不内存泄漏即可。用智能指针的方式实现代码如下:

#include <iostream>
#include <thread>
using namespace std;
class Singleton{
public:// 公有接口获取唯一实例static shared_ptr<Singleton> getInstance() {cout << "地址为:" << instance << endl;return instance;}// 定义销毁的实例static void destoryInstance(Singleton* x) {cout << "自定义释放实例" << endl;delete x;}private:Singleton() {cout << "运行构造函数" << endl;};~Singleton() {cout << "运行析构函数" << endl;};
private:// 静态私有对象static shared_ptr<Singleton> instance;
};// 初始化
shared_ptr<Singleton> Singleton::instance(new Singleton(), destoryInstance);int main(){thread t1([] {shared_ptr<Singleton> s1 = Singleton::getInstance();});thread t2([] {shared_ptr<Singleton> s2 = Singleton::getInstance();});t1.join();t2.join();return 0;
}

运行结果为:

运行构造函数。
地址为:0x55977895ceb0
地址为:0x55977895ceb0
自定义释放实例
运行析构函数。

也可以考虑优雅的写法:

#include <iostream>
#include <thread>
using namespace std;
class Singleton{
public:// 公有接口获取唯一实例static Singleton* getInstance() {static Singleton instance;cout << "地址为:" << &instance << endl;return &instance;}private:// 私有构造函数Singleton() {cout << "运行构造函数。" << endl;};// 私有析构函数~Singleton() {cout << "运行析构函数。" << endl;};
};int main(){thread t1([] {Singleton* s1 = Singleton::getInstance();});thread t2([] {Singleton* s2 = Singleton::getInstance();});t1.join();t2.join();return 0;
}

输出的结果为:

运行构造函数。
地址为:0x55aa7a895152
地址为:0x55aa7a895152
运行析构函数。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

算法day11

算法day11 239 滑动窗口最大值237 前K个高频元素栈与队列总结 滑动窗口最大值 第一想法&#xff0c;暴力解&#xff1a;这个解法会超时。&#xff08;这就是为啥是困难题&#xff09; 思路&#xff1a;每到一个新的窗口&#xff0c;就重新进行一次窗口中的max迭代&#xff0c…

ubuntu22.04@laptop OpenCV Get Started: 000_hello_opencv

ubuntu22.04laptop OpenCV Get Started: 000_hello_opencv 1. 源由2. Hello OpenCV2.1 C应用Demo2.2 Python应用Demo 3. 参考资料 1. 源由 之前&#xff0c;通过敲门砖已经砸开了OpenCV的大门&#xff0c;接下来是体验下“Hello World&#xff01;”程序。 2. Hello OpenCV …

HarmonyOS鸿蒙ArkTS证件照生成模板(适合二次开发,全套源码版)

预览效果 部分代码 开发语言 HarmonyOS 鸿蒙 ArkTS语言 &#xff08;Stage模型&#xff09; 备注 一键生成&#xff0c;自带证件照数集&#xff0c; 为开发者带来二次开发和学习体验&#xff0c; 在这祝福开发者们使用愉快。 使用方法 下载后通过DevEco Studio开发工…

物联网ARM开发-STM32之RTC浅谈

RTC 一.RTC简单介绍 RTC好比我们用来记录时间的一个钟表&#xff0c;他里面有年月日&#xff0c;还可以记录星期&#xff0c;小时&#xff0c;分钟等。是Real Time Clock的缩写&#xff0c;译为实时时钟&#xff0c;本质上是一个独立的定时器。 1. 1 与通用定时器的区别 可以…

如何使用LNMP让网站顺利工作?

如何使用LNMP让网站顺利工作&#xff1f; 1. Nginx的安装和部署 2. nginxphpmysql 3. nginx php-fpm安装配置 4. Nginx配置性能优化的方法 5. 如何使用Nginx实现限制各种恶意访问

EEPROM之MB85RC64介绍

一、芯片介绍 工作频率 : 400 kHz &#xff08;最大&#xff09; 即&#xff0c;当主机和一个EEPROM通信时&#xff0c;从机地址为1010 000WR&#xff0c;如果主机和多个EEPROM通信时&#xff0c;从机地址为1010 A2 A1 A0 WR 二、时序分析 &#xff08;1&#xff09;从机地址…

快速排序板子(备战蓝桥杯)

题目&#xff1a; 活动 - AcWing 蓝桥杯省赛无忧班&#xff08;C&C 组&#xff09;第 4 期_蓝桥杯 - 蓝桥云课 【模板】排序 - 洛谷 板子&#xff1a; void quick_sort(int q[] , int l , int r) {if(l > r) return ;//这里的x 尽量折半查找 不然找左区间或者右区间…

适用于 Windows 和 Mac 的 16 款最佳数据恢复软件

数据恢复软件是找回因硬盘损坏、病毒攻击或意外删除数据等原因而在设备上丢失的数据的最佳方法。在数字世界中&#xff0c;丢失数据是一件非常糟糕的事情&#xff0c;这会让许多人的情况变得更糟。使用最佳数据恢复软件可以减轻您必须努力恢复丢失数据的压力。它将带回您的大部…

C# OpenCvSharp DNN 部署yolov4目标检测

目录 效果 项目 代码 下载 效果 项目 代码 using OpenCvSharp; using OpenCvSharp.Dnn; using System; using System.Collections.Generic; using System.Drawing; using System.IO; using System.Linq; using System.Windows.Forms;namespace OpenCvSharp_DNN_Demo {publ…

C# async/await的使用

C# 中的 async 和 await 关键字是用于实现异步编程的重要工具&#xff0c;它们简化了编写和维护非阻塞代码的过程。以下是对这两个关键字用法的简要说明&#xff1a; async 关键字 定义异步方法&#xff1a;在方法声明前使用 async 关键字&#xff0c;表示该方法是一个异步方…

Linux环境下配置mysql主从复制

主从配置需要注意的地方 1、主DB server和从DB server数据库的版本一致 2、主DB server和从DB server数据库数据一致[这里就会可以把主的备份在从上还原&#xff0c;也可以直接将主的数据目录拷贝到从的相应数据目录] 3、主DB server开启二进制日志,主DB server和从DB serve…

二分查找------蓝桥杯

题目描述&#xff1a; 请实现无重复数字的升序数组的二分查找 给定一个元素升序的、无重复数字的整型数组 nums 和一个目标值 target&#xff0c;写一个函数搜索 nums 中的target&#xff0c;如果目标值存在返回下标 (下标从0 开始)&#xff0c;否则返回-1 数据范围: 0 < l…

分布式文件存储系统minio

参考Linux搭建免费开源对象存储 wget https://dl.minio.io/server/minio/release/linux-amd64/minio yum install -y wget yum install -y wget wget https://dl.minio.io/server/minio/release/linux-amd64/minio chmod x minio sudo mv minio /usr/local/bin/ minio --vers…

外汇天眼:外汇天眼:注意,19个外汇平台因诈骗被监管拉黑!

近年来&#xff0c;全球金融市场出现了众多非法投资平台&#xff0c;这些平台利用虚假宣传和高回报承诺欺骗投资者&#xff0c;造成了严重的经济损失。为了保护投资者利益&#xff0c;监管机构也在加大力度打击这些非法平台。就在最近&#xff0c;又有19个外汇交易平台因涉嫌诈…

CSS 控制 video 标签的控制栏组件的显隐

隐藏下载功能 <video src"" controlsList"nodownload" />controlslist 取值如下(设定多个值则使用空格进行间隔) 如&#xff1a;controlslist"nodownload nofullscreen noremoteplayback"nodownload&#xff1a;取消更多控件弹窗的下载功…

系统学习Python——装饰器:函数装饰器-[添加装饰器参数:基础知识]

分类目录&#xff1a;《系统学习Python》总目录 前面文章介绍的计时器装饰器有效运行&#xff0c;但是如果它可配置性更强的话&#xff0c;就会更好一一一例如提供一个输出标签并且可以打开或关闭跟踪消息&#xff0c;这些在一个多用途工具中可能很有用。装饰器参数在这里派上了…

【C语言 - 力扣 - 反转链表】

反转链表题目描述 给你单链表的头节点 head &#xff0c;请你反转链表&#xff0c;并返回反转后的链表。 题解1-迭代 假设链表为 1→2→3→∅&#xff0c;我们想要把它改成 ∅←1←2←3。 在遍历链表时&#xff0c;将当前节点的 next 指针改为指向前一个节点。由于节点没…

计算机网络-差错控制(纠错编码 海明码 纠错方法)

文章目录 纠错编码-海明码海明距离1.确定校验码位数r2.确定校验码和数据的位置3.求出校验码的值4.检错并纠错纠错方法1纠错方法2 小结 纠错编码-海明码 奇偶校验码&#xff1a;只能发现错误不能找到错误位置和纠正错误 海明距离 如果找到码距为1&#xff0c;那肯定为1了&…

7、Qt5开发及实列(笔记)

文章目录 第二章 Qt5模板库、工具类及控件2.2 容器类2.2.1 QList类 # 2.3 QVariant类 #2.4 算法及正则表达式2.5控件 第二章 Qt5模板库、工具类及控件 2.2 容器类 2.2.1 QList类 //2.2容器类 - QList类QList<QString> list;//声明了一个QList<QString>栈对象{QSt…

idea运行程序报错 java 程序包org.junit不存在

在 IntelliJ IDEA 中运行程序时遇到错误提示&#xff1a;“java: 程序包org.junit不存在”&#xff0c;针对这一问题&#xff0c;我们可以考虑以下三步来解决&#xff1a; 第一步&#xff1a;检查JUnit依赖 尽管现代项目创建时通常会默认引入JUnit依赖&#xff0c;但仍需检查…