C++中单例模式详解

news2025/6/7 15:20:18

在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 methods
        if (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.coloradmin.cn/o/2401225.html

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

相关文章

舆情监控系统爬虫技术解析

之前我已经详细解释过爬虫在系统中的角色和技术要点&#xff0c;这次需要更聚焦“如何实现”这个动作。 我注意到上次回复偏重架构设计&#xff0c;这次应该拆解为更具体的操作步骤&#xff1a;从目标定义到数据落地的完整流水线。尤其要强调动态调度这个容易被忽视的环节——…

Vue3中Ant-design-vue的使用-附完整代码

前言 首先介绍一下什么是Ant-design-vue Ant Design Vue 是基于 Vue 3 的企业级 UI 组件库&#xff08;同时兼容 Vue 2&#xff09;&#xff0c;是蚂蚁金服开源项目 Ant Design 的 Vue 实现版本。它遵循 Ant Design 的设计规范&#xff0c;提供丰富的组件和高质量的设计体系&…

Redis Sorted Set 深度解析:从原理到实战应用

Redis Sorted Set 深度解析&#xff1a;从原理到实战应用 在 Redis 丰富的数据结构家族中&#xff0c;Sorted Set&#xff08;有序集合&#xff09;凭借独特的设计和强大的功能&#xff0c;成为处理有序数据场景的得力工具。无论是构建实时排行榜&#xff0c;还是实现基于时间的…

若依框架修改模板,添加通过excel导入数据功能

版本&#xff1a;我后端使用的是RuoYi-Vue-fast版本&#xff0c;前端是RuoYi-Vue3 需求: 我需要每个侧边栏功能都需要具有导入excel功能&#xff0c;但是若依只有用户才具备&#xff0c;我需要代码生成的每个功能都拥有导入功能。​ 每次生成一个一个改实在是太麻烦了。索性…

web全栈开发学习-01html基础

背景 最近在付费网站学习web全栈开发&#xff0c;记录一下阶段性学习。今天刚好学完html基础&#xff0c;跟着教程画了个基础的网站。 样品展示: 开发工具 vscode Visual Studio Code - Code Editing. Redefined 常用插件 Prettier&#xff1a;格式优化 Live Sever:实时调…

多线程环境中,如果多个线程同时尝试向同一个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+源码】基于微信小程序的旅游论坛系统的设计与实现

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

贝叶斯优化+LSTM+时序预测=Nature子刊!

贝叶斯优化与LSTM的融合在时间序列预测领域取得了显著成效&#xff0c;特别是在处理那些涉及众多超参数调整的复杂问题时。 1.这种结合不仅极大提高了预测的精确度&#xff0c;还优化了模型训练流程&#xff0c;提升了效率和成本效益。超参数优化的新篇章&#xff1a;LSTM因其…

Vue3(ref与reactive)

一&#xff0c;ref创建_基本类型的响应式数据 在 Vue 3 中&#xff0c;ref是创建响应式数据的核心 API 之一 ** ref的基本概念** ref用于创建一个可变的响应式数据引用&#xff0c;适用于任何类型的值&#xff08;基本类型、对象、数组等&#xff09;。通过ref包装的值会被转…

Starrocks中RoaringBitmap杂谈

背景 最近在阅读Starrocks源码的时候&#xff0c;遇到ColumnRefSet的RoaringBitmap使用&#xff0c;所以借此来讨论一下RoaringBitmap这个数据结构,这种思想是很值得借鉴的。 对于的实现可以参考一下 <dependency><groupId>org.roaringbitmap</groupId><…

涂胶协作机器人解决方案 | Kinova Link 6 Cobot在涂胶工业的方案应用与价值

涂胶工业现状背景&#xff1a; 涂胶工艺在汽车制造、电子组装、航空航天等工业领域极为关键&#xff0c;关乎产品密封、防水、绝缘性能及外观质量。 然而&#xff0c;传统涂胶作业问题频发。人工操作重复性强易疲劳&#xff0c;涂胶质量波动大&#xff1b;大型涂胶器使用增加工…

新手小白使用VMware创建虚拟机安装Linux

新手小白想要练习linux&#xff0c;找不到合适的地方&#xff0c;可以先创建一个虚拟机&#xff0c;在自己创建的虚拟机里面进行练习&#xff0c;接下来我给大家接受一下创建虚拟机的步骤。 VMware选择创建新的虚拟机 选择自定义 硬件兼容性选择第一个&#xff0c;不同的版本&a…

EscapeX:去中心化游戏,开启极限娱乐新体验

VEX 平台推出全新去中心化游戏 EscapeX&#xff08;数字逃脫&#xff09;&#xff0c;创新性地将大逃杀玩法与区块链技术相融合。用户不仅能畅享紧张刺激的解谜过程&#xff0c;更能在去中心化、公正透明的环境中参与游戏。EscapeX 的上线&#xff0c;为 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设计 三、…

JavaWeb:前端工程化-TS(TypeScript)

概述 快速入门 常用类型 基础类型 联合类型 函数类型 对象类型 接口Interface Interface和type区别 典型推论

unity+ spine切换武器不换皮肤解决方案

1.在spine编辑中获取到角色武器插槽名称 这里的武器插槽名称为“zj_22”。角色的spine正常导出到unity中。 2.将需要替换的武器图片单独放在一个spine项目里面&#xff0c;并为每个武器单独建立一个插槽。 而且全部放在根骨骼Root下。 3.将武器的spine动画导出&#xff0c;会…

[java八股文][MySQL面试篇]SQL基础

NOSQL和SQL的区别&#xff1f; SQL数据库&#xff0c;指关系型数据库 - 主要代表&#xff1a;SQL Server&#xff0c;Oracle&#xff0c;MySQL(开源)&#xff0c;PostgreSQL(开源)。 关系型数据库存储结构化数据。这些数据逻辑上以行列二维表的形式存在&#xff0c;每一列代表…

【AI论文】SWE-rebench:一个用于软件工程代理的任务收集和净化评估的自动化管道

摘要&#xff1a;基于LLM的代理在越来越多的软件工程&#xff08;SWE&#xff09;任务中显示出有前景的能力。 然而&#xff0c;推进这一领域面临着两个关键挑战。 首先&#xff0c;高质量的训练数据稀缺&#xff0c;尤其是反映现实世界软件工程场景的数据&#xff0c;在这些场…

Flask文件处理全攻略:安全上传下载与异常处理实战

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐&#xff1a;「storms…

【算法深练】分组循环:“分”出条理,化繁为简

目录 引言 分组循环 2760. 最长奇偶子数组 1446. 连续字符 1869. 哪种连续子字符串更长 2414. 最长的字母序连续子字符串的长度 3456. 找出长度为 K 的特殊子字符串 1957. 删除字符使字符串变好 674. 最长连续递增序列 978. 最长湍流子数组 2110. 股票平滑下跌阶段的…