C++八股 —— 单例模式

news2025/6/12 8:02:44

文章目录

  • 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:
    // 要点2
	static Singleton1 * GetInstance() {
		if(_instance == nullptr) {
			_instance = new Singleton1();
		}
		return _instance;
	}
private:
    // 要点1
	Singleton1() {}
	~Singleton1() {
		std::cout << "~Singleton1()\n";
	}
    // 要点3
	Singleton1(const Singleton1 &) = delete;
	Singleton1& operator = (const Singleton1&) = delete;
	Singleton1(Singleton1 &&) = delete;
	Singleton1& operator = (Singleton1 &&) = delete;
	// 要点2
	static 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是因为需要其对子类时可见的
  • 友元是为了让基类能访问子类的构造析构函数

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

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

相关文章

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数据访问模块设计

前面我们已经完成了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;还有的人是独立思考做了一遍就等于刷了。…

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

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

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…

【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…

【开发技术】.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;提供时间序列预测和文本生成…

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

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

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

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

蓝桥杯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;并通…

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

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

项目部署到Linux上时遇到的错误(Redis,MySQL,无法正确连接,地址占用问题)

Redis无法正确连接 在运行jar包时出现了这样的错误 查询得知问题核心在于Redis连接失败&#xff0c;具体原因是客户端发送了密码认证请求&#xff0c;但Redis服务器未设置密码 1.为Redis设置密码&#xff08;匹配客户端配置&#xff09; 步骤&#xff1a; 1&#xff09;.修…

智能分布式爬虫的数据处理流水线优化:基于深度强化学习的数据质量控制

在数字化浪潮席卷全球的今天&#xff0c;数据已成为企业和研究机构的核心资产。智能分布式爬虫作为高效的数据采集工具&#xff0c;在大规模数据获取中发挥着关键作用。然而&#xff0c;传统的数据处理流水线在面对复杂多变的网络环境和海量异构数据时&#xff0c;常出现数据质…

OPenCV CUDA模块图像处理-----对图像执行 均值漂移滤波(Mean Shift Filtering)函数meanShiftFiltering()

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 在 GPU 上对图像执行 均值漂移滤波&#xff08;Mean Shift Filtering&#xff09;&#xff0c;用于图像分割或平滑处理。 该函数将输入图像中的…