从C++编程入手设计模式1——单例模式
从C++编程入手设计模式
在这之前,为什么要有设计模式
Design Pattern是一个非常贴近工程化的一个议题,我们首先再开始之前(尽管有一些朋友可能已经早早就掌握了设计模式,但是出于看乐子的心态还是进来看看我写的有多烂(x)),最好还是对我们讨论的对象存在一个基础的认知是比较好的。
我相信任何一个初学编程的朋友都遇到这个苦恼。跟其他人协作的时候,都遇到过代码写的乱七八糟(即:可修改性差,可维护性差,可扩展性差)的严重问题。当我们增加新的需求的时候,我们会面临代码混杂等若干棘手的问题。这就像是一个人盖房子,地基不牢,只能在上面打补丁——越修越没法看!最后只会再新的需求中轰然倒塌。如果我们以开始就像架构师一样,设计好一个最基本的框架,在编程的思考与实践中完善我们的框架。我们虽然可能还是无法回避史山,但是至少对于小的递增性需求,仍然游刃有余不慌不忙的扩展我们的代码。换而言之——**我们希望采用一个符合被抽象对象的实际的抽象,从而自然的完成我们的编程。**设计模式就是想要解决这个问题。
设计模式被视为前人经验的结晶,是解决常见设计问题的典范方案。它们不仅提升了代码的质量和可维护性,还促进了开发团队之间的协作与沟通。换而言之,他是开发团队对一个子模块的抽象方式的共同认识,大家都遵守这样的框架抽象增添修改删除代码。这样至少不会太乱。自然,这种宽泛的说法不会落实到具体的代码实现,设计模式自身就是为了解决一类场景问题而不是特定问题而存在的,我们学习设计模式就是使用我们选择的设计模式的基本范式解决我们眼下的问题。(使用方法论解决我们的问题)
设计模式说到这里,就要聊聊它包含什么了,大伙看一眼即可,知道设计模式有什么就行!
- 模式名称:每个设计模式都有一个简洁的名称,便于开发人员之间的交流和讨论。
- 问题描述:明确指出在特定上下文中需要解决的问题。
- 解决方案:提供一个通用的设计结构,用于解决上述问题。
- 效果:描述应用该模式后可能产生的结果,包括优点和可能的副作用。
设计模式也存在一定的分类,我们就是按照这个大纲一步一步的解决我们的问题:
- 创建型模式:关注对象的创建过程,旨在以适当的方式创建对象。例如:单例模式、工厂方法模式、抽象工厂模式、建造者模式和原型模式。
- 结构型模式:关注类和对象的组合,旨在实现更大的结构。例如:适配器模式、桥接模式、装饰器模式、组合模式和代理模式。
- 行为型模式:关注对象之间的通信和职责分配。例如:观察者模式、策略模式、命令模式、状态模式和责任链模式。
嗯,枯燥的概念,没关系,我们会一步一步学习具体的设计模式,然后慢慢理解他们的!
单例模式
单例模式是这样的一个模式,它实际上声明了我们的模块中全局应当**有且只有(Have And Only Have one
)**一个的对象。举个例子,对于日志薄记系统,我们一个模块或者甚至一个系统,只应该有一个实际的日志登记对象,而不是生成一大堆日志对象混乱的向文件/标准输出输出纷杂的内容。亦或者我们需要有一个全局的配置管理器,不应当存在第二个导致两个对象配置不一致造成系统状态的不确定性(你也不想看着一个配置叫你刹车另一个配置叫你这个时候踩油门吧!)
当我们希望全局对象唯一的时候,这个对象就应该被设计为一个单例(单例模式的设计对象),单例的访问需要使用一个接口:全局访问点来获取该实例。
说了半天,无非就是两个重要的点:
- 确保一个类只有一个实例:通过限制类的实例化过程,防止外部创建多个实例。
- 提供一个全局访问点:通过一个静态方法或属性,使得其他对象可以访问该唯一实例。
这种模式适用于需要全局共享资源的场景,如配置管理器、日志记录器、数据库连接池等。
使用C++实现的时候,我们需要注意的内容
1. 私有化构造函数:防止外部通过 new
操作符创建多个实例。
我们需要把创建的构造函数放置到我们的私有函数部分,让我们信任的接口吐出来我们的对象访问指针。这就是把我们的Singleton放置到我们的private区域上,这样我们就没办法肆意的显示或者是隐式(这个最重要,C++喜欢自己偷摸干点事情让代码跑起来)
2. 提供静态方法获取实例:例如 getInstance()
方法,用于返回唯一的实例。
我们上面把构造函数藏了起来,但是我们的确需要访问单例,这要咋办呢?答案是让我们信任的接口吐出来我们的对象访问指针。这里的getInstance()
只是一个指代,你像我就会使用instance()
这个名称。
3. 确保线程安全:在多线程环境下,确保不会创建多个实例。
这个议题跟并发编程交叉,我们不希望在初始化的时候,因为数据竞争的问题导致重复多次的创建。举个例子:
if(!instance){// create this instance = new Instance();
}
对于线程一和线程二,有如下的进程视图
!instance <- | !instance <-
do_create | do_create
在外面的线程一刚判断完准备执行do_create的汇编代码的时候,立马被CPU甩开给了线程二,这个时候线程二创建好了之后,恢复线程一的执行就会出现第二次创建(他已经过了判断了!)
这个时候上锁就是一个正确的抉择,这是我们后面谈论的——双重上锁机制。
一些经典的实现
饿汉和懒汉单例是我们常见的讨论的实现区别,虽然这两个名称被取出来我认为纯粹闲得慌。实际上就是说明——我们的单例是何使创建的。是类存在的时候,咱们就开始加载类呢(饿汉单例)?还是delay until we use呢?(懒汉单例)
饿汉式(Eager Initialization)
在类加载时就创建实例,线程安全,但可能导致资源浪费。
class Singleton {
private:static Singleton instance;Singleton() {}
public:static Singleton& getInstance() {return instance;}
};
// 放私有文件隔离
Singleton Singleton::instance;
懒汉式(Lazy Initialization)
在首次使用时创建实例,需注意线程安全问题。
class Singleton {
private:static Singleton* instance;Singleton() {}
public:// 有问题!static Singleton* getInstance() {if (instance == nullptr) {instance = new Singleton();}return instance;}
};
Singleton* Singleton::instance = nullptr;
这个问题是我上面已经谈论过的为什么不安全的问题,忘记的bro自己翻上去看两眼
改进版本(双重检查锁定(Double-Checked Locking))
#include <mutex>class Singleton {
private:static Singleton* instance;static std::mutex mtx;Singleton() {}
public:static Singleton* getInstance() {if (instance == nullptr) {std::lock_guard<std::mutex> lock(mtx);if (instance == nullptr) {instance = new Singleton();}}return instance;}
};
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mtx;
你可以看到这样我们就把事情解决了,思考一下为什么?
- 第一次检查(无锁):
if (instance == nullptr)
在加锁前快速判断实例是否已存在。若已存在,直接返回,避免不必要的锁竞争,提升性能。- 加锁保护:
当实例未初始化时,通过std::lock_guard
对互斥量mtx
加锁,确保同一时间只有一个线程能进入临界区,防止多个线程同时创建实例。- 第二次检查(有锁):
在锁内再次检查instance == nullptr
,防止其他线程在第一次检查后、加锁前已经完成了实例化(避免重复创建)。- 内存安全:
std::mutex
和std::lock_guard
保证了new Singleton()
的原子性,确保实例指针的赋值操作对其他线程可见,避免未初始化或部分初始化的对象被访问。
C++11以上的朋友们有福了:static一招击杀
利用 C++11 的特性,线程安全且简洁。
class Singleton {
private:Singleton() {}
public:static Singleton& getInstance() {static Singleton instance;return instance;}
};
优缺点一览
优点:
- 节省资源:避免重复创建对象,节省系统资源。
- 全局访问:提供一个全局访问点,方便管理。
缺点:
- 隐藏的全局状态:可能导致代码难以测试和维护。
- 并发问题:在多线程环境下,需确保线程安全。
- 生命周期管理:在某些语言中,单例的销毁需要特别处理。
上coding
我们现在就来看一个笔者自己写的例子。这个是题目,你可以自己先试试?
题目一:实现一个线程安全的懒汉式单例
请使用 C++20 的特性,实现一个线程安全的懒汉式(懒加载)单例类 Logger
。要求如下:
- 类中包含一个
void log(const std::string& message)
函数,用于将日志打印到终端。 - 单例对象要在第一次调用
getInstance
时创建,并保证多线程安全。 - 禁止拷贝和移动构造。
示例接口:
class Logger {
public:static Logger& getInstance();void log(const std::string& message);private:Logger();~Logger();Logger(const Logger&) = delete;Logger& operator=(const Logger&) = delete;Logger(Logger&&) = delete;Logger& operator=(Logger&&) = delete;
};
你只需要完成类的定义与实现部分,并编写一个 main 函数示例(单线程或多线程均可)来测试它。
看看笔者的实现
Note:笔者的实现肯定不是最优的,也许会存在其他问题,请各位看官如果发现了任何问题,请严肃的提Issue or PR,我会深入同您探讨,共同进步!
#pragma once
#include <mutex>
#include <string>
class SimpleLogger {
public:/*** @brief instance make the query of getting instances** @return SimpleLogger& instance ref itself*/static SimpleLogger& instance();/*** @brief log messages** @param message message to log*/void log_messages(const std::string& message);private:SimpleLogger();~SimpleLogger() = default;/* Logger is nether copiable and movable */SimpleLogger& operator=(const SimpleLogger&) = delete;SimpleLogger(const SimpleLogger&) = delete;SimpleLogger(SimpleLogger&&) = delete;SimpleLogger& operator=(SimpleLogger&&) = delete;
};
你可以看到笔者把我们的任何构造都放到了私有区域上,防止我们犯傻,创建了不该创建的东西,所有的实例接口都是使用static SimpleLogger& instance();
这个静态接口进行访问。
log_messages
是作为一个类正常工作的实例表达的,实际上你可以换成你自己的东西。
C++11开始,使用static初始化足以保证我们的线程安全的做初始化了
#include "logger.h"
#include <print>SimpleLogger::SimpleLogger() {std::println("[logger: ] Logger init invoke once");
}SimpleLogger& SimpleLogger::instance() {static SimpleLogger simpleLogger; // only init oncereturn simpleLogger;
}void SimpleLogger::log_messages(const std::string& message) {std::println("[logger: ] {}", message);
}
测试一下?
这是笔者的测试文件:
#include "logger.h"
#include <string>
#include <thread>
#include <vector>
static constexpr unsigned int TEST_TIME = 50;void test_functions(const std::string messages) {auto& logger_instance = SimpleLogger::instance();logger_instance.log_messages(messages);
}int main() {std::vector<std::thread> test_threads;for (int i = 0; i < TEST_TIME; i++) {std::string result = "Hello from Times: " + std::to_string(i);test_threads.emplace_back(test_functions, result);}for (auto& each : test_threads) {each.join();}
}
[charliechen@Charliechen build]$ ./logger
[logger: ] Logger init invoke once
[logger: ] Hello from Times: 0
[logger: ] Hello from Times: 1
...
[logger: ] Hello from Times: 47
[logger: ] Hello from Times: 48
[logger: ] Hello from Times: 49
实现代码:My Implementations
习题
虽然我没这个能力和胆子随意出题,但是下面这个例题还是相当经典的:
配置管理器
设计一个 ConfigManager
类,使用单例模式来管理配置文件。要求如下:
ConfigManager
负责从配置文件(可假设配置文件叫做config.txt
)读取配置项(每行一个键值对,如:key=value
),并能返回对应的值。- 提供
std::optional<std::string> getValue(const std::string& key)
接口,返回配置项的值(如果有的话)。 - 使用现代 C++20(如
std::unordered_map
,std::string_view
,std::optional
)来简化实现。 - 文件读取可简单处理(例如固定几行内容模拟文件读入即可)。
笔者也有实现:My Implementations
Reference
设计模式 (计算机) - 维基百科,自由的百科全书
相关文章:
从C++编程入手设计模式1——单例模式
从C编程入手设计模式 在这之前,为什么要有设计模式 Design Pattern是一个非常贴近工程化的一个议题,我们首先再开始之前(尽管有一些朋友可能已经早早就掌握了设计模式,但是出于看乐子的心态还是进来看看我写的有多烂…...

根据Cortex-M3(包括STM32F1)权威指南讲解MCU内存架构与如何查看编译器生成的地址具体位置
首先我们先查看官方对于Cortex-M3预定义的存储器映射 1.存储器映射 1.1 Cortex-M3架构的存储器结构 内部私有外设总线:即AHB总线,包括NVIC中断,ITM硬件调试,FPB, DWT。 外部私有外设总线:即APB总线,用于…...
vue的h函数(在 Vue 2中也称为 createElement)理解
官方定义 定义: 返回一个“虚拟节点” ,通常缩写为 VNode: 一个普通对象,其中包含向 Vue 描述它应该在页面上呈现哪种节点的信息,包括对任何子节点的描述。用于手动编写render h函数格式说明及使用 h 接收三个参数 type: 必需,…...

MCP入门实战(极简案例)
MCP简介 MCP(Model Context Protocol,模型上下文协议)2024年11月底由 Antbropic 推出的一种开放标准,旨在统一大型语言模型(LLM)与外部数据源和工具之间的通信协议。 Function Calling是AI模型调用函数的机制,MCP是一个标准协议,使AI模型与API无缝交互,而Al Agent是一个…...
STM32中,如何理解看门狗
在STM32微控制器中,看门狗(Watchdog)是一种硬件计时器,用于监控系统运行状态,防止软件死锁或跑飞。其核心机制是:系统需定期“喂狗”(复位看门狗计数器),若未及时喂狗&am…...

Cursor从入门到精通实战指南(一):开始使用Cursor
一、简介与核心优势 Cursor是一款基于VSCode开发的AI编程工具,集成了GPT-4、Claude 3.5等先进大语言模型,支持代码补全、生成、重构、调试等功能。其核心优势包括: 高效协作:通过自然语言对话实现代码开发,支持跨文件…...
麒麟v10+信创x86处理器离线搭建k8s集群完整过程
前言 最近为某客户搭建内网的信创环境下的x8s集群,走了一些弯路,客户提供的环境完全与互联网分离,通过yum、apt这些直接拉依赖就别想了,用的操作系统和cpu都是国产版本,好在仍然是x86的,不是其他架构&…...

计算机组成原理——cache
3.4cache 出自up主Beokayy传送门 1.局部性原理 时间局部性: 在最近的未来要用到的信息,很可能是现在正在使用的信息,因为程序中存在循环。 空间局部性: 在最近的未来要用到的信息,很可能与现在正在使用的信息在存储…...

EasyExcel使用导出模版后设置 CellStyle失效问题解决
EasyExcel使用导出模版后在CellWriteHandler的afterCellDispose方法设置 CellStyle失效问题解决方法 问题描述:excel 模版塞入数据后,需要设置单元格的个性化设置时失效,本文以设置数据格式为例(设置列的数据展示时需要加上千分位…...
关于AWESOME-DIGITAL-HUMAN的部署
AWESOME-DIGITAL-HUMAN是一个开源数字人项目,可以容器化部署,资源占用少,可以对接dify,使用起来也很方便,非常感谢开发者。 容器化部署后,其实是有两个容器,分别启动两个服务,一个前…...
WebAssembly 及 HTML Streaming:重塑前端性能与用户体验
WebAssembly 及 HTML Streaming:重塑前端性能与用户体验 引言 在移动互联网时代,用户对 Web 应用的性能和体验要求日益苛刻。白屏时间、首屏渲染速度、交互流畅度,甚至 SEO 优化,都成为前端工程师必须面对的挑战。传统的前端技术…...
python同步mysql数据
python写了一个简单的mysql数据同步脚本,只作为学习练习,大佬勿喷 # -*- coding: utf-8 -*- """ Time:2025/5/29 14:38 Auth:HEhandsome """ import pymysql from pymysql import Connectclass Mysql:def __init__(self):#源数据库self.sou_hos…...
shell之通配符及正则表达式,grep参数
通配符与正则表达式 通配符(Globbing) 通配符是由 Shell 处理的特殊字符,用于路径或文件名匹配。当 Shell 在命令参数中遇到通配符时,会将其扩展为匹配的文件路径;若没有匹配项,则作为普通字符传递给命令…...
RuoYi前后端分离框架集成手机短信验证码(一)之后端篇
一、背景 本项目基于RuoYi 3.8.9前后端分离框架构建,采用Spring Security实现系统权限管理。作为企业级应用架构的子模块,系统需要与顶层项目实现用户数据无缝对接(以手机号作为统一用户标识),同时承担用户信息采集的重要职能。为此,我们在保留原有账号密码登录方式的基…...

Knife4j框架的使用
文章目录 引入依赖配置Knife4j使用Knife4j 访问 SpringBoot 生成的文档 Knife4j 是基于 Swagger 的增强工具,对 Swagger 进行了拓展和优化,从而有更美观的界面设计和更强的功能 引入依赖 Spring Boot 2.7.18 版本 <dependency> <groupId>c…...

深兰科技陈海波率队考察南京,加速AI医诊大模型区域落地应用
近日,深兰科技创始人、董事长陈海波受邀率队赴南京市,先后考察了南京江宁滨江经济开发区与鼓楼区,就推进深兰AI医诊大模型在南京的落地应用,与当地政府及相关部门进行了深入交流与合作探讨。 此次考察聚焦于深兰科技自主研发的AI医…...

【芯片设计中的交通网络革命:Crossbar与NoC架构的博弈C架构的博弈】
在芯片设计领域,总线架构如同城市交通网,决定了数据流的通行效率。随着AI芯片、车载芯片等复杂场景的爆发式增长,传统总线架构正面临前所未有的挑战。本文将深入解析两大主流互连架构——Crossbar与NoC的优劣,揭示芯片"交通网…...

deepseek告诉您http与https有何区别?
有用户经常问什么是Http , 什么是Https ? 两者有什么区别,下面为大家介绍一下两者的区别 一、什么是HTTP HTTP是一种无状态的应用层协议,用于在客户端浏览器和服务器之间传输网页信息,默认使用80端口 二、HTTP协议的特点 HTTP协议…...

mac将自己网络暴露到公网
安装服务 brew tap probezy/core && brew install cpolar// 安装cpolar sudo cpolar service install // 启动服务 sudo cpolar service start访问管理网站 http://127.0.0.1:9200/#/tunnels/list 菜单“隧道列表” 》 编辑 自定义暴露的端口 再到在线列表中查看公网…...
考研政治资料分享 百度网盘
考研资料分享考研资料合集 百度网盘(仅供参考学习) 通过网盘分享的文件:2026考研英语数学政治最新等3个文件 链接: https://pan.baidu.com/s/1iK2LvbkoreNxHZ7fmOkcyQ?pwd4drt 提取码: 4drt 链接: https://pan.baidu.com/s/1FuNV…...

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

罗马-华为
SPA应用:single-page application:单页应用SPA是一种网络应用程序或网站的模型,它通过动态重写当前页面来与用户交互,这种方法避免了页面之间切换打断用户体验在单页应用中 集成 ROMA Connect 主要包含四个组件:数据集成( FDI )、服务集成( APIC )、消息集成 ( MQS …...
单例模式的隐秘危机
引言 单例模式作为设计模式中的基石,广泛应用于配置管理、线程池、缓存系统等关键场景。然而,许多开发者误以为“私有构造函数”足以保障其唯一性,却忽视了反射机制、对象克隆、序列化反序列化这三把“隐形利刃”——它们能绕过常规防御&…...
微信小程序常用方法
微信小程序 常用方法 setData() https://developers.weixin.qq.com/miniprogram/dev/reference/api/Page.html#%E7%BB%84%E4%BB%B6%E4%BA%8B%E4%BB%B6%E5%A4%84%E7%90%86%E5%87%BD%E6%95%B0 在微信小程序中,setData 是一个非常重要的方法,主要用于更新…...

切片器导航-大量报告页查看的更好方式
切片器导航-大量报告页查看的更好方式 现在很多报告使用的是按钮导航,即使用书签按钮来制作页面导航的方式。但是当我们的报告有几十页甚至上百页的时候,使用书签按钮来制作页面导航,无论是对于报表制作者还是报告使用者来说都是一种很繁琐的…...
火山引擎声音复刻
首先,我需要确定火山引擎是什么,扣子声音复刻具体指什么。火山引擎是字节跳动旗下的云服务平台,提供各种技术解决方案。声音复刻应该属于他们的AI语音相关服务。 接下来,用户可能想知道这个功能的应用场景。比如,企业用…...
【数据分析】Pandas
目录 🌟 前言🏗️ 技术背景与价值🩹 当前技术痛点🛠️ 解决方案概述👥 目标读者说明 🧠 一、技术原理剖析📊 核心概念图解💡 核心作用讲解🔧 关键技术模块说明⚖️ 技术选…...
【ROS2】Qt Debug日志重定向到ROS2日志管理系统中
1、注册消息处理函数 Qt 利用 qInstallMessageHandler 接口可以 注册消息处理函数; 将QDebug等输出重定向到ROS2的日志管理中,使用 RCLCPP_DEBUG 输出日志 示例: 1)定义消息处理函数 namespace GW {void ros2Logger(QtMsgType type, const QMessageLogContext &cont…...
经典SQL查询问题的练习第一天
首先有三张表,学生表、课程表、成绩表 student:studentId,studentName; course:courseId,courseName,teacher; score:score,studentId,courseId; 接着有以下几道题目: ①查询课程编号为‘0006’的总成绩: 首先总成绩&#x…...

ubuntu 22.04安装k8s高可用集群
文章目录 1.环境准备(所有节点)1.1 关闭无用服务1.2 环境和网络1.3 apt源1.4 系统优化1.5 安装nfs客户端 2. 装containerd(所有节点)3. master的高可用方案(master上操作)3.1 安装以及配置haproxyÿ…...