【设计模式】创建型 -- 单例模式 (c++实现)

文章目录

  • 单例模式
  • 使用场景
  • c++实现
    • 静态局部变量
    • 饿汉式(线程安全)
    • 懒汉式(线程安全)
    • 懒汉式(线程安全)+ 智能指针
    • 懒汉式(线程安全)+智能指针+call_once
    • 懒汉式(线程安全)+智能指针+call_once+CRTP


单例模式

单例模式是指在内存只会创建且仅创建一次对象的设计模式,确保在程序运行期间只有唯一的实例。

使用场景

当对象需要被共享的时候又或者某类需要频繁实例化.

  • 设备管理器,系统中可能有多个设备,但是只有一个设备管理器,用于管理设备驱动;
  • 数据池,用来缓存数据的数据结构,需要在一处写,多处读取或者多处写,多处读取;
  • 回收站,在整个系统运行过程中,回收站一直维护着仅有的一个实例;
  • 应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加;
  • 网站的计数器,一般也是采用单例模式实现,否则难以同步。

实际开发中,如果不是完美符合使用场景,不推荐使用。
如果实际开发经验不够,很容易看什么都是单例。

c++实现

单例模式的关键点:创建且仅创建一次对象
2个关键点:

  1. 如何只创建一次?
  2. 如何禁止拷贝和赋值?(保证只有一个)

静态局部变量

对于1:这很容易想到静态局部变量
当一个函数中定义一个局部静态变量,那么这个局部静态变量只会初始化一次,就是在这个函数第一次调用的时候,以后无论调用几次这个函数,函数内的局部静态变量都不再初始化。

对于2:可以将拷贝构造和赋值重载设置位私有成员。

综上,我们可以得到第一个版本

class Singleton1 
{
public:static Singleton1& getInstance(){static Singleton1 s_single;return s_single;}
private:Singleton1() = default;Singleton1(const Singleton1&) = delete;Singleton1& operator=(const Singleton1&) = delete;
};

上述版本的单例模式在C++11 以前存在多线程不安全的情况,多个线程同时执行这段代码,编译器可能会初始化多个静态变量

magic static, 它是C++11标准中提供的新特性

  • 如果在初始化变量时控制同时进入声明,则并发执行应等待初始化完成。
  • 如果当变量在初始化的时候,并发同时进入声明语句,并发线程将会阻塞等待初始化结束。

即c++规定各厂商优化编译器,能保证线程安全。所以为了保证运行安全请确保使用C++11以上的标准。

但是有些编译器它就是不遵循c++的规定,比如vistual studio

/Zc:threadSafeInit 是 Microsoft Visual Studio 编译器中的一个编译选项,作用是启用或禁用线程安全的静态局部变量初始化。这个选项对于 C++11 引入的“magic statics”(线程安全的静态局部变量)机制尤为重要。
当启用 /Zc:threadSafeInit(默认在 C++11 及更高标准中启用)时,编译器会确保静态局部变量的初始化是线程安全的。这意味着如果多个线程首次访问同一个静态局部变量,编译器会保证该变量只被初始化一次,并确保其他线程可以看到初始化后的正确值。

在项目- 属性 - C/C++ -命令行里可以查看,我这里没有,即默认开启。
在这里插入图片描述

实际开发中一定要注意是否遵循规定。
如果遵循,推荐使用静态局部变量的方式,又简单又安全。

饿汉式(线程安全)

饿汉式:程序启动即初始化

在C++11 推出以前,局部静态变量的方式实现单例存在线程安全问题,所以部分人提出了一种方案,就是在主线程启动后,其他线程没有启动前,由主线程先初始化单例资源,这样其他线程获取的资源就不涉及重复初始化的情况了。

//饿汉式初始化
class Singleton2
{
public:static Singleton2* getInstance(){if (s_single == nullptr){s_single = new Singleton2();}return s_single;}
private:Singleton2() = default;Singleton2(const Singleton2&) = delete;Singleton2& operator=(const Singleton2&) = delete;static Singleton2* s_single;
};
Singleton2* Singleton2::s_single = Singleton2::getInstance();

虽然从使用的角度规避多线程的安全问题,但是又引出了很多问题,如1. 启动即初始化,可能导致程序启动时间延长。2. 从规则上束缚了开发者

懒汉式(线程安全)

懒汉式:需要时即初始化

事例何时初始化应该由开发者决定。因此我们使用懒汉式初始化。但懒汉式初始化存在线程安全问题,即资源的重复初始化,因此,我们需要加锁。

#include <mutex>
class Singleton3
{
public:static Singleton3* getInstance(){//这里不加锁判断,提高性能if (s_single != nullptr){return s_single;}s_mutex.lock();//1处if (s_single != nullptr) //2处{s_mutex.unlock();return s_single;}s_single = new Singleton3();//3处s_mutex.unlock();return s_single;}
private:Singleton3() = default;Singleton3(const Singleton3&) = delete;Singleton3& operator=(const Singleton3&) = delete;static Singleton3* s_single;static std::mutex s_mutex;
};
Singleton3* Singleton3::s_single = nullptr;
std::mutex Singleton3::s_mutex;

为什么2处要加一个判断呢?
假如现在有线程A, B同时调用getInstance()

  1. 此时s_single == nullptr, A和B同时进入1处,假设A加上锁,B等待
  2. A执行完3处的命令后,通过s_mutex.unlock()解锁,此时B加上锁。
  3. 如果没有2处,B会再执行一遍3处,这会导致内存泄漏,而加上2处后,B会判断s_single != nullptr, 解锁返回

懒汉式(线程安全)+ 智能指针

但这还没完,懒汉式相比饿汉式有一个最大的不同:不确定是哪个线程初始化的。那之后由谁析构呢?
其实不必操心,我们可以利用c++的RAIII,使用智能指针。

#include <mutex>
class Singleton3
{
public:static std::shared_ptr<Singleton3> getInstance(){if (s_single != nullptr){return s_single;}s_mutex.lock();if (s_single != nullptr){s_mutex.unlock();return s_single;}s_single = std::shared_ptr<Singleton3>(new Singleton3);s_mutex.unlock();return s_single;}
private:Singleton3() = default;Singleton3(const Singleton3&) = delete;Singleton3& operator=(const Singleton3&) = delete;static std::shared_ptr<Singleton3> s_single;static std::mutex s_mutex;
};
std::shared_ptr<Singleton3> Singleton3::s_single = nullptr;
std::mutex Singleton3::s_mutex;

有些人认为虽然智能指针能自动回收内存,如果有开发人员手动delete指针怎么办?将析构函数设为私有,为智能指针添加删除器

#include <mutex>
class Singleton3
{
public:static std::shared_ptr<Singleton3> getInstance(){if (s_single != nullptr){return s_single;}s_mutex.lock();if (s_single != nullptr){s_mutex.unlock();return s_single;}s_single = std::shared_ptr<Singleton3>(new Singleton3, [](Singleton3* single) {delete single;});s_mutex.unlock();return s_single;}
private:Singleton3() = default;~Singleton3() = default; //析构私有Singleton3(const Singleton3&) = delete;Singleton3& operator=(const Singleton3&) = delete;static std::shared_ptr<Singleton3> s_single;static std::mutex s_mutex;
};
std::shared_ptr<Singleton3> Singleton3::s_single = nullptr;
std::mutex Singleton3::s_mutex;

上面的代码仍然存在危险,主要原因在于new操作是由三部分组成的

  1. 分配内存
    在第一个阶段,new 操作会调用内存分配函数(默认是 operator new),在堆上为新对象分配足够的空间。如果内存分配失败,通常会抛出 std::bad_alloc 异常。

  2. 调用构造函数
    分配到内存后,new 操作会在刚刚分配的内存上调用对象的构造函数,初始化该对象的各个成员。构造函数的参数可以在 new 语句中直接传递。

  3. 返回指针
    构造函数执行完毕后,new 操作会返回一个指向新创建对象的指针。如果是 new[] 操作符(即分配数组),则返回指向数组起始元素的指针

这里的问题就再2和3的顺序上,有些编译器会优化,将2和3的顺序颠倒。

    static Singleton3* getInstance(){if (s_single != nullptr) //1处{return s_single;}s_mutex.lock();if (s_single != nullptr) {s_mutex.unlock();return s_single;}s_single = new Singleton3();//2处s_mutex.unlock();return s_single;}

如果2和3的顺序颠倒,那么顺序变为
1.分配内存
3.返回指针
2.调用构造
可能出现下面的情况:
线程A执行到2处的new的第3步,此时s_single已经不为空,但是指向的对象还未调用构造。
线程B刚好执行1处,此时s_single != nullptr, 直接返回s_single。外部将接受到一个还没来的及调用构造函数的对象的指针。

为解决这个问题,C++11 推出了std::call_once函数保证多个线程只执行一次

懒汉式(线程安全)+智能指针+call_once

std::call_once 是 C++11 引入的一个函数,用于保证某段代码在多线程环境中只被执行一次。这对单例模式、懒加载或只需执行一次的初始化操作非常有用。
std::call_once 与一个 std::once_flag 对象配合使用。std::once_flag 是一个标志,确保 std::call_once 所调用的函数只会执行一次,不论有多少个线程试图同时调用它。

#include <mutex>
class Singleton
{
public:static std::shared_ptr<Singleton> getInstance(){static std::once_flag s_flag;std::call_once(s_flag, [&]() {s_single = std::shared_ptr<Singleton>(new Singleton, [](Singleton* single) {delete single;});});return s_single;}
private:Singleton() = default;~Singleton() = default; //析构私有Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;static std::shared_ptr<Singleton> s_single;
};
std::shared_ptr<Singleton> Singleton::s_single = nullptr;

懒汉式(线程安全)+智能指针+call_once+CRTP

为了让单例类更通用,可以通过继承实现多个单例类。
注:这里需要使用c++的CRTP(奇异递归模板模式),不知道是什么,自己查一下。

#include <mutex>
template<typename T>
class Singleton
{
public:static std::shared_ptr<T> getInstance(){static std::once_flag s_flag;std::call_once(s_flag, [&]() {s_instance = std::shared_ptr<T>(new T);});return s_instance;}
protected: Singleton() = default;Singleton(const Singleton<T>&) = delete;Singleton& operator=(const Singleton<T>&) = delete;static std::shared_ptr<T> s_instance;
};
template<typename T>
std::shared_ptr<T> Singleton<T>::s_instance = nullptr;class A :public Singleton<A> //CRTP
{friend class Singleton<A>;
public://...};

friend class Singleton<A>;的目的是允许 Singleton<A> 类访问 A 的受保护构造函数。没有这个 friend 声明,Singleton<A> 将无法调用 A 的构造函数,从而无法在 getInstance 方法中正确地创建 A 的实例。

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

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

相关文章

C语言之九九乘法表

一、代码展示 二、运行结果 三、代码分析 首先->是外层循环是小于等于9的 然后->是内层循环是小于等于外层循环的 最后->就是\n让九九乘法表的格式更加美观(当然 电脑不同 有可能%2d 也有可能%3d) 四、与以下素数题目逻辑相似 五、运行结果

自动化备份全网服务器数据平台

自动化备份全网服务器数据平台 项目背景知识 总体需求 某企业里有一台Web服务器&#xff0c;里面的数据很重要&#xff0c;但是如果硬盘坏了数据就会丢失&#xff0c;现在领导要求把数据做备份&#xff0c;这样Web服务器数据丢失在可以进行恢复。要求如下&#xff1a;1.每天0…

stm32+esp8266+机智云手机app

现在很多大学嵌入式毕设都要求云端控制&#xff0c;本文章就教一下大家如何使用esp8266去连接机智云的app去进行显示stm32的外设传感器数据啊&#xff0c;控制一些外设啊等。 因为本文章主要教大家如何移植机智云的代码到自己的工程&#xff0c;所以前面的一些准备工作&#x…

时序数据库 TDengine Cloud 私有连接实战指南:4步实现数据安全传输与成本优化

小T导读&#xff1a;在物联网和工业互联网场景下&#xff0c;企业对高并发、低延迟的数据处理需求愈发迫切。本文将带你深入了解 TDengineCloud 如何通过全托管服务与私有连接&#xff0c;帮助企业实现更安全、更高效、更低成本的数据采集与传输&#xff0c;从架构解析到实际配…

【Java面试系列】Spring Boot中自动配置原理与自定义Starter开发实践详解 - 3-5年Java开发必备知识

【Java面试系列】Spring Boot中自动配置原理与自定义Starter开发实践详解 - 3-5年Java开发必备知识 引言 Spring Boot作为Java生态中最流行的框架之一&#xff0c;其自动配置机制和Starter开发是面试中的高频考点。对于3-5年经验的Java开发者来说&#xff0c;深入理解这些原理…

解决Spring Boot Test中的ByteBuddy类缺失问题

目录 解决Spring Boot Test中的ByteBuddy类缺失问题前奏问题描述问题解决第一步&#xff1a;移除ByteBuddy的特定版本号第二步&#xff1a;更新maven-surefire-plugin配置第三步&#xff1a;清理并重新构建项目 结语 解决Spring Boot Test中的ByteBuddy类缺失问题 前奏 今天&…

IntelliJ IDEA使用技巧(json字符串格式化)

文章目录 一、IDEA自动格式化json字符串二、配置/查找格式化快捷键 本文主要讲述idea中怎么将json字符串转换为JSON格式的内容并且有层级结构。 效果&#xff1a; 转换前&#xff1a; 转换后&#xff1a; 一、IDEA自动格式化json字符串 步骤一&#xff1a;首先创建一个临…

眨眼睛查看密码工具类

“眨眼睛查看密码”工具类实现思路&#xff1a; 一、核心功能 实现点击眼睛图标切换密码明文/星号显示&#xff0c;提升表单输入体验。包含以下关键功能&#xff1a; • 初始状态&#xff1a;密码框显示为星号&#xff0c;闭眼图标可见。 • 点击闭眼图标&#xff1a;切换为明…

【GPT入门】第33课 从应用场景出发,区分 TavilyAnswer 和 TavilySearchResults,代码实战

【GPT入门】第33课 从应用场景出发&#xff0c;区分 TavilyAnswer 和 TavilySearchResults&#xff0c;代码实战 1. 区别应用场景 2. 代码使用3.代码执行效果 在langchain_community.tools.tavily_search中&#xff0c;TavilyAnswer和TavilySearchResults有以下区别和应用场景&…

【Java设计模式】第10章 外观模式讲解

10. 外观模式 10.1 外观模式讲解 定义:为子系统提供统一接口,简化调用。类型:结构型模式适用场景: 子系统复杂需简化调用分层系统需统一入口优点: 降低耦合符合迪米特法则(最少知道原则)缺点: 扩展子系统需修改外观类,违反开闭原则10.2 外观模式 Coding // 子系统:…

Dubbo的简单介绍

Dubbo的简单介绍 Dubbo 是一个高性能的 Java RPC 框架&#xff0c;最初由阿里巴巴开发&#xff0c;用于构建分布式服务。它主要用于提供服务间的通信&#xff0c;支持高效的远程调用和服务治理&#xff0c;常用于大规模分布式系统中。Dubbo 提供了以下几个核心功能&#xff1a…

每日一题(小白)数组娱乐篇17

对一个数组进行接收进行操作后输出。输入三个操作数abc&#xff0c;将数组下标a到b的数字加上c&#xff1b;输入四个操作数abcd&#xff0c;将下标c到d的数字复制到a到b&#xff0c;可以借用一个中间量数组实现&#xff1b;两个操作数ab&#xff0c;将数组下标a到b的数字加和输…

总结一下常见的EasyExcel面试题

说一下你了解的POI和EasyExcel POI&#xff08;Poor Obfuscation Implementation&#xff09;&#xff1a;它是 Apache 软件基金会的一个开源项目&#xff0c;为 Java 程序提供了读写 Microsoft Office 格式文件的功能&#xff0c;支持如 Excel、Word、PowerPoint 等多种文件格…

01-Redis-基础

1 redis诞生历程 redis的作者笔名叫做antirez&#xff0c;2008年的时候他做了一个记录网站访问情况的系统&#xff0c;比如每天有多少个用户&#xff0c;多少个页面被浏览&#xff0c;访客的IP、操作系统、浏览器、使用的搜索关键词等等(跟百度统计、CNZZ功能一样)。最开始存储…

在 Ubuntu 上离线安装 Prometheus 和 Grafana

在 Ubuntu 上离线安装 Prometheus 和 Grafana 的步骤如下: 一.安装验证 二.安装步骤 1.准备离线安装包 在一台可以访问互联网的机器上下载 Prometheus 和 Grafana 的二进制文件。 Prometheus 下载地址:Prometheus 官方下载页面Grafana 下载地址:Grafana 官方下载页面下载所…

mapbox基础,加载ESRI OpenStreetMap开放街景标准风格矢量图

👨‍⚕️ 主页: gis分享者 👨‍⚕️ 感谢各位大佬 点赞👍 收藏⭐ 留言📝 加关注✅! 👨‍⚕️ 收录于专栏:mapbox 从入门到精通 文章目录 一、🍀前言1.1 ☘️mapboxgl.Map 地图对象1.1 ☘️mapboxgl.Map style属性二、🍀加载ESRI OpenStreetMap开放街景标准风…

Java 集合有序性与重复性总结及记忆技巧

Java 集合有序性与重复性总结及记忆技巧 一、集合分类速查表 集合类型是否有序是否允许重复记忆口诀ArrayList✅ 有序&#xff08;插入顺序&#xff09;✅ 可重复"数组列表&#xff0c;顺序记牢"LinkedList✅ 有序&#xff08;插入顺序&#xff09;✅ 可重复"…

记录学习的第二十三天

老样子&#xff0c;每日一题开胃。 我一开始还想着暴力解一下试试呢&#xff0c;结果不太行&#x1f602; 接着两道动态规划。 这道题我本来是想用最长递增子序列来做的&#xff0c;不过实在是太麻烦了&#xff0c;实在做不下去了。 然后看了题解&#xff0c;发现可以倒着数。 …

MTK-Android12-13 屏幕永不休眠功能实现

MTK-Android12-13 屏幕永不休眠功能实现 文章目录 需求场景参考资料修改文件简要分析实现方案默认休眠时间设置 def_screen_off_timeout息屏时间添加永不休眠 screen_timeout_entries更新休眠时间 updateUserActivitySummaryLocked 总结 需求 屏幕永不休眠功能 备注&#xff…

Lua 中,`math.random` 的详细用法

在 Lua 中&#xff0c;math.random 是用于生成伪随机数的核心函数。以下是其详细用法、注意事项及常见问题的解决方案&#xff1a; Lua 中&#xff0c;math.random 的详细用法—目录 一、基础用法1. 生成随机浮点数&#xff08;0 ≤ x < 1&#xff09;2. 生成指定范围的随机…