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

深入浅出设计模式——创建型模式之单例模式 Singleton

文章目录

  • “天上天下,唯我独尊”——单例模式
  • 单例模式简介
  • 单例模式结构
  • 饿汉式
  • 懒汉式
  • 客户端示例
    • 运行结果
  • 单例模式总结
  • 构建型模式 Creational Patterns 小结 Summary

代码仓库
在这里插入图片描述

“天上天下,唯我独尊”——单例模式

你能在电脑上调出两个Windows任务管理器吗?
假设能,如果两个管理器显示的数据相同,那何必要存在两个呢?
如果两个管理器显示的数据不同,那我该相信哪一个呢?

试试看,应该有且仅有一个吧?一个系统里有且仅有一个Windows任务管理器实例供外界访问 。如何保证系统里有且仅有一个实例对象呢?并且能够供外界访问?你可以在系统里定义一个统一的全局变量,但这并不能防止创建多个对象(想一想,为什么?)这就是单例模式的典型应用。

对于一个软件系统中的某些类来说,只有一个实例很重要。假设Windows系统上可以同时调出两个Windows任务管理器,这两个任务管理器显示的都是同样的信息,那势必造成内存资源的浪费;如果这两个任务管理器显示的是不同的信息,这也给用户带来了困惑,到底哪一个才是真实的状态?

单例模式简介

单例模式定义:
确保一个类只有一个实例,并提供一个全局访问点来访问这个唯一实例。

在这里插入图片描述

单例模式结构

单例模式结构非常简单,其UML图如下所示,只包含一个类,即单例类。为防止创建多个对象,其构造函数必须是私有的(外界不能访问)。另一方面,为了提供一个全局访问点来访问该唯一实例,单例类提供了一个公有方法getInstance来返回该实例。

在这里插入图片描述

饿汉式

饿汉式:变量在声明时便初始化。

// 饿汉式(立即加载)
// 饿汉式(Hungry Singleton):程序启动时立即创建对象
class Singleton_Hungry {
public:static Singleton_Hungry* getInstance() {std::cout << "\n[Hungry] 获取单例实例" << std::endl;static Singleton_Hungry instance; // 推荐方法,更安全更现代化return &instance;}void doSomething() const {std::cout << "\n[Hungry] 正在执行任务..." << std::endl;}private:// 禁止外界创建新的实例(私有构造、删除拷贝构造和拷贝赋值)。Singleton_Hungry() {std::cout << "\n[Hungry] 构造函数调用" << std::endl;}~Singleton_Hungry() {std::cout << "\n[Hungry] 析构函数调用" << std::endl;}Singleton_Hungry(const Singleton_Hungry&) = delete;Singleton_Hungry& operator=(const Singleton_Hungry&) = delete;// static Singleton_Hungry* instance;
};// 静态成员初始化 ⚠️头文件定义静态变量,错误!
// 根本原因:违反了单一定义规则(One Definition Rule, ODR)
// 你在头文件中定义了一个静态变量,头文件会被多个.cpp文件包含,这就导致在每个.cpp文件中都定义了一遍,最终会导致链接时的冲突,出现重复定义(multiple definition)错误。
// Singleton_Hungry* Singleton_Hungry::instance = new Singleton_Hungry();

可以看到,我们将构造方法定义为 private,这就保证了其他类无法实例化此类,必须通过 getInstance 方法才能获取到唯一的 instance 实例,非常直观。但饿汉式有一个弊端,那就是即使这个单例不需要使用,它也会在类加载之后立即创建出来,占用一块内存,并增加类初始化时间。就好比一个电工在修理灯泡时,先把所有工具拿出来,不管是不是所有的工具都用得上。就像一个饥不择食的饿汉,所以称之为饿汉式。

懒汉式

懒汉式:先声明一个空变量,需要用时才初始化。例如:

我们先声明了一个 instance 变量,当需要使用时判断此变量是否已被初始化,没有初始化的话才 new 一个实例出来。就好比电工在修理灯泡时,开始比较偷懒,什么工具都不拿,当发现需要使用螺丝刀时,才把螺丝刀拿出来。当需要用钳子时,再把钳子拿出来。就像一个不到万不得已不会行动的懒汉,所以称之为懒汉式

懒汉式解决了饿汉式的弊端,好处是按需加载,避免了内存浪费,减少了类初始化时间。

Singleton.h

// 确保一个类只有一个实例。
// 提供全局访问点,让用户方便访问// 懒汉式(线程安全,推荐写法)
// 懒汉式(Lazy Singleton):延迟创建对象(用时才创建)
class Singleton_Lazy {
public:// 第一次 调用时,执行内部的 lambda 表达式,创建一个新的单例对象。// 后续所有次 调用时,都直接跳过这个 lambda,不再执行创建对象的操作。static Singleton_Lazy* getInstance() {// template< class Callable, class... Args >// void call_once(std::once_flag& flag, Callable&& f, Args&&... args);// 确保给定的**可调用对象(如lambda表达式)**仅被调用一次。// 如果多个线程同时执行该行代码,也能确保只有一个线程实际执行了初始化操作,其他线程则会等待,直到第一次调用完成后再继续执行,且不会重复执行初始化操作。// flag:一个标志位,标识对应的初始化是否已经完成。// Callable:一个可调用对象(比如lambda表达式或函数),代表真正要执行的初始化动作。std::call_once(initFlag, []() {std::cout << "\n[Lazy] 创建新的实例" << std::endl;// instance = std::make_unique<Singleton_Lazy>(); // ✅ 更安全,更推荐instance.reset(new Singleton_Lazy());});std::cout << "\n[Lazy] 获取单例实例" << std::endl;return instance.get();}void doSomething() const {std::cout << "\n[Lazy] 正在执行任务..." << std::endl;}private:Singleton_Lazy() {std::cout << "\n[Lazy] 构造函数调用" << std::endl;}~Singleton_Lazy() {std::cout << "\n[Lazy] 析构函数调用" << std::endl;}// 拷贝构造函数(Copy Constructor)// 当使用一个已存在对象初始化另一个新对象时,调用拷贝构造函数Singleton_Lazy(const Singleton_Lazy&) = delete;// 拷贝赋值运算符(Copy Assignment Operator)// 当使用一个已存在的对象去给另一个已存在的对象赋值时调用。Singleton_Lazy& operator=(const Singleton_Lazy&) = delete;// ⚠️一定要保留这两个声明!static std::unique_ptr<Singleton_Lazy> instance;static std::once_flag initFlag;// 🔑加上这一句:// 允许 std::unique_ptr 调用私有析构函数friend class std::default_delete<Singleton_Lazy>;
};

Singleton.cpp

#include "Singleton.h"std::unique_ptr<Singleton_Lazy> Singleton_Lazy::instance = nullptr;
std::once_flag Singleton_Lazy::initFlag;

客户端示例

#define THREAD_NUM 6
#include <iostream>
#include <pthread.h>
#include <sys/syscall.h>
#include <unistd.h>
#include "Singleton.h"pid_t GetThreadId() {// syscall 是一个系统调用接口,可以让你直接调用操作系统提供的底层功能。// SYS_gettid 是 Linux 系统调用号,表示获取当前线程的线程ID(gettid)。// syscall(SYS_gettid) 实际上是执行 gettid() 系统调用的操作,返回当前线程的线程ID。// 该调用返回当前线程的线程ID,通常与 pthread_self() 的返回值相同,但是 gettid 是返回内核级线程ID,而 pthread_self() 返回的是 POSIX 线程库级别的线程ID// SYS_gettid 是一个常量,表示获取当前线程ID的系统调用号。// 每个系统调用都有一个唯一的编号(常量),用于标识该系统调用。SYS_gettid 对应的是获取线程ID的操作。return syscall(SYS_gettid);
}void* callSingleton_Lazy(void* arg) {int threadID = *(int*)arg;Singleton_Lazy *s = Singleton_Lazy::getInstance();printf("[Lazy] 线程编号: %d, 实例地址: %d\n", threadID, GetThreadId());// printf("[Hungry] 线程编号: %d, 实例地址: %p\n", threadID, s);return 0;
}void* callSingleton_Hungry(void* arg) {// 将arg 从 void* 类型的通用指针强制转换成 int*类型的指针, 然后对转换后的指针解引用,取出实际的整型数值(即线程编号)。int threadID = *(int*)arg;Singleton_Hungry *s = Singleton_Hungry::getInstance();printf("[Hungry] 线程编号: %d, 实例地址: %d\n", threadID, GetThreadId());// printf("[Hungry] 线程编号: %d, 实例地址: %p\n", threadID, s);return 0;
}int main() {pthread_t threads_pool[THREAD_NUM];int tids[THREAD_NUM], params[THREAD_NUM];for(int i = 0; i < THREAD_NUM; i++) {params[i] = i; // 独立参数,避免竞争/*int pthread_create(pthread_t *restrict thread,const pthread_attr_t *restrict attr,void *(*start_routine)(void *),void *restrict arg);*/// 前半部分线程调用懒汉式单例if(i < THREAD_NUM / 2)tids[i] = pthread_create(&threads_pool[i], NULL, callSingleton_Lazy, (void*)&params[i]);else // 后半部分线程调用饿汉式单例tids[i] = pthread_create(&threads_pool[i], NULL, callSingleton_Hungry, (void*)&params[i]);// On success, pthread_create() returns 0; on error, it returns an error number, and the contents of *thread are undefined.if(tids[i]) {printf("Error: unable to create thread.\n");exit(-1);}}for(int i = 0; i < THREAD_NUM; i++) {// On success, pthread_join() returns 0; on error, it returns an error numbertids[i] = pthread_join(threads_pool[i], NULL);if(tids[i]) {printf("Error: unable to join thread.\n");exit(-1);}}printf("main exiting.\n");return 0;
}

运行结果

在这里插入图片描述

单例模式总结

单例模式让一个类同时负责了『业务功能』和『自身的创建与生命周期管理』两个职责。
在这里插入图片描述
在这里插入图片描述

构建型模式 Creational Patterns 小结 Summary

在这里插入图片描述

之后我会持续更新,如果喜欢我的文章,请记得一键三连哦,点赞关注收藏,你的每一个赞每一份关注每一次收藏都将是我前进路上的无限动力 !!!↖(▔▽▔)↗感谢支持!

http://www.dtcms.com/a/306275.html

相关文章:

  • Docker离线安装依赖包地址
  • USRP捕获手机/路由器数据传输信号波形
  • Windows环境下安装Python和PyCharm
  • 【ip】IP地址能否直接填写255?
  • 【maven】仓库配置
  • 02 基于sklearn的机械学习-特征降维(特征选择、PCA)、KNN算法、模型选择与调优(交叉验证、朴素贝叶斯算法、拉普拉斯平滑)
  • 2507C++,介绍名字对象
  • Apache Ignite 集群状态(Cluster States)
  • fortigate的waf功能
  • openwrt中br-lan,eth0,eth0.1,eth0.2
  • day08(if-esle)
  • Linux 软件包管理
  • 【机器学习】机器学习新手入门概述
  • 基于C++的智能交通和智能公交流量预测与调度优化
  • 【物联网】基于树莓派的物联网开发【18】——树莓派安装Mosquitto服务
  • 【WPS】邮件合并教程\Excel批量写入数据进Word模板
  • 【0基础PS】PS工具详解--画笔工具
  • C++ 双缓冲(Double Buffering)实现无锁读取与高效数据更新
  • InfluxDB 与 Java 框架集成:Spring Boot 实战(二)
  • 用 Python 轻松实现时间序列预测:Darts 协变量 Covariates
  • 构建工具和脚手架:从源码到dist
  • uvm tlm preface
  • 若依前后端分离版学习笔记(四)——目录文件及主配置文件介绍
  • TP-Link Archer C50路由器曝安全漏洞,硬编码DES密钥可解密敏感配置
  • 用 Go Typed Client 快速上手 Elasticsearch —— 从建索引到聚合的完整实战
  • 基于 Hadoop 生态圈的数据仓库实践 —— OLAP 与数据可视化(一)
  • vscode配置rust环境
  • CVAE 回顾版
  • 工作笔记-----存储器类型相关知识
  • BCD (Binary-Coded Decimal) 指令介绍