C++中单例模式详解
在C++中,单例模式 (Singleton Pattern) 确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。这在需要一个全局对象来协调整个系统行为的场景中非常有用。
为什么要有单例模式?
在许多项目中,某些类从逻辑上讲只需要一个实例。例如:
-
全局配置管理器:应用程序的配置信息通常是全局唯一的。
-
日志记录器:整个应用程序通常共享一个日志记录器。
-
数据库连接池:管理数据库连接,通常只需要一个实例来控制连接数和分配。
-
硬件接口访问:如果一个类直接与某个硬件设备通信,通常也只需要一个实例来避免冲突。
-
线程池:管理一组工作线程,通常也是全局唯一的。
如果允许多个这样的实例存在,可能会导致:
-
资源冲突:多个实例可能争抢同一资源。
-
状态不一致:不同的实例可能持有不同的状态,导致行为不可预测。
-
资源浪费:创建多个不必要的对象会占用内存和CPU资源。
单例模式通过限制类的实例化来解决这些问题。
单例模式的作用
单例模式主要有以下几个作用:
-
保证唯一实例:这是单例模式的核心。它确保在程序的整个生命周期中,特定类只有一个对象存在。
-
提供全局访问点:通过一个静态方法,如
GetInstance()
,可以从程序的任何地方访问这个唯一的实例,而无需传递对象的引用。 -
延迟初始化:实例可以在第一次被请求时才创建,而不是在程序启动时就创建,这可以节省资源,特别是在实例创建开销较大或不一定会被使用时。
-
集中控制:将相关的资源和行为集中到一个对象中管理,方便协调和维护。
结合项目ClientCtrl
的单例模式分析
在我最近在复盘的一个远程控制项目中,将客户端的某一个功能设计为单例模式是非常合适的。让我们看看为什么以及它是如何工作的。本项目的设计模式也是MVC模式,其中的C就是本节里的例子。
项目背景:
ClientCtrl
类作为控制层核心,负责:
-
管理对话框实例 :
WatchDialog
,StatusDialog
,RemoteDialog
等,这些是我MVC模式中V里的一些功能。 -
管理一个后台工作线程,处理自定义消息(发送数据、更新状态),这些是我MVC模式中M里的一些功能。
-
提供初始化 (
InitControl()
)、启动 (Invoke()
)、释放资源 (Release()
) 等接口,这就是C的职责。
为什么ClientCtrl
需要是单例?(可以结合如下所示的图去理解)
-
全局协调者:
ClientCtrl
扮演着应用程序中用户界面(对话框)和后台通信/逻辑处理(线程)之间的核心协调者角色。整个应用程序只需要一个这样的协调中心。如果存在多个ClientCtrl
实例,就会出现:-
多个线程:每个
ClientCtrl
实例都会创建自己的线程,导致资源浪费和潜在的逻辑冲突。 -
对话框管理混乱:哪个
ClientCtrl
实例应该管理哪些对话框?这会导致界面状态不一致。 -
消息处理冲突:自定义消息应该由哪个实例的线程来处理?
-
-
资源集中管理:
-
对话框实例:
WatchDialog
、StatusDialog
、RemoteDialog
这些界面元素通常是全局唯一的,由一个统一的控制器来管理其生命周期和交互是合理的。 -
线程句柄和ID (
m_hThread
,m_nThreadID
): 这些资源与核心控制逻辑紧密相关,应由唯一的控制器实例拥有。
-
-
全局访问需求:
应用程序的不同部分可能需要与控制层交互,例如触发数据发送或更新界面状态。通过单例的
GetInstance()
方法,任何模块都可以方便地获取到ClientCtrl
的唯一实例并调用其公有方法。这避免了在各个模块之间传递ClientCtrl
对象引用的复杂性。
ClientCtrl
单例模式实现分析
class ClientCtrl {
private:// 1. 静态私有成员变量,用于保存类的唯一实例static ClientCtrl* m_instance;// 2. 私有构造函数,防止外部直接通过 new 创建实例ClientCtrl() {// 构造函数中创建线程// 例如: m_hThread = (HANDLE)_beginthreadex(nullptr, 0, &ClientCtrl::ThreadEntry, this, 0, &m_nThreadID);// 初始化对话框实例等// m_pWatchDialog = new WatchDialog();// ...// 注册消息映射// RegisterMsgHandler(WM_USER + 1, &ClientCtrl::HandleSendData);// ...std::cout << "ClientCtrl instance created. Thread started." << std::endl;}// 3. 私有析构函数,确保实例只能通过定义的 Release 方法释放 (如果需要的话)// 或者在程序结束时由操作系统自动回收(如果m_instance是静态对象而非指针)// 在这个例子中,通过 Release() 主动释放。~ClientCtrl() {// 析构函数中等待线程结束,释放资源if (m_hThread != INVALID_HANDLE_VALUE) {// PostThreadMessage(m_nThreadID, WM_QUIT, 0, 0); // 通知线程退出消息循环// WaitForSingleObject(m_hThread, INFINITE);// CloseHandle(m_hThread);// m_hThread = INVALID_HANDLE_VALUE;}// delete m_pWatchDialog;// ...std::cout << "ClientCtrl instance destroyed. Resources released." << std::endl;}// (可选) 私有拷贝构造函数和赋值运算符,防止复制实例ClientCtrl(const ClientCtrl&) = delete;ClientCtrl& operator=(const ClientCtrl&) = delete;public:// 4. 静态公有方法,用于获取类的唯一实例static ClientCtrl* GetInstance() {if (m_instance == nullptr) { // 第一次调用时创建实例 (延迟初始化)m_instance = new ClientCtrl();}return m_instance;}// 5. (可选) 静态公有方法,用于显式释放单例实例// 在某些情况下需要手动控制单例的销毁时机static void ReleaseInstance() { // Renamed from 'Release' to avoid conflict if ClientCtrl has other Release methodsif (m_instance != nullptr) {delete m_instance;m_instance = nullptr;}}// 公有成员方法void InitControl() { std::cout << "ClientCtrl InitControl called." << std::endl; /* ... */ }void Invoke() { std::cout << "ClientCtrl Invoke called." << std::endl; /* ... */ }void ReleaseResources() { std::cout << "ClientCtrl Release (logic) called." << std::endl; /* ... */ } // Renamed for clarity// 线程入口函数 (必须是静态的,或者使用lambda捕获this)static unsigned __stdcall ThreadEntry(void* pParam) {// ClientCtrl* pThis = static_cast<ClientCtrl*>(pParam);// MSG msg;// PeekMessage(&msg, NULL, WM_USER, WM_USER, PM_NOREMOVE); // 创建消息队列// while (GetMessage(&msg, nullptr, 0, 0)) {// if (msg.message == WM_QUIT) {// break;// }// // 通过映射表查找并调用处理函数// // pThis->DispatchMsg(&msg);// TranslateMessage(&msg);// DispatchMessage(&msg);// }std::cout << "ClientCtrl thread exiting." << std::endl;return 0;}private:// WatchDialog* m_pWatchDialog;// StatusDialog* m_pStatusDialog;// RemoteDialog* m_pRemoteDialog;// HANDLE m_hThread = INVALID_HANDLE_VALUE;// unsigned m_nThreadID = 0;// std::map<UINT, std::function<void(WPARAM, LPARAM)>> m_messageMap;// void RegisterMsgHandler(UINT msg, std::function<void(WPARAM, LPARAM)> handler) {// m_messageMap[msg] = handler;// }// void DispatchMsg(MSG* pMsg) {// auto it = m_messageMap.find(pMsg->message);// if (it != m_messageMap.end()) {// it->second(pMsg->wParam, pMsg->lParam);// } else {// // Default handling or log unknown message// }// }// void HandleSendData(WPARAM wParam, LPARAM lParam) { /* ... */ }// void HandleUpdateStatus(WPARAM wParam, LPARAM lParam) { /* ... */ }// void HandleRemoteMonitor(WPARAM wParam, LPARAM lParam) { /* ... */ }
};// 6. 在类外初始化静态成员变量
ClientCtrl* ClientCtrl::m_instance = nullptr;
使用示例:
// 在 App 类或其他任何地方使用 ClientCtrl
// #include "ClientCtrl.h" // 假设在头文件中int main() {// 获取 ClientCtrl 的唯一实例ClientCtrl* controller1 = ClientCtrl::GetInstance();controller1->InitControl();controller1->Invoke();ClientCtrl* controller2 = ClientCtrl::GetInstance(); // controller2 与 controller1 指向同一个实例if (controller1 == controller2) {std::cout << "controller1 and controller2 are the same instance." << std::endl;}// 模拟发送消息 (实际应在线程中处理)// PostThreadMessage(controller1->GetThreadId(), WM_USER + 1, 0, 0);controller1->ReleaseResources(); // 调用业务逻辑上的释放// 程序结束前释放单例资源ClientCtrl::ReleaseInstance();std::cout << "ClientCtrl instance explicitly released." << std::endl;return 0;
}
关键点解释:
-
static ClientCtrl* m_instance;
: 这是一个静态成员指针,它将在类的所有对象之间共享。由于它是静态的,它在程序启动时(或第一次使用前,取决于具体实现和编译器)被初始化为nullptr
。 -
private ClientCtrl()
: 构造函数是私有的。这意味着不能在类的外部使用new ClientCtrl()
来创建对象。这是强制执行单例的关键。 -
static ClientCtrl* GetInstance()
: 这是全局访问点。-
它首先检查
m_instance
是否为nullptr
。 -
如果是,说明这是第一次请求实例,于是它
new ClientCtrl()
来创建唯一的实例,并将其地址赋给m_instance
。 -
如果不是
nullptr
,说明实例已经存在,直接返回m_instance
。
-
-
static void ReleaseInstance()
: 提供了一个释放单例占用的内存的方法。这在程序退出前或者明确不再需要该单例时调用,以避免内存泄漏。注意:在多线程环境中,GetInstance()
中的if (m_instance == nullptr)
判断和m_instance = new ClientCtrl();
的赋值操作之间存在线程安全问题(竞态条件)。如果多个线程同时调用GetInstance()
并且m_instance
恰好是nullptr
,可能会创建多个实例。更健壮的实现通常需要使用互斥锁 (mutex) 或其他同步机制(如C++11的std::call_once
或双重检查锁定模式 (Double-Checked Locking Pattern))来确保线程安全。 -
ClientCtrl* ClientCtrl::m_instance = nullptr;
: 这是静态成员变量的定义和初始化,必须在类的外部(通常在 .cpp 文件中)进行。
通过这种方式,ClientCtrl
确保了在整个应用程序中,所有对控制层的操作都是通过同一个实例进行的,从而有效地管理了对话框、线程和消息处理,避免了多实例可能引发的冲突和混乱。
总结,单例模式可以确保全局访问控制层,避免多实例冲突,适合集中管理应用状态。
相关文章:

C++中单例模式详解
在C中,单例模式 (Singleton Pattern) 确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。这在需要一个全局对象来协调整个系统行为的场景中非常有用。 为什么要有单例模式? 在许多项目中,某些类从逻辑上讲只需要一个实…...

舆情监控系统爬虫技术解析
之前我已经详细解释过爬虫在系统中的角色和技术要点,这次需要更聚焦“如何实现”这个动作。 我注意到上次回复偏重架构设计,这次应该拆解为更具体的操作步骤:从目标定义到数据落地的完整流水线。尤其要强调动态调度这个容易被忽视的环节——…...
Windows上用FFmpeg采集摄像头推流 → MediaMTX服务器转发流 → WSL2上拉流播放
1. Windows上 FFmpeg 推流(摄像头采集) 设备名称可用 ffmpeg -list_devices true -f dshow -i dummy 查询,假设为Integrated Camera 采集推流示例(推RTMP到MediaMTX): ffmpeg -rtbufsize 100M -f dshow …...
cpp多线程学习
1.thread std::thread是 C11 引入的跨平台线程管理类,封装了操作系统的线程 API(如 pthread、Windows 线程),提供统一的线程操作接口。线程的生命周期由join()和detach()控制。 thread在创建时就开始执行 join():阻…...

Vue3中Ant-design-vue的使用-附完整代码
前言 首先介绍一下什么是Ant-design-vue Ant Design Vue 是基于 Vue 3 的企业级 UI 组件库(同时兼容 Vue 2),是蚂蚁金服开源项目 Ant Design 的 Vue 实现版本。它遵循 Ant Design 的设计规范,提供丰富的组件和高质量的设计体系&…...
k8s热更新-subPath 不支持热更新
文章目录 k8s热更新-subPath 不支持热更新背景subPath 不支持热更新1. 为什么 subPath 不支持热更新?2. 挂载整个目录为何支持热更新?使用demo举例:挂载整个目录(不使用 subPath) k8s热更新-subPath 不支持热更新 背景…...

Redis Sorted Set 深度解析:从原理到实战应用
Redis Sorted Set 深度解析:从原理到实战应用 在 Redis 丰富的数据结构家族中,Sorted Set(有序集合)凭借独特的设计和强大的功能,成为处理有序数据场景的得力工具。无论是构建实时排行榜,还是实现基于时间的…...
docker中组合这几个命令来排查 import 模块失败 的问题
pwd ls echo $PYTHONPATH这三个命令是你在 Linux 或 Docker 容器中常用来「查看环境状态」的基础命令。 ✅ 1. echo $PYTHONPATH 🔍 含义 这是在查看当前的 Python 模块搜索路径。 🧠 分解解释: echo:打印某个变量的值&#x…...

若依框架修改模板,添加通过excel导入数据功能
版本:我后端使用的是RuoYi-Vue-fast版本,前端是RuoYi-Vue3 需求: 我需要每个侧边栏功能都需要具有导入excel功能,但是若依只有用户才具备,我需要代码生成的每个功能都拥有导入功能。 每次生成一个一个改实在是太麻烦了。索性…...

web全栈开发学习-01html基础
背景 最近在付费网站学习web全栈开发,记录一下阶段性学习。今天刚好学完html基础,跟着教程画了个基础的网站。 样品展示: 开发工具 vscode Visual Studio Code - Code Editing. Redefined 常用插件 Prettier:格式优化 Live Sever:实时调…...
基于Socketserver+ThreadPoolExecutor+Thread构造的TCP网络实时通信程序
目录 介绍: 源代码: Socketserver-服务端代码 Socketserver客户端代码: 介绍: socketserver是一种传统的传输层网络编程接口,相比WebSocket这种应用层的协议来说,socketserver比较底层,soc…...
[Java 基础]枚举
枚举是一种特殊的类,表示一组固定的常量。枚举跟普通类一样可以用自己的变量、方法和构造函数,构造函数只能使用 private 访问修饰符,所以外部无法调用。 现实生活中的例子: 一周七天(MONDAY ~ SUNDAY) …...

多线程环境中,如果多个线程同时尝试向同一个TCP客户端发送数据,添加同步机制
原代码 public async Task SendToClientAsync(TcpClient targetClient, byte[] data, int offset, int length) {try{// 1. 检查客户端是否有效if (targetClient null || !targetClient.Connected){Console.WriteLine("Cannot send: client is not connected");ret…...

【含文档+PPT+源码】基于微信小程序的旅游论坛系统的设计与实现
项目介绍 本课程演示的是一款基于微信小程序的旅游论坛系统的设计与实现,主要针对计算机相关专业的正在做毕设的学生与需要项目实战练习的 Java 学习者。 1.包含:项目源码、项目文档、数据库脚本、软件工具等所有资料 2.带你从零开始部署运行本套系统 …...

贝叶斯优化+LSTM+时序预测=Nature子刊!
贝叶斯优化与LSTM的融合在时间序列预测领域取得了显著成效,特别是在处理那些涉及众多超参数调整的复杂问题时。 1.这种结合不仅极大提高了预测的精确度,还优化了模型训练流程,提升了效率和成本效益。超参数优化的新篇章:LSTM因其…...
NodeJS全栈WEB3面试题——P3Web3.js / Ethers.js 使用
3.1 Ethers.js 和 Web3.js 的主要区别是什么? 比较点Ethers.jsWeb3.js体积更轻量,适合前端较大,加载慢,适合 Node文档文档简洁、现代化,支持 TypeScript文档丰富,但不够现代化模块化设计高度模块化&#x…...
Quick UI 组件加载到 Axure
将 Quick UI 组件加载到 Axure 的完整指南 Axure 支持通过自定义元件库加载外部 UI 组件库(如 Quick UI),以下是详细的操作流程: 一、准备工作 获取 Quick UI 组件库文件: 下载 .rplib 格式的 Quick UI 元件库文件&a…...

Vue3(ref与reactive)
一,ref创建_基本类型的响应式数据 在 Vue 3 中,ref是创建响应式数据的核心 API 之一 ** ref的基本概念** ref用于创建一个可变的响应式数据引用,适用于任何类型的值(基本类型、对象、数组等)。通过ref包装的值会被转…...

Starrocks中RoaringBitmap杂谈
背景 最近在阅读Starrocks源码的时候,遇到ColumnRefSet的RoaringBitmap使用,所以借此来讨论一下RoaringBitmap这个数据结构,这种思想是很值得借鉴的。 对于的实现可以参考一下 <dependency><groupId>org.roaringbitmap</groupId><…...
通过ca证书的方式设置允许远程访问Docker服务
设置允许远程访问Docker服务 使用场景 环境 系统:anolis7.9 修改Docker服务配置,配置安全证书 生成ca证书到/etc/docker目录中,后续会要用到 #该步骤需要设置密码,后面步骤会要用到,此处设置密码为123456 openss…...

涂胶协作机器人解决方案 | Kinova Link 6 Cobot在涂胶工业的方案应用与价值
涂胶工业现状背景: 涂胶工艺在汽车制造、电子组装、航空航天等工业领域极为关键,关乎产品密封、防水、绝缘性能及外观质量。 然而,传统涂胶作业问题频发。人工操作重复性强易疲劳,涂胶质量波动大;大型涂胶器使用增加工…...
理解继承与组合的本质:Qt 项目中的设计选择指南
文章目录 理解继承与组合的本质:Qt 项目中的设计选择指南一、继承与组合的本质区别1. 继承(Inheritance)2. 组合(Composition) 二、继承的适用场景三、组合的适用场景四、错误使用继承的后果五、判断继承或组合的三问法…...

新手小白使用VMware创建虚拟机安装Linux
新手小白想要练习linux,找不到合适的地方,可以先创建一个虚拟机,在自己创建的虚拟机里面进行练习,接下来我给大家接受一下创建虚拟机的步骤。 VMware选择创建新的虚拟机 选择自定义 硬件兼容性选择第一个,不同的版本&a…...
使用 PHP 和 Guzzle 对接印度股票数据源API
对接 StockTV API 可能涉及获取实时或历史的金融市场数据,如股票价格、交易量、市场新闻等。为了帮助你更好地理解如何使用 PHP 对接 StockTV API,下面我将提供一个通用指南和示例代码。 前提条件 注册并获取API密钥:首先你需要在 StockTV …...

EscapeX:去中心化游戏,开启极限娱乐新体验
VEX 平台推出全新去中心化游戏 EscapeX(数字逃脫),创新性地将大逃杀玩法与区块链技术相融合。用户不仅能畅享紧张刺激的解谜过程,更能在去中心化、公正透明的环境中参与游戏。EscapeX 的上线,为 VEX 生态注入全新活力&…...

使用PyQt5的图形用户界面(GUI)开发教程
文章目录 写在前面一、PyQt5的安装1.1 使用Conda管理环境1.1.1 新建环境1.1.2 conda list和pip list的区别1.1.3 conda install和pip install的区别 1.2 安装PyQt5和Qt Designer1.3 VsCode中配置Qt Designer 二、PyQt5的UI设计2.1 .ui文件设计2.2 .qrc文件建立2.3 qss设计 三、…...
STM32实战:智能环境监测站设计方案
下面是一个基于STM32的智能环境监测站设计方案,使用Keil MDK-ARM开发环境。这个系统集成了多种传感器,并通过OLED显示数据,同时具备数据存储和报警功能。 [STM32F4系列MCU] ├── I2C总线 │ ├── SHT30温湿度传感器 │ ├──…...
猎板硬金镀层厚度:新能源汽车高压系统的可靠性基石
在新能源汽车的电池管理系统(BMS)和电机控制器中,硬金镀层厚度直接关系到高压环境下的电气稳定性与使用寿命。猎板针对车载场景开发的耐电迁移方案(金层 2.5μm,镍层 8μm),经 150℃/85% RH 高压…...
KEYSIGHT是德科技 E5063A 18G ENA系列网络分析仪
KEYSIGHT是德科技 E5063A 18G ENA系列网络分析仪 E5063A ENA 矢量网络分析仪 18GHz 2端口 降低无源射频元器件的测试成本 Keysight E5063A ENA 是一款经济适用的台式矢量网络分析仪,可用于测试简单的无源元器件,例如频率最高达到 18 GHz 的天线、滤…...
VR 虚拟仿真工器具:开启医学新视界的智慧钥匙
VR 虚拟仿真工器具在医疗领域的应用,为医疗行业的发展带来了新的机遇。在手术模拟训练中,它让医生提前熟悉手术流程和操作技巧。对于一些复杂的手术,如心脏搭桥手术、神经外科手术等,手术难度大、风险高,对医生的操作技…...