C++中单例模式详解

article/2025/8/25 4:52:31

在C++中,单例模式 (Singleton Pattern) 确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。这在需要一个全局对象来协调整个系统行为的场景中非常有用。


 为什么要有单例模式?

在许多项目中,某些类从逻辑上讲只需要一个实例。例如:

  1. 全局配置管理器:应用程序的配置信息通常是全局唯一的。

  2. 日志记录器:整个应用程序通常共享一个日志记录器。

  3. 数据库连接池:管理数据库连接,通常只需要一个实例来控制连接数和分配。

  4. 硬件接口访问:如果一个类直接与某个硬件设备通信,通常也只需要一个实例来避免冲突。

  5. 线程池:管理一组工作线程,通常也是全局唯一的。

如果允许多个这样的实例存在,可能会导致:

  • 资源冲突多个实例可能争抢同一资源

  • 状态不一致:不同的实例可能持有不同的状态,导致行为不可预测。

  • 资源浪费:创建多个不必要的对象会占用内存和CPU资源。

单例模式通过限制类的实例化来解决这些问题。


单例模式的作用

单例模式主要有以下几个作用:

  1. 保证唯一实例:这是单例模式的核心。它确保在程序的整个生命周期中,特定类只有一个对象存在。

  2. 提供全局访问点:通过一个静态方法,如 GetInstance(),可以从程序的任何地方访问这个唯一的实例,而无需传递对象的引用。

  3. 延迟初始化:实例可以在第一次被请求时才创建,而不是在程序启动时就创建,这可以节省资源,特别是在实例创建开销较大或不一定会被使用时。

  4. 集中控制:将相关的资源和行为集中到一个对象中管理,方便协调和维护。 


 结合项目ClientCtrl的单例模式分析

在我最近在复盘的一个远程控制项目中,将客户端的某一个功能设计为单例模式是非常合适的。让我们看看为什么以及它是如何工作的。本项目的设计模式也是MVC模式,其中的C就是本节里的例子。

项目背景:

ClientCtrl 类作为控制层核心,负责:

  • 管理对话框实例 :WatchDialog, StatusDialog, RemoteDialog等,这些是我MVC模式中V里的一些功能。

  • 管理一个后台工作线程,处理自定义消息(发送数据、更新状态),这些是我MVC模式中M里的一些功能。

  • 提供初始化 (InitControl())、启动 (Invoke())、释放资源 (Release()) 等接口,这就是C的职责。

为什么ClientCtrl需要是单例?(可以结合如下所示的图去理解)

  1. 全局协调者ClientCtrl 扮演着应用程序中用户界面(对话框)和后台通信/逻辑处理(线程)之间的核心协调者角色。整个应用程序只需要一个这样的协调中心。如果存在多个 ClientCtrl 实例,就会出现:

    • 多个线程:每个 ClientCtrl 实例都会创建自己的线程,导致资源浪费和潜在的逻辑冲突。

    • 对话框管理混乱:哪个 ClientCtrl 实例应该管理哪些对话框?这会导致界面状态不一致。

    • 消息处理冲突:自定义消息应该由哪个实例的线程来处理?

  2. 资源集中管理

    • 对话框实例WatchDialogStatusDialogRemoteDialog 这些界面元素通常是全局唯一的,由一个统一的控制器来管理其生命周期和交互是合理的。

    • 线程句柄和ID (m_hThread, m_nThreadID): 这些资源与核心控制逻辑紧密相关,应由唯一的控制器实例拥有。

  3. 全局访问需求

    应用程序的不同部分可能需要与控制层交互,例如触发数据发送或更新界面状态。通过单例的 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;
}

关键点解释:

  1. static ClientCtrl* m_instance;: 这是一个静态成员指针,它将在类的所有对象之间共享。由于它是静态的,它在程序启动时(或第一次使用前,取决于具体实现和编译器)被初始化为 nullptr

  2. private ClientCtrl(): 构造函数是私有的。这意味着不能在类的外部使用 new ClientCtrl() 来创建对象。这是强制执行单例的关键。

  3. static ClientCtrl* GetInstance(): 这是全局访问点。

    • 它首先检查 m_instance 是否为 nullptr

    • 如果是,说明这是第一次请求实例,于是它 new ClientCtrl() 来创建唯一的实例,并将其地址赋给 m_instance

    • 如果不是 nullptr,说明实例已经存在,直接返回 m_instance

  4. 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))来确保线程安全。

  5. ClientCtrl* ClientCtrl::m_instance = nullptr;: 这是静态成员变量的定义和初始化,必须在类的外部(通常在 .cpp 文件中)进行。

通过这种方式,ClientCtrl 确保了在整个应用程序中,所有对控制层的操作都是通过同一个实例进行的,从而有效地管理了对话框、线程和消息处理,避免了多实例可能引发的冲突和混乱。

总结,单例模式可以确保全局访问控制层,避免多实例冲突,适合集中管理应用状态


http://www.hkcw.cn/article/BqowehXnTD.shtml

相关文章

什么是单片机?

众所周知&#xff0c;人类行为受大脑调控&#xff0c;正如视觉、听觉、味觉、嗅觉、触觉及运动功能等感官与肢体活动均受其指挥&#xff1b;换言之&#xff0c;大脑作为人体的中枢神经系统&#xff0c;负责管理所有可控制的生理功能。 在电子设备领域&#xff0c;单片机…

DMBOK对比知识点整理(4)

1.常见数据质量维度 常见数据质量维度(DMBOK-P353)质量维度

Web攻防-SQL注入增删改查盲注延时布尔报错有无回显错误处理

知识点&#xff1a; 1、Web攻防-SQL注入-操作方法&增删改查 2、Web攻防-SQL注入-布尔&延时&报错&盲注 案例说明&#xff1a; 在应用中&#xff0c;存在增删改查数据的操作&#xff0c;其中SQL语句结构不一导致注入语句也要针对应用达到兼容执行&#xff0c;另…

动态规划-152.乘积最大子数组-力扣(LeetCode)

一、题目解析 根据示例nums数组中存在负数&#xff0c;下面分析时需注意 二、算法原理 1、状态表示 此时f[i]表示&#xff1a;以i位置为结尾的所有子数组中的最大乘积&#xff0c;但是由于nums中存在负数&#xff0c;所以还需要g[i]表示&#xff1a;以i位置为结尾的所有子数组…

Leetcode 159. 至多包含两个不同字符的最长子串

1.题目基本信息 1.1.题目描述 给你一个字符串 s &#xff0c;请你找出 至多 包含 两个不同字符 的最长子串&#xff0c;并返回该子串的长度。 1.2.题目地址 https://leetcode.cn/problems/longest-substring-with-at-most-two-distinct-characters/description/ 2.解题方法…

MATLAB 横向剪切干涉系统用户界面设计及其波前重构研究

▒▒本文目录▒▒ 一、横向剪切干涉系统效果预览二、引言三、横向剪切干涉理论基础四、MATLAB 横向剪切干涉系统用户界面设计五、参考文献六、实验指导与matlab代码获取 一、横向剪切干涉系统效果预览 开发的系统如下所示&#xff1a; 横向剪切干涉系统 二、引言 横向剪切干…

C54-动态开辟内存空间

1.malloc 原型&#xff1a;void* malloc(size_t size);&#xff08;位于 <stdlib.h> 头文件中&#xff09; 作用&#xff1a;分配一块连续的、未初始化的内存块&#xff0c;大小为 size 字节。 返回值&#xff1a; 成功&#xff1a;返回指向分配内存首地址的 void* 指针…

【Linux网络篇】:初步理解应用层协议以及何为序列化和反序列化

✨感谢您阅读本篇文章&#xff0c;文章内容是个人学习笔记的整理&#xff0c;如果哪里有误的话还请您指正噢✨ ✨ 个人主页&#xff1a;余辉zmh–CSDN博客 ✨ 文章所属专栏&#xff1a;Linux篇–CSDN博客 文章目录 一.序列化和反序列化为什么需要序列化和反序列化为什么应用层…

【Tips】关于PCI和PCIe的配置空间差异和io/memory io读写

最近在看同事2023年讲的PCI基础课&#xff0c;感觉确实是豁然开朗了&#xff0c;赞美同事。 PCIe实际上是PCI的扩展&#xff08;extended&#xff09;&#xff0c;PCIe设备相当于是迭代升级产品。 而PCIe的配置空间基于PCI原有的0xFF&#xff08;256字节&#xff09;配置空间…

华为OD机试真题——阿里巴巴找黄金宝箱(III)(2025A卷:100分)Java/python/JavaScript/C/C++/GO最佳实现

2025 A卷 100分 题型 本专栏内全部题目均提供Java、python、JavaScript、C、C++、GO六种语言的最佳实现方式; 并且每种语言均涵盖详细的问题分析、解题思路、代码实现、代码详解、3个测试用例以及综合分析; 本文收录于专栏:《2025华为OD真题目录+全流程解析+备考攻略+经验分…

拓扑排序算法剖析与py/cpp/Java语言实现

拓扑排序算法深度剖析与py/cpp/Java语言实现 一、拓扑排序算法的基本概念1.1 有向无环图&#xff08;DAG&#xff09;1.2 拓扑排序的定义1.3 拓扑排序的性质 二、拓扑排序算法的原理与流程2.1 核心原理2.2 算法流程 三、拓扑排序算法的代码实现3.1 Python实现3.2 C实现3.3 Java…

C#学习:基于LLM的简历评估程序

前言 在pocketflow的例子中看到了一个基于LLM的简历评估程序的例子&#xff0c;感觉还挺好玩的&#xff0c;为了练习一下C#&#xff0c;我最近使用C#重写了一个。 准备不同的简历&#xff1a; 查看效果&#xff1a; 不足之处是现实的简历应该是pdf格式的&#xff0c;后面可以…

华为OD机试真题——阿里巴巴找黄金宝箱(II)(2025A卷:100分)Java/python/JavaScript/C/C++/GO最佳实现

2025 A卷 100分 题型 本专栏内全部题目均提供Java、python、JavaScript、C、C++、GO六种语言的最佳实现方式; 并且每种语言均涵盖详细的问题分析、解题思路、代码实现、代码详解、3个测试用例以及综合分析; 本文收录于专栏:《2025华为OD真题目录+全流程解析+备考攻略+经验分…

最大流-Ford-Fulkerson增广路径算法py/cpp/Java三语言实现

最大流-Ford-Fulkerson增广路径算法py/cpp/Java三语言实现 一、网络流问题与相关概念1.1 网络流问题定义1.2 关键概念 二、Ford-Fulkerson算法原理2.1 核心思想2.2 算法步骤 三、Ford-Fulkerson算法的代码实现3.1 Python实现3.2 C实现3.3 Java实现 四、Ford-Fulkerson算法的时间…

摄像头模块的镜头类型

一、‌按光学功能分类‌ ‌球面镜&#xff08;Spherical Lens&#xff09;‌ ‌特点‌&#xff1a;表面为球面曲率&#xff0c;工艺简单且成本低&#xff0c;但存在球面像差和色差&#xff0c;边缘画质易模糊。 ‌应用‌&#xff1a;低端监控设备、玩具相机等对画质要求低的…

汽车EPS系统的核心:驱动芯片的精准控制原理

随着科技的飞速发展&#xff0c;电机及其驱动技术在现代工业、汽车电子、家用电器等领域扮演着越来越重要的角色。有刷马达因其结构简单、成本低廉、维护方便等优点&#xff0c;在市场上占据了一定的份额。然而&#xff0c;为了充分发挥有刷马达的性能&#xff0c;一款高效能、…

51c视觉~3D~合集3

我自己的原文哦~ https://blog.51cto.com/whaosoft/13954440 #SceneTracker 在4D时空中追踪万物&#xff01;国防科大提出首个长时场景流估计方法 本篇分享 TPAMI 2025 论文​​SceneTracker: Long-term Scene Flow Estimation Network​​&#xff0c;国防科大提出首…

【25-cv-05855】Keith律所代理Paula Alejandra Navarro 版权图

Paula Alejandra Navarro 版权图 案件号&#xff1a;25-cv-05855 立案时间&#xff1a;2025年5月27日 原告&#xff1a;Paula Alejandra Navarro 代理律所&#xff1a;Keith 原告介绍 原告是来自巴拿马的自由职业艺术家&#xff0c;擅长将精灵、中世纪服饰等经典奇幻元素…

vue自定义穿梭框(内容体+多选框)

最近需要做一个资源分配的一个功能&#xff0c;然后用到了穿梭框&#xff0c;但是需要更多的功能控制。具体业务场景如下&#xff1a;需要同时可以分配查看和下载的权限。实现效果如下&#xff1a; 组件用的是&#xff1a; Ant Design Vue 的穿梭框 操作方式&#xff1a;在左…

各国竞争的下一代液晶技术:中国铁电液晶取得重大突破突破

一、全球下一代液晶技术发展格局 &#xff08;一&#xff09;韩国&#xff1a;OLED 技术持续领先&#xff0c;布局量子点与柔性显示 韩国作为显示产业强国&#xff0c;三星、LG 等企业在 OLED 领域占据全球主导地位。三星的 AMOLED 技术广泛应用于高端智能手机&#xff0c;其柔…