实用指南:精读C++20设计模式——创造型设计模式:单例模式

news/2025/10/13 12:16:12/文章来源:https://www.cnblogs.com/yxysuanfa/p/19138180

精读C++20设计模式——创造型设计模式:单例模式

​ 我当时读到这里的时候更多的是惊讶,因为作者并不喜欢单例模式。当然单例模式的确存在它的意义。在很多场景下,如果我们期待全局程序总是访问唯一对象的情况下,我们才会去使用单例模式。比如说——全局唯一的数据库,全局唯一的日志对象。这个时候,单例模式就会显得非常的有用。老样子,我们不立马上代码,而是一步一步从头开始,思考着如何编写更好的符合单例模式范式的代码。

所以,从最基本的开始

​ 单例模式比较奇怪的是——最开始的时候,我相信很多朋友第一次出现遇到单例构造的场景的解决方案,下意识的是写注释(笔者没有略去书中提到的,是因为我真的看到过有人是这样写注释的,还特别的打了很多感叹号)

struct GlobalOLED {
/*
You should only invoke the creation for once
*/
GlobalOLED();
};

​ 但是问题在于,我们假设人人在开发的时候都会阅读文档,但是麻烦的点在于C++是一个很喜欢隐式操作的语言,我们人可以看懂,但是在传播的时候,我们很容易触发拷贝构造/赋值,或者是利用RAII的时候很容易自动的触发。这是我们不希望的。

​ 有一种办法,**我想我们的读者朋友显然已经想到:那就是将我们的任何破坏了单例特性(或者说全局唯一特性)的拷贝或者移动的构造/赋值禁用掉。**很是合理!

struct GlobalOLED {
public:
// ...
private:
/*
You should only invoke the creation for once
*/
GlobalOLED();
GlobalOLED& operator=(const GlobalOLED&) = delete;
GlobalOLED(const GlobalOLED&) = delete;
GlobalOLED& operator=(GlobalOLED&&) = delete;
GlobalOLED(GlobalOLED&&) = delete;
};

​ 这个时候,我们自然会注意到,这好像也不太对,因为这个时候,我们没办法创建这个对象了:构造函数被放到了私有域中。那我们就必须要求存在一个接口访问到这个对象,这样,问题就被缩小到这个场景:在构造上下文中,保证我们的对象总是唯一的。其他所有外置使用咱们要走请求的接口。

​ 构造上下文的唯一性,一个在C++11之后的方式就是直接static修饰变量,他就会保证咱们的初始化只有一次。非常的安全而且简单。

struct GlobalOLED {
public:
static GlobalOLED&	get_instance() {
static GlobalOLED oled;	// static construct the oled
return oled;
}
private:
/*
You should only invoke the creation for once
*/
GlobalOLED();
GlobalOLED& operator=(const GlobalOLED&) = delete;
GlobalOLED(const GlobalOLED&) = delete;
GlobalOLED& operator=(GlobalOLED&&) = delete;
GlobalOLED(GlobalOLED&&) = delete;
};

​ 当然还有类似std::call_once啊,双重检查的办法。这个笔者不打算深入聊。这里只是给出代码:

static GlobalOLED&	get_instance() {
GlobalOLED* _oled = oled.load(std::memory_order_consume);
if(_oled) return _oled; // already inited!
std::lock_guard<std::mutex> _(instance_lock);_oled = oled.load(std::memory_order_consume); // load againif(!_oled) // still request init{_oled = new GlobalOLED; // process init sessionsoled.store(_oled, std::memory_order_release);}return _oled;}std::atomic<GlobalOLED*> oled;

单例模式为什么不讨喜?

压倒了单一职责

​ 做过软件工程的朋友也许能理解作者严肃的说“单例模式是最糟糕的模式”:本质上就体现在了最经典的单例模式实际上就是一个全局唯一对象的封装。举个例子:

void doWork() {
// ...
GlobalOLED::get_instance().processBufferUpdate();
}

​ 我们编写doWork的时候,实际上就让GlobalOLED直接穿透了咱们的模块,本来,我们的模块需要依赖到接口上,但是现在,这种书写方式却让我们依赖到了实现上去了,穿透了我们的接口。

​ 这种穿透同样的也造成了单元测试的麻烦。笔者编写单元测试的时候,就发现这样的问题:

void testDoWork() {// 想替换 Logger?抱歉,它是全局唯一的doWork();
}

​ 我们没办法替换Logger了!这下我们测试的时候,不得不依赖实际上正在使用的模块,而我们没办法保证**单元测试中每个单元是独立的!单例模式强迫每一个涉及到单例的模块都必须连同单例模块一起被测试,而且完全没法解耦。**更好的做法是使用 依赖注入(Dependency Injection):把 Logger 当作参数传进去,而不是写死成单例。

Tips:static单例的问题

​ 下面的是针对static实现单例的问题:如果单例持有一些重量级资源(比如大缓存、文件句柄),而实际只在很短时间用到,却始终常驻内存,就会浪费资源。C++ 中静态对象的销毁顺序不可控。如果单例比它依赖的对象更早析构,就会引发“野指针”问题。

难以扩展

假如说我们后面想要控制多个OLED,我们就发现,一旦 GlobalOLED 被设计为单例,就会很难扩展成多实例,往往需要重构大面积代码。单例在本质上 违背了“对扩展开放、对修改关闭”的开闭原则(OCP)

倒不如说——真正的单例模式实际上极端的少见。我们完全可以按照这些方式,重新评估我们单例模式的使用。


改进!翻转过来使用DI方式

​ 上面的问题发现没有?实际上,我们很多场景下,更多涉及到的是——对于一个子模块是全局唯一的。那么,我们就没有必要让他们在程序运行期间的时候就出现。而是提供一个针对封闭子系统下,注入一个对象作为单例进行使用。这样我们就能改善问题所在。这就是依赖注入

class OLEDUpdater {
public:
Worker(GlobalOLED& oled) : oled(oled) {}
void doWork() {
oled.processBufferUpdate();
}
GlobalOLED& getWorkInstance() {return oled;}
private:
GlobalOLED& oled;
};

这样就能在测试中轻松替换 Logger。因为现在,我们可以缩小单例到一个更小的子系统!


总结

我们在解决什么问题?

单例模式要解决的问题很明确:在程序的运行过程中,确保某个对象始终只有一个实例,并且提供一个全局访问点

但是,C++ 语言的隐式拷贝构造、赋值以及对象生命周期问题,让“保证唯一性”并不天然成立,需要明确的范式来约束。


我们是怎么解决的?

解决方式经历了一个演进的过程:

  1. 注释约束(错误示范)
    最原始的做法是靠注释提醒“只允许创建一次”,但这种约定显然不可靠,编译器不会帮你检查。

  2. 禁用拷贝/赋值构造
    通过 = delete 显式禁用复制、移动构造和赋值运算符,从语义上防止对象被意外复制:

    struct GlobalOLED {
    private:
    GlobalOLED();
    GlobalOLED(const GlobalOLED&) = delete;
    GlobalOLED& operator=(const GlobalOLED&) = delete;
    };
  3. 私有构造 + 公有访问接口
    通过将构造函数私有化,并提供一个静态方法来访问唯一实例:

    static GlobalOLED& get_instance() {
    static GlobalOLED oled; // static 保证只初始化一次
    return oled;
    }
  4. 多线程安全实现
    在复杂场景下,使用 std::call_once 或“双重检查锁”来确保多线程初始化时的安全性。

整体思路就是:封死一切可能产生多实例的路径,只保留唯一的访问入口


这样做的优缺点如何?
✅ 优点
❌ 缺点
  • 全局状态污染:引入隐式依赖,破坏模块解耦,本质上就是“伪装的全局变量”。
  • 难以测试:强制依赖全局实例,导致单元测试无法 Mock 或替换实现。
  • 生命周期不可控:静态对象的销毁顺序不确定,可能引发“野指针”问题;重量级资源常驻内存也可能浪费资源。
  • 扩展性差:一旦设计为单例,就很难扩展成多实例,违背开闭原则(OCP)。
  • 容易掩盖设计问题:单例常常被滥用来快速解决问题,反而隐藏了架构上的不合理。
方面优点缺点
唯一性确保全局对象唯一存在,避免状态冲突无法灵活扩展为多实例,违背 OCP
访问方式全局访问点,调用方便引入隐式依赖,模块耦合度增加
资源管理可用于全局缓存、配置管理等场景生命周期难以控制,可能导致资源浪费或析构顺序问题
实现复杂度C++11 之后可用 staticstd::call_once 实现线程安全多线程安全实现稍有不慎会引发竞态,调试困难
工程实践适用于日志、配置、驱动接口等全局唯一模块不利于单元测试,难以 Mock,掩盖架构问题
进一步的做法:DI

单例模式的最大问题在于它“穿透”了接口,把实现强制塞进全局调用中。一个更好的方式是——翻转依赖关系

class OLEDUpdater {
public:
OLEDUpdater(GlobalOLED& oled) : oled(oled) {}
void doWork() {
oled.processBufferUpdate();
}
private:
GlobalOLED& oled;
};
// 使用时手动注入
int main() {
auto& oled = GlobalOLED::get_instance();
OLEDUpdater updater(oled);
updater.doWork();
}

这样一来:

  • 单例被约束在更小的作用域(比如子系统内部)
  • 调用方只依赖接口,不依赖具体实现
  • 测试时可以轻松替换 Mock 对象,提高可测试性

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

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

相关文章

2025 年潜水泵厂家推荐榜,轴流/混流/全贯流/浮筒轴流/立式轴流/大流量轴流潜水泵厂家怎么选?3 个核心参数帮你挑对款!

随着国家对基础设施建设投入的加大、工业绿色转型的推进及智慧水务政策的落地,水泵作为流体输送与环境治理的核心设备,市场需求持续攀升。但市场上厂商资质与产品实力差异显著,消费者在选型时往往面临技术适配、质量…

2025年10月机械加工订制厂家最新推荐榜单:专业实力与客户满意度深度解析

2025年10月机械加工订制厂家最新推荐榜单:专业实力与客户满意度深度解析随着制造业的快速发展,机械加工行业的重要性日益凸显。优质的机械加工厂家不仅能够提供高质量的产品,还能在技术、服务等方面为企业带来显著优…

GitLab Duo Agent平台如何革新DataOps工作流

本文详细介绍了GitLab Duo Agent平台如何通过AI技术自动生成dbt数据模型,将原本需要6-8小时的手工编码工作缩短至6-8分钟,涵盖完整的模型结构、测试和文档生成。GitLab Duo Agent平台如何革新DataOps 手动创建dbt模型…

详细介绍:从零开始学神经网络——GRU(门控循环单元)

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

2025年10月方钢厂家最新推荐排行榜,热轧方钢,冷拉方钢,高强度方钢,优质钢材公司推荐!

2025年10月方钢厂家最新推荐排行榜,热轧方钢,冷拉方钢,高强度方钢,优质钢材公司推荐!随着建筑、机械制造和基础设施建设的不断发展,对方钢的需求日益增加。方钢作为重要的建筑材料之一,其质量和性能直接影响到工…

好用又免费的wiki推荐!Pandawiki免费同步GitHub,文档更新再也不会漏

好用又免费的wiki推荐!Pandawiki免费同步GitHub,文档更新再也不会漏在2025年的今天,文档管理已经成为每个团队都必须面对的挑战。特别是对于技术团队而言,如何确保文档与代码的同步更新,避免信息滞后造成的沟通成…

基于MATLAB的POD-DMD联合分析实现方案

基于MATLAB的POD-DMD联合分析实现方案,包含CFD数据读取、分解算法、可视化及工程优化策略:一、CFD数据预处理(适配OpenFOAM/CFX) 1.1 数据读取(Tecplot格式) function [U,V,W,P] = readTecplot3D(filename)% 读取…

Grafana 专题【左扬精讲】—— 提升 Grafana 安全性:LDAP 升级 LDAPS 的核心步骤与常见问题解决

Grafana 专题【左扬精讲】—— 提升 Grafana 安全性:LDAP 升级 LDAPS 的核心步骤与常见问题解决 https://grafana.com/docs/grafana/next/setup-grafana/configure-security/configure-authentication/ldap/ https://…

2025年10月锯床厂家最新推荐排行榜,金属锯床,木工锯床,数控锯床,带锯床公司推荐!

2025年10月锯床厂家最新推荐排行榜:金属锯床、木工锯床、数控锯床、带锯床公司推荐随着制造业的不断发展和技术进步,锯床作为重要的加工设备,在金属加工、木材加工等领域发挥着越来越重要的作用。为了帮助筛选锯床品…

C++入门学习准备

入门学习准备新生学习指南: 首先,在这下载学习视频。 然后按视频文件编号顺序,边看边操作边领会。

2025年10月确有专长培训机构最新推荐榜单:专业师资与高通过率口碑之选!

2025年10月确有专长培训机构最新推荐榜单:专业师资与高通过率口碑之选!随着社会对专业技能和专长人才需求的不断增长,确有专长培训市场也迎来了前所未有的发展机遇。为了帮助企业和个人筛选出真正具备实力和口碑的品…

深入解析:【论文阅读 | WACV 2025 | MCOR:通过跨模态信息互补和余弦相似性通道重采样模块增强的多光谱目标检测】

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

【基础软件专项训练营】笔记 - 详解

【基础软件专项训练营】笔记 - 详解2025-10-13 11:57 tlnshuju 阅读(0) 评论(0) 收藏 举报pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !imp…

vs2022程序包管理器控制台中文乱码

在程序包管理器控制台中输入以下命令,即可解决中文乱码问题:[Console]::OutputEncoding = [System.Text.Encoding]::UTF8

2025年10月精密弹簧厂家最新推荐排行榜,高精度弹簧,耐疲劳弹簧,定制弹簧,工业弹簧公司推荐!

2025年10月精密弹簧厂家最新推荐排行榜:高精度弹簧、耐疲劳弹簧与定制弹簧的行业标杆随着工业技术的不断进步,精密弹簧在各个领域的应用越来越广泛。无论是高精度弹簧、耐疲劳弹簧还是定制弹簧,选择一家可靠的生产厂…

LockSupport是什么

LockSupport是什么LockSupport让线程等待和唤醒的方法和Object类中的区别:方式一:使用Object中的wait()方法让线程等待,使用Object中的notify()方法唤醒线程 方式二:LockSupport类可以阻塞当前线程以及唤醒指定被阻…

(26)ASP.NET Core2.2 EF保存(基本保存、保存相关数据、级联删除、启用事务)

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

2025 年国内脱硫剂生产厂家最新推荐排行榜:氧化铁 / 羟基氧化铁 / 常温氧化铁 / 沼气等多类型产品优质企业全方位解析

引言当前工业领域对气体脱硫需求日益严苛,天然气、沼气、甲醇等行业在生产中,需依赖高性能脱硫剂保障安全与环保。但市场上脱硫剂厂家繁杂,部分企业存在工艺落后、质量不稳定、售后缺失等问题,让采购企业难以抉择。…

护花使者

这晚在街中偶遇心中的她 两脚决定不听使唤跟她归家 深宵的冷风 不准吹去她 她那幽幽眼神快要对我说话 纤纤身影 飘飘身影 默默转来吧 对我说浪漫情人爱我吗 贪心的晚风 竟敢拥吻她 将她秀发温温柔柔每缕每缕放下 卑污的…

实用指南:Kafka 合格候选主副本(ELR)在严格 min ISR 约束下提升选主韧性

实用指南:Kafka 合格候选主副本(ELR)在严格 min ISR 约束下提升选主韧性pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-famil…