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

news2025/4/18 19:16:25

文章目录

  • 单例模式
  • 使用场景
  • 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 及更高标准中启用)时,编译器会确保静态局部变量的初始化是线程安全的。这意味着如果多个线程首次访问同一个静态局部变量,编译器会保证该变量只被初始化一次,并确保其他线程可以看到初始化后的正确值。
​​VS2015+​​:默认启用线程安全初始化(等效于主动设置 /Zc:threadSafeInit)。
​​VS2013 及之前​​:不启用线程安全初始化,需手动加锁或升级编译器

在项目- 属性 - C/C++ -命令行里可以查看。如果想禁用,输入/Zc:threadSafeInit-
在这里插入图片描述

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

饿汉式(线程安全)

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

在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.coloradmin.cn/o/2332265.html

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

相关文章

共享内存(与消息队列相似)

目录 共享内存概述 共享内存函数 &#xff08;1&#xff09;shmget函数 功能概述 函数原型 参数解释 返回值 示例 结果 &#xff08;2&#xff09;shmat函数 功能概述 函数原型 参数解释 返回值 &#xff08;3&#xff09;shmdt函数 功能概述 函数原型 参数解释…

2025年常见渗透测试面试题- PHP考察(题目+回答)

网络安全领域各种资源&#xff0c;学习文档&#xff0c;以及工具分享、前沿信息分享、POC、EXP分享。不定期分享各种好玩的项目及好用的工具&#xff0c;欢迎关注。 PHP考察 php的LFI&#xff0c;本地包含漏洞原理是什么&#xff1f;写一段带有漏洞的代码。手工的话如何发掘&am…

【C++进阶】关联容器:multimap类型

目录 一、multimap 基础概念与底层实现 1.1 定义与核心特性 1.2 底层数据结构 1.3 类模板定义 1.4 与其他容器的对比 二、multimap 核心操作详解 2.1 定义与初始化 2.2 插入元素 2.3 查找元素 2.4 删除元素 2.5 遍历元素 三、性能分析与适用场景 3.1 时间复杂度分…

远程管理命令:关机和重启

关机/重启 序号命令对应英文作用01shutdown 选项 时间shutdown关机 / 重新启动 一、shutdown shutdown 命令可以安全关闭 或者 重新启动系统。 选项含义-r重新启动 提示&#xff1a; 不指定选项和参数&#xff0c;默认表示 1 分钟之后 关闭电脑远程维护服务器时&#xff0…

【MySQL】001.MySQL安装

文章目录 一. MySQL在Ubuntu 20.04 环境安装1.1 更新软件包列表1.2 安装MySQL服务器1.3 配置安全设置1.4 检查mysql server是否正在运行1.5 进行连接1.6 查询自带的数据库 二. 配置文件的修改三. MySQL连接TCP/IP时的登陆问题四. MySQL中的命令 一. MySQL在Ubuntu 20.04 环境安…

vue 入门:组件事件

文章目录 vue介绍vue 入门简单示例自定义组件事件 vue介绍 vue2 官网 Vue (读音 /vjuː/&#xff0c;类似于 view) 是一套用于构建用户界面的渐进式框架。Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层。 vue 入门 Vue.js 的核心是一个允许采用简洁的模板语…

数据质量问题中,数据及时性怎么保证?如何有深度体系化回答!

数据治理&#xff0c;数据质量这快是中大厂&#xff0c;高阶大数据开发面试必备技能&#xff0c;企业基于大数据底座去做数仓&#xff0c;那么首先需要保障的就是数据质量。 数据质量的重要性在现代企业中变得越发突出。以下是数据质量的几个关键方面&#xff0c;说明其对企业…

数据可视化 —— 折线图应用(大全)

一、导入需要的库 # Matplotlib 是 Python 最常用的绘图库&#xff0c;pyplot 提供了类似 MATLAB 的绘图接口 import matplotlib.pyplot as plt import numpy as np import pandas as pd 二、常用的库函数 plt.plot(x轴,y轴)&#xff1a;plot()是画折线图的函数。 plt.xlabe…

什么是中性线、零线、地线,三相四线制如何入户用电

在变压器三相电侧&#xff0c;按照星形连接法&#xff0c;有一个中心点&#xff0c;这根线引出来的线接不接地&#xff1a;不接地就是中性线&#xff0c;接地就是零线 下面就是没有接地&#xff1a;中性线 接地了以后就可以叫做零线了 三相电在高压输电的时候是没有零线的&a…

【含文档+PPT+源码】基于Android家政服务系统的开发与实现

介绍视频&#xff1a; 课程简介&#xff1a; 本课程演示的是一款基于Android家政服务系统的开发与实现&#xff0c;主要针对计算机相关专业的正在做毕设的学生与需要项目实战练习的 Java 学习者。 1.包含&#xff1a;项目源码、项目文档、数据库脚本、软件工具等所有资料 2.…

配置mac mini M4 的一些软件

最近更换了 mac mini M4 &#xff0c;想要重新下载配置软件 &#xff0c;记录一下。 Homebrew是什么&#xff1f; homebrew是一款Mac OS平台下的软件包管理工具&#xff0c;拥有安装、卸载、更新、查看、搜索等功能。通过简单的指令可以实现包管理&#xff0c;而不用关心各种…

Java——抽象方法抽象类 接口 详解及综合案例

1.抽象方法抽象类 介绍 抽象方法: 将共性的行为(方法)抽取到父类之后&#xff0c; 由于每一个子类执行的内容是不一样&#xff0c; 所以&#xff0c;在父类中不能确定具体的方法体。 该方法就可以定义为抽象方法。 抽象类: 如果一个类中存在抽象方法&#xff0c;那么该类就必须…

【计网】一二章习题

1. (单选题, 3 分) 假设主机A和B之间的链路带宽为100Mbps&#xff0c;主机A的网卡速率为1Gbps&#xff0c;主机B的网卡速率为10Mbps&#xff0c;主机A给主机B发送数据的最高理论速率为&#xff08; &#xff09;。 A. 100Mbps B. 1Gbps C. 1Mbps D. 10Mbps 正确答案 D 发…

【软考-高级】【信息系统项目管理师】【论文基础】进度管理过程输入输出及工具技术的使用方法

定义 项目进度管理是为了保证项目按时完成&#xff0c;对项目中所需的各个过程进行管理的过程&#xff0c;包括规划进度、定义活动、活动优先级排序、活动持续时间、制定进度计划和控制进度。 管理基础 制定进度计划的一般步骤 选择进度计划方法&#xff08;如关键路径法&a…

TOGAF之架构标准规范-技术架构

TOGAF是工业级的企业架构标准规范&#xff0c;本文主要描述技术架构阶段。 如上所示&#xff0c;技术架构&#xff08;Technology Architecture&#xff09;在TOGAF标准规范中处于D阶段 技术架构阶段 技术架构阶段的主要内容包括阶段目标、阶段输入、流程步骤、阶段输出、架构…

Ansys Electronics 变压器 ACT

你好&#xff0c; 在本博客中&#xff0c;我将讨论如何使用 Ansys 电子变压器 ACT 自动快速地设计电力电子电感器或变压器。我将逐步介绍设计和创建电力电子变压器示例的步骤&#xff0c;该变压器为同心组件&#xff0c;双绕组&#xff0c;采用正弦电压激励&#xff0c;并应用…

十三种物联网/通信模块综合对比——《数据手册--物联网/通信模块》

物联网&#xff0f;通信模块 名称 功能 应用场景 USB转换模块 用于将USB接口转换为其他类型的接口&#xff0c;如串口、并口等&#xff0c;实现不同设备之间的通信。 常用于计算机与外部设备&#xff08;如打印机、扫描仪等&#xff09;的连接&#xff0c;以及数据传输和设…

Redis安装(Windows环境)

文章目录 Resid简介:下载Redis启动Redis服务设置Windows服务常用的Redis服务命令 Resid简介: Redis 是一个开源的使用 ANSI C 语言编写、遵守 BSD 协议、支持网络、可基于内存、分布式、可选持久性的键值对(Key-Value)存储数据库&#xff0c;并提供多种语言的 API。 Redis通常…

FreeRTOS项目工程完善指南:STM32F103C8T6系列

FreeRTOS项目工程完善指南&#xff1a;STM32系列 本文是FreeRTOS STM32开发系列教程的一部分。我们将完善之前移植的FreeRTOS工程&#xff0c;添加串口功能并优化配置文件。 更多优质资源&#xff0c;请访问我的GitHub仓库&#xff1a;https://github.com/Despacito0o/FreeRTO…

论坛系统(测试报告)

文章目录 一、项目介绍二、设计测试用例三、自动化测试用例的部分展示用户名或密码错误登录成功编辑自己的帖子成功修改个人信息成功回复帖子信息成功 四、性能测试总结 一、项目介绍 本平台是用Java开发&#xff0c;基于SpringBoot、SpringMVC、MyBatis框架搭建的小型论坛系统…