当前位置: 首页 > article >正文

C++八股 —— 单例模式

文章目录

  • 1. 基本概念
  • 2. 设计要点
  • 3. 实现方式
  • 4. 详解懒汉模式

1. 基本概念

线程安全(Thread Safety)

线程安全是指在多线程环境下,某个函数、类或代码片段能够被多个线程同时调用时,仍能保证数据的一致性和逻辑的正确性,不会因线程切换导致错误结果。

单例模式(Singleton Pattern)

单例设计模式是一种创建型设计模式,其核心目的是确保一个类只有一个实例存在,并提供全局访问点来获取该实例。它常用于管理全局资源(如配置信息、日志系统、数据库连接池等),避免重复创建和资源竞争。

2. 设计要点

  1. 构造函数和析构函数是私有的,不允许外部生成和释放
    • 禁止外部实例化:外部代码无法通过 new 或直接声明的方式创建对象,确保唯一实例的控制权在类自身。
    • 控制生命周期:析构函数私有化可防止外部意外删除单例对象,保证其生命周期与程序一致。
    • 符合单一职责原则:类的创建和销毁逻辑由自身管理,避免外部干扰。
  2. 静态成员变量和静态返回单例的成员函数
    • 全局访问点:通过静态方法 getInstance() 提供统一的实例获取方式,替代直接访问全局变量。
    • 延迟初始化(懒汉式):仅在首次调用时创建实例,节省资源。
    • 线程安全(需额外处理):可通过锁或局部静态变量(C++11 后)确保多线程安全。
      • 在单例模式中,如果多个线程同时调用 getInstance() 方法,可能导致多次创建实例(如懒汉模式未加锁时),破坏单例的唯一性。
      • 解决方案
        • 加锁(互斥量):在 getInstance() 中使用互斥锁(如 std::mutex)确保线程同步。
        • 局部静态变量(C++11):利用编译器保证局部静态变量的初始化是线程安全的。
        • 饿汉模式:提前初始化实例,避免多线程竞争。
  3. 禁用拷贝构造函数和赋值运算符
    • 防止拷贝:避免通过拷贝构造函数复制单例对象,破坏唯一性。
    • 防止赋值:禁止通过赋值运算符覆盖单例对象,如 instance2 = instance1
    • 强制单例约束:从语法层面杜绝意外破坏单例模式的行为。
要点解决的问题实际意义
私有构造/析构外部随意创建或销毁实例确保实例的唯一性和可控性
静态成员与访问方法全局访问与资源管理提供统一入口,支持延迟初始化与线程安全
禁用拷贝与赋值意外复制导致多实例维护单例的严格唯一性

3. 实现方式

懒汉模式

懒汉模式的核心是延迟初始化(Lazy Initialization),即在首次调用 getInstance() 时才创建单例实例。在此之前,实例未被分配内存。

特点

  • 优点
    • 节省资源:若单例对象未被使用,则不会创建。
    • 适合初始化耗时的对象(如文件系统、网络连接)。
  • 缺点
    • 需处理线程安全问题(多线程下可能重复创建)。
    • 首次访问可能因初始化导致延迟。

饿汉模式

饿汉模式的核心是提前初始化,即在程序启动时(或类加载时)直接创建单例实例,无论是否被使用。

特点

  • 优点
    • 线程安全:实例在程序启动时初始化,避免多线程竞争。
    • 代码简单:无需处理复杂的线程同步逻辑。
  • 缺点
    • 可能浪费资源:即使未使用单例对象,也会占用内存。
    • 初始化时间可能影响程序启动速度。

实现样例

class Singleton {
public:static Singleton* getInstance() {return &instance; // 直接返回已初始化的实例}
private:static Singleton instance;Singleton() {}~Singleton() {}
};
// 程序启动时初始化(饿汉模式)
Singleton Singleton::instance;

对比懒汉模式与饿汉模式

特性懒汉模式饿汉模式
初始化时机首次调用 getInstance()程序启动时(或类加载时)
线程安全需额外处理(如加锁或 C++11 特性)天然线程安全
资源占用按需分配,节省资源提前占用内存,可能浪费资源
适用场景初始化耗时、使用频率不确定的对象初始化简单、使用频繁的对象

实际开发中,推荐使用 C++11 的局部静态变量懒汉模式(Meyers’ Singleton,线程安全且代码简洁),或根据场景选择饿汉模式。

4. 详解懒汉模式

参考:【C++面试题】手撕单例模式_哔哩哔哩_bilibili

样例1

class Singleton1 {
public:// 要点2static Singleton1 * GetInstance() {if(_instance == nullptr) {_instance = new Singleton1();}return _instance;}
private:// 要点1Singleton1() {}~Singleton1() {std::cout << "~Singleton1()\n";}// 要点3Singleton1(const Singleton1 &) = delete;Singleton1& operator = (const Singleton1&) = delete;Singleton1(Singleton1 &&) = delete;Singleton1& operator = (Singleton1 &&) = delete;// 要点2static Singleton1 *_instance; 
};
Singleton1* Singleton1::_instance = nullptr;

存在错误:

  • 该类创建的单例对象在堆中,虽然资源会被释放,但其在释放的时候是无法调用析构函数的。
  • 非线程安全

样例2

class Singleton2 {
public:static Singleton2 * GetInstance() {if(_instance == nullpte) {_instance = new Singleton2();atexit(Destructor);}return _instance;}
private:static void Destructor() {if(nullptr != _instance) {delete _instance;_instance = nullptr;}}Singleton2() {}~Singleton2() {std::cout << "~Singleton2()\n";}Singleton2(const Singleton2 &) = delete;Singleton2& operator = (const Singleton2&) = delete;Singleton2(Singleton2 &&) = delete;Singleton2& operator = (Singleton2 &&) = delete;static Singleton2 *_instance; 
};
Singleton2* Singleton2::_instance = nullptr;

针对样例1的问题,添加atexit(),在程序结束时手动释放对象,从而调用析构函数

存在问题:

  • 非线程安全

样例3

class Singleton3 {
public:static Singleton3 * GetInstance() {std::lock_guard<std::mutex> lock(_mutex);if(_instance == nullptr) {std::lock_guard<std::mutex> lock(_mutex);if(_instance == nullptr) {_instance = new Singleton3();// 1. 分配内存// 2. 调用构造函数// 3. 返回对象指针 atexit(Destructor);}}return _instance;}
private:static void Destructor() {if(nullptr != _instance) {delete _instance;_instance = nullptr;}}Singleton3() {}~Singleton3() {std::cout << "~Singleton3()\n";}Singleton3(const Singleton3 &) = delete;Singleton3& operator = (const Singleton3&) = delete;Singleton3(Singleton3 &&) = delete;Singleton3& operator = (Singleton3 &&) = delete;static Singleton3 *_instance; static std::mutex _mutex;
};
Singleton3* Singleton3::_instance = nullptr;
std::mutex Singleton3::_mutex;

在创建实例对象是使用互斥锁来实现线程安全

  • 单检测

    先加锁,再判断是否需要创建对象;

    该方法只需要检测一次,但是在已经创建对象的情况下,只需要检测然后返回就行,不需要再第一次检测前加锁(力度过大,效率低)

  • 双检测(Double-Checked Locking,DCL)

    先做第一次检测,然后在需要创建对象时才加锁,此时多线程程序会出现多个线程同时通过一次检测到创建对象的代码块,所以需要第二次检测对象是否创建来避免重复创建

存在问题

在多线程程序中,CPU会进行指令重排,如new操作的正常顺序应该是(1-2-3),在指令重排之后执行顺会变为(1-3-2)。此时如果某个线程执行到new的“返回对象指针操作”,而另外一个线程执行到第一次检测,则会出现另外一个线程返回为初始化对象的情况。


样例4:(面试八股的重点

class Singleton4 {
public:static Singleton4 * GetInstance() {Singleton4* tmp = _instance.load(std::memory_order_relaxed);std::atomic_thread_fence(std::memory_order_acquire);if(tmp == nullptr) {std::lock_guard<std::mutex> lock(_mutex);tmp = _instance.load(std::memory_order_relaxed);if(tmp == nullptr) {tmp = new Singleton4();std::atomic_thread_fence(std::memory_order_release);_instance.store(tmp, std::memory_order_relaxed);atexit(Destructor);}}return tmp;}
private:static void Destructor() {Singleton4* tmp = _instance.load(std::memory_order_relaxed);if(nullptr != tmp) {delete tmp;}}Singleton4() {}~Singleton4() {std::cout << "~Singleton4()\n";}Singleton4(const Singleton4 &) = delete;Singleton4& operator = (const Singleton4&) = delete;Singleton4(Singleton4 &&) = delete;Singleton4& operator = (Singleton4 &&) = delete;static std::atomic<Singleton4*> _instance;static std::mutex _mutex;
};
std::atomic<Singleton4*> Singleton4::_instance;
std::mutex Singleton4::_mutex;

使用内存屏障和原子操作来解决指令重排的问题

内存屏障

  • 作用
    强制限制指令重排,并确保内存操作的可见性(即一个线程的写入对其他线程立即可见)。
  • 类型
    • 获取屏障(acquire fence)
      后续读/写操作不会重排到屏障前,且能读取其他线程的释放操作结果。
    • 释放屏障(release fence)
      前面的读/写操作不会重排到屏障后,且保证当前线程的写入对其他线程可见。
  • 代码中的应用
    • 获取屏障:确保 if(tmp == nullptr) 之后的代码能看到其他线程的完整初始化结果。
    • 释放屏障:确保 new 的构造操作完成后,再存储指针到 _instance

原子操作

  • 定义
    不可分割的操作,保证对变量的读写要么完全执行,要么不执行,不会出现中间状态。
  • 内存顺序(Memory Order)
    • memory_order_relaxed:仅保证原子性,无同步或顺序约束(允许指令重排)。
    • memory_order_acquire/release:与屏障配合,实现同步语义。
  • 代码中的应用
    _instance 被声明为 std::atomic<Singleton4*>,确保其读写是原子的,避免数据竞争。

原子操作详情参考:C++八股 —— 原子操作-CSDN博客


样例5

class Singleton5 {
public:static Singleton5* GetInstance() {static Singleton5 instance;return &instance;}
private:Singleton5() {}~Singleton5() {std::cout << "~Singleton5()\n";}Singleton5(const Singleton5 &) = delete;Singleton5& operator = (const Singleton5&) = delete;Singleton5(Singleton5 &&) = delete;Singleton5& operator = (Singleton5 &&) = delete;
};

静态局部变量具备单例的全部三个特性

最简单也是最推荐的版本


样例6

template<typename T>
class Singleton {
public:static T* GetInstance() {static T instance;return &instance;}
protected:Singleton() {}virtual ~Singleton() {std::cout << "~Singleton()\n";}
private:Singleton(const Singleton &) = delete;Singleton& operator = (const Singleton&) = delete;Singleton(Singleton &&) = delete;Singleton& operator = (Singleton &&) = delete;
};class DesignPattern : public Singleton<DesignPattern> {friend class Singleton<DesignPattern>;
private:DesignPattern() {}~DesignPattern() {std::cout << "~DesignPattern()\n";}
};

类模板封装单例的三个特性,使用时直接继承即可。

  • 基类构造和析构函数设置为protected是因为需要其对子类时可见的
  • 友元是为了让基类能访问子类的构造析构函数

相关文章:

C++八股 —— 单例模式

文章目录 1. 基本概念2. 设计要点3. 实现方式4. 详解懒汉模式 1. 基本概念 线程安全&#xff08;Thread Safety&#xff09; 线程安全是指在多线程环境下&#xff0c;某个函数、类或代码片段能够被多个线程同时调用时&#xff0c;仍能保证数据的一致性和逻辑的正确性&#xf…...

Map相关知识

数据结构 二叉树 二叉树&#xff0c;顾名思义&#xff0c;每个节点最多有两个“叉”&#xff0c;也就是两个子节点&#xff0c;分别是左子 节点和右子节点。不过&#xff0c;二叉树并不要求每个节点都有两个子节点&#xff0c;有的节点只 有左子节点&#xff0c;有的节点只有…...

selenium学习实战【Python爬虫】

selenium学习实战【Python爬虫】 文章目录 selenium学习实战【Python爬虫】一、声明二、学习目标三、安装依赖3.1 安装selenium库3.2 安装浏览器驱动3.2.1 查看Edge版本3.2.2 驱动安装 四、代码讲解4.1 配置浏览器4.2 加载更多4.3 寻找内容4.4 完整代码 五、报告文件爬取5.1 提…...

图表类系列各种样式PPT模版分享

图标图表系列PPT模版&#xff0c;柱状图PPT模版&#xff0c;线状图PPT模版&#xff0c;折线图PPT模版&#xff0c;饼状图PPT模版&#xff0c;雷达图PPT模版&#xff0c;树状图PPT模版 图表类系列各种样式PPT模版分享&#xff1a;图表系列PPT模板https://pan.quark.cn/s/20d40aa…...

tree 树组件大数据卡顿问题优化

问题背景 项目中有用到树组件用来做文件目录&#xff0c;但是由于这个树组件的节点越来越多&#xff0c;导致页面在滚动这个树组件的时候浏览器就很容易卡死。这种问题基本上都是因为dom节点太多&#xff0c;导致的浏览器卡顿&#xff0c;这里很明显就需要用到虚拟列表的技术&…...

Spring AI与Spring Modulith核心技术解析

Spring AI核心架构解析 Spring AI&#xff08;https://spring.io/projects/spring-ai&#xff09;作为Spring生态中的AI集成框架&#xff0c;其核心设计理念是通过模块化架构降低AI应用的开发复杂度。与Python生态中的LangChain/LlamaIndex等工具类似&#xff0c;但特别为多语…...

Spring数据访问模块设计

前面我们已经完成了IoC和web模块的设计&#xff0c;聪明的码友立马就知道了&#xff0c;该到数据访问模块了&#xff0c;要不就这俩玩个6啊&#xff0c;查库势在必行&#xff0c;至此&#xff0c;它来了。 一、核心设计理念 1、痛点在哪 应用离不开数据&#xff08;数据库、No…...

如何在最短时间内提升打ctf(web)的水平?

刚刚刷完2遍 bugku 的 web 题&#xff0c;前来答题。 每个人对刷题理解是不同&#xff0c;有的人是看了writeup就等于刷了&#xff0c;有的人是收藏了writeup就等于刷了&#xff0c;有的人是跟着writeup做了一遍就等于刷了&#xff0c;还有的人是独立思考做了一遍就等于刷了。…...

MySQL用户和授权

开放MySQL白名单 可以通过iptables-save命令确认对应客户端ip是否可以访问MySQL服务&#xff1a; test: # iptables-save | grep 3306 -A mp_srv_whitelist -s 172.16.14.102/32 -p tcp -m tcp --dport 3306 -j ACCEPT -A mp_srv_whitelist -s 172.16.4.16/32 -p tcp -m tcp -…...

如何理解 IP 数据报中的 TTL?

目录 前言理解 前言 面试灵魂一问&#xff1a;说说对 IP 数据报中 TTL 的理解&#xff1f;我们都知道&#xff0c;IP 数据报由首部和数据两部分组成&#xff0c;首部又分为两部分&#xff1a;固定部分和可变部分&#xff0c;共占 20 字节&#xff0c;而即将讨论的 TTL 就位于首…...

Swagger和OpenApi的前世今生

Swagger与OpenAPI的关系演进是API标准化进程中的重要篇章&#xff0c;二者共同塑造了现代RESTful API的开发范式。 本期就扒一扒其技术演进的关键节点与核心逻辑&#xff1a; &#x1f504; 一、起源与初创期&#xff1a;Swagger的诞生&#xff08;2010-2014&#xff09; 核心…...

Android 之 kotlin 语言学习笔记三(Kotlin-Java 互操作)

参考官方文档&#xff1a;https://developer.android.google.cn/kotlin/interop?hlzh-cn 一、Java&#xff08;供 Kotlin 使用&#xff09; 1、不得使用硬关键字 不要使用 Kotlin 的任何硬关键字作为方法的名称 或字段。允许使用 Kotlin 的软关键字、修饰符关键字和特殊标识…...

C++ Visual Studio 2017厂商给的源码没有.sln文件 易兆微芯片下载工具加开机动画下载。

1.先用Visual Studio 2017打开Yichip YC31xx loader.vcxproj&#xff0c;再用Visual Studio 2022打开。再保侟就有.sln文件了。 易兆微芯片下载工具加开机动画下载 ExtraDownloadFile1Info.\logo.bin|0|0|10D2000|0 MFC应用兼容CMD 在BOOL CYichipYC31xxloaderDlg::OnIni…...

【HarmonyOS 5 开发速记】如何获取用户信息(头像/昵称/手机号)

1.获取 authorizationCode&#xff1a; 2.利用 authorizationCode 获取 accessToken&#xff1a;文档中心 3.获取手机&#xff1a;文档中心 4.获取昵称头像&#xff1a;文档中心 首先创建 request 若要获取手机号&#xff0c;scope必填 phone&#xff0c;permissions 必填 …...

【Oracle】分区表

个人主页&#xff1a;Guiat 归属专栏&#xff1a;Oracle 文章目录 1. 分区表基础概述1.1 分区表的概念与优势1.2 分区类型概览1.3 分区表的工作原理 2. 范围分区 (RANGE Partitioning)2.1 基础范围分区2.1.1 按日期范围分区2.1.2 按数值范围分区 2.2 间隔分区 (INTERVAL Partit…...

CMake控制VS2022项目文件分组

我们可以通过 CMake 控制源文件的组织结构,使它们在 VS 解决方案资源管理器中以“组”(Filter)的形式进行分类展示。 🎯 目标 通过 CMake 脚本将 .cpp、.h 等源文件分组显示在 Visual Studio 2022 的解决方案资源管理器中。 ✅ 支持的方法汇总(共4种) 方法描述是否推荐…...

【开发技术】.Net使用FFmpeg视频特定帧上绘制内容

目录 一、目的 二、解决方案 2.1 什么是FFmpeg 2.2 FFmpeg主要功能 2.3 使用Xabe.FFmpeg调用FFmpeg功能 2.4 使用 FFmpeg 的 drawbox 滤镜来绘制 ROI 三、总结 一、目的 当前市场上有很多目标检测智能识别的相关算法&#xff0c;当前调用一个医疗行业的AI识别算法后返回…...

RNN避坑指南:从数学推导到LSTM/GRU工业级部署实战流程

本文较长&#xff0c;建议点赞收藏&#xff0c;以免遗失。更多AI大模型应用开发学习视频及资料&#xff0c;尽在聚客AI学院。 本文全面剖析RNN核心原理&#xff0c;深入讲解梯度消失/爆炸问题&#xff0c;并通过LSTM/GRU结构实现解决方案&#xff0c;提供时间序列预测和文本生成…...

【碎碎念】宝可梦 Mesh GO : 基于MESH网络的口袋妖怪 宝可梦GO游戏自组网系统

目录 游戏说明《宝可梦 Mesh GO》 —— 局域宝可梦探索Pokmon GO 类游戏核心理念应用场景Mesh 特性 宝可梦玩法融合设计游戏构想要素1. 地图探索&#xff08;基于物理空间 广播范围&#xff09;2. 野生宝可梦生成与广播3. 对战系统4. 道具与通信5. 延伸玩法 安全性设计 技术选…...

精益数据分析(97/126):邮件营销与用户参与度的关键指标优化指南

精益数据分析&#xff08;97/126&#xff09;&#xff1a;邮件营销与用户参与度的关键指标优化指南 在数字化营销时代&#xff0c;邮件列表效度、用户参与度和网站性能等指标往往决定着创业公司的增长成败。今天&#xff0c;我们将深入解析邮件打开率、网站可用性、页面参与时…...

是否存在路径(FIFOBB算法)

题目描述 一个具有 n 个顶点e条边的无向图&#xff0c;该图顶点的编号依次为0到n-1且不存在顶点与自身相连的边。请使用FIFOBB算法编写程序&#xff0c;确定是否存在从顶点 source到顶点 destination的路径。 输入 第一行两个整数&#xff0c;分别表示n 和 e 的值&#xff08;1…...

Element Plus 表单(el-form)中关于正整数输入的校验规则

目录 1 单个正整数输入1.1 模板1.2 校验规则 2 两个正整数输入&#xff08;联动&#xff09;2.1 模板2.2 校验规则2.3 CSS 1 单个正整数输入 1.1 模板 <el-formref"formRef":model"formData":rules"formRules"label-width"150px"…...

学习STC51单片机32(芯片为STC89C52RCRC)OLED显示屏2

每日一言 今天的每一份坚持&#xff0c;都是在为未来积攒底气。 案例&#xff1a;OLED显示一个A 这边观察到一个点&#xff0c;怎么雪花了就是都是乱七八糟的占满了屏幕。。 解释 &#xff1a; 如果代码里信号切换太快&#xff08;比如 SDA 刚变&#xff0c;SCL 立刻变&#…...

大数据学习(132)-HIve数据分析

​​​​&#x1f34b;&#x1f34b;大数据学习&#x1f34b;&#x1f34b; &#x1f525;系列专栏&#xff1a; &#x1f451;哲学语录: 用力所能及&#xff0c;改变世界。 &#x1f496;如果觉得博主的文章还不错的话&#xff0c;请点赞&#x1f44d;收藏⭐️留言&#x1f4…...

3-11单元格区域边界定位(End属性)学习笔记

返回一个Range 对象&#xff0c;只读。该对象代表包含源区域的区域上端下端左端右端的最后一个单元格。等同于按键 End 向上键(End(xlUp))、End向下键(End(xlDown))、End向左键(End(xlToLeft)End向右键(End(xlToRight)) 注意&#xff1a;它移动的位置必须是相连的有内容的单元格…...

Rapidio门铃消息FIFO溢出机制

关于RapidIO门铃消息FIFO的溢出机制及其与中断抖动的关系&#xff0c;以下是深入解析&#xff1a; 门铃FIFO溢出的本质 在RapidIO系统中&#xff0c;门铃消息FIFO是硬件控制器内部的缓冲区&#xff0c;用于临时存储接收到的门铃消息&#xff08;Doorbell Message&#xff09;。…...

蓝桥杯3498 01串的熵

问题描述 对于一个长度为 23333333的 01 串, 如果其信息熵为 11625907.5798&#xff0c; 且 0 出现次数比 1 少, 那么这个 01 串中 0 出现了多少次? #include<iostream> #include<cmath> using namespace std;int n 23333333;int main() {//枚举 0 出现的次数//因…...

什么是Ansible Jinja2

理解 Ansible Jinja2 模板 Ansible 是一款功能强大的开源自动化工具&#xff0c;可让您无缝地管理和配置系统。Ansible 的一大亮点是它使用 Jinja2 模板&#xff0c;允许您根据变量数据动态生成文件、配置设置和脚本。本文将向您介绍 Ansible 中的 Jinja2 模板&#xff0c;并通…...

.Net Framework 4/C# 关键字(非常用,持续更新...)

一、is 关键字 is 关键字用于检查对象是否于给定类型兼容,如果兼容将返回 true,如果不兼容则返回 false,在进行类型转换前,可以先使用 is 关键字判断对象是否与指定类型兼容,如果兼容才进行转换,这样的转换是安全的。 例如有:首先创建一个字符串对象,然后将字符串对象隐…...

优选算法第十二讲:队列 + 宽搜 优先级队列

优选算法第十二讲&#xff1a;队列 宽搜 && 优先级队列 1.N叉树的层序遍历2.二叉树的锯齿型层序遍历3.二叉树最大宽度4.在每个树行中找最大值5.优先级队列 -- 最后一块石头的重量6.数据流中的第K大元素7.前K个高频单词8.数据流的中位数 1.N叉树的层序遍历 2.二叉树的锯…...