从C++编程入手设计模式1——单例模式

news2025/5/31 14:52:16

从C++编程入手设计模式

在这之前,为什么要有设计模式

​ Design Pattern是一个非常贴近工程化的一个议题,我们首先再开始之前(尽管有一些朋友可能已经早早就掌握了设计模式,但是出于看乐子的心态还是进来看看我写的有多烂(x)),最好还是对我们讨论的对象存在一个基础的认知是比较好的。

​ 我相信任何一个初学编程的朋友都遇到这个苦恼。跟其他人协作的时候,都遇到过代码写的乱七八糟(即:可修改性差,可维护性差,可扩展性差)的严重问题。当我们增加新的需求的时候,我们会面临代码混杂等若干棘手的问题。这就像是一个人盖房子,地基不牢,只能在上面打补丁——越修越没法看!最后只会再新的需求中轰然倒塌。如果我们以开始就像架构师一样,设计好一个最基本的框架,在编程的思考与实践中完善我们的框架。我们虽然可能还是无法回避史山,但是至少对于小的递增性需求,仍然游刃有余不慌不忙的扩展我们的代码。换而言之——**我们希望采用一个符合被抽象对象的实际的抽象,从而自然的完成我们的编程。**设计模式就是想要解决这个问题。

​ 设计模式被视为前人经验的结晶,是解决常见设计问题的典范方案。它们不仅提升了代码的质量和可维护性,还促进了开发团队之间的协作与沟通。换而言之,他是开发团队对一个子模块的抽象方式的共同认识,大家都遵守这样的框架抽象增添修改删除代码。这样至少不会太乱。自然,这种宽泛的说法不会落实到具体的代码实现,设计模式自身就是为了解决一类场景问题而不是特定问题而存在的,我们学习设计模式就是使用我们选择的设计模式的基本范式解决我们眼下的问题。(使用方法论解决我们的问题)

​ 设计模式说到这里,就要聊聊它包含什么了,大伙看一眼即可,知道设计模式有什么就行!

  1. 模式名称:每个设计模式都有一个简洁的名称,便于开发人员之间的交流和讨论。
  2. 问题描述:明确指出在特定上下文中需要解决的问题。
  3. 解决方案:提供一个通用的设计结构,用于解决上述问题。
  4. 效果:描述应用该模式后可能产生的结果,包括优点和可能的副作用。

​ 设计模式也存在一定的分类,我们就是按照这个大纲一步一步的解决我们的问题:

  1. 创建型模式:关注对象的创建过程,旨在以适当的方式创建对象。例如:单例模式、工厂方法模式、抽象工厂模式、建造者模式和原型模式。
  2. 结构型模式:关注类和对象的组合,旨在实现更大的结构。例如:适配器模式、桥接模式、装饰器模式、组合模式和代理模式。
  3. 行为型模式:关注对象之间的通信和职责分配。例如:观察者模式、策略模式、命令模式、状态模式和责任链模式。

​ 嗯,枯燥的概念,没关系,我们会一步一步学习具体的设计模式,然后慢慢理解他们的!

单例模式

​ 单例模式是这样的一个模式,它实际上声明了我们的模块中全局应当**有且只有(Have And Only Have one)**一个的对象。举个例子,对于日志薄记系统,我们一个模块或者甚至一个系统,只应该有一个实际的日志登记对象,而不是生成一大堆日志对象混乱的向文件/标准输出输出纷杂的内容。亦或者我们需要有一个全局的配置管理器,不应当存在第二个导致两个对象配置不一致造成系统状态的不确定性(你也不想看着一个配置叫你刹车另一个配置叫你这个时候踩油门吧!)

当我们希望全局对象唯一的时候,这个对象就应该被设计为一个单例(单例模式的设计对象),单例的访问需要使用一个接口:全局访问点来获取该实例。

​ 说了半天,无非就是两个重要的点:

  1. 确保一个类只有一个实例:通过限制类的实例化过程,防止外部创建多个实例。
  2. 提供一个全局访问点:通过一个静态方法或属性,使得其他对象可以访问该唯一实例。

这种模式适用于需要全局共享资源的场景,如配置管理器、日志记录器、数据库连接池等。

使用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;

​ 你可以看到这样我们就把事情解决了,思考一下为什么?

  1. 第一次检查(无锁)
    if (instance == nullptr) 在加锁前快速判断实例是否已存在。若已存在,直接返回,避免不必要的锁竞争,提升性能。
  2. 加锁保护
    当实例未初始化时,通过 std::lock_guard 对互斥量 mtx 加锁,确保同一时间只有一个线程能进入临界区,防止多个线程同时创建实例。
  3. 第二次检查(有锁)
    在锁内再次检查 instance == nullptr,防止其他线程在第一次检查后、加锁前已经完成了实例化(避免重复创建)。
  4. 内存安全
    std::mutexstd::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。要求如下:

  1. 类中包含一个 void log(const std::string& message) 函数,用于将日志打印到终端。
  2. 单例对象要在第一次调用 getInstance 时创建,并保证多线程安全。
  3. 禁止拷贝和移动构造。

示例接口:

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 once
	return 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 类,使用单例模式来管理配置文件。要求如下:

  1. ConfigManager 负责从配置文件(可假设配置文件叫做 config.txt)读取配置项(每行一个键值对,如:key=value),并能返回对应的值。
  2. 提供 std::optional<std::string> getValue(const std::string& key) 接口,返回配置项的值(如果有的话)。
  3. 使用现代 C++20(如 std::unordered_map, std::string_view, std::optional)来简化实现。
  4. 文件读取可简单处理(例如固定几行内容模拟文件读入即可)。

笔者也有实现:My Implementations

Reference

设计模式 (计算机) - 维基百科,自由的百科全书

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

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

相关文章

根据Cortex-M3(包括STM32F1)权威指南讲解MCU内存架构与如何查看编译器生成的地址具体位置

首先我们先查看官方对于Cortex-M3预定义的存储器映射 1.存储器映射 1.1 Cortex-M3架构的存储器结构 内部私有外设总线&#xff1a;即AHB总线&#xff0c;包括NVIC中断&#xff0c;ITM硬件调试&#xff0c;FPB, DWT。 外部私有外设总线&#xff1a;即APB总线&#xff0c;用于…

MCP入门实战(极简案例)

MCP简介 MCP(Model Context Protocol,模型上下文协议)2024年11月底由 Antbropic 推出的一种开放标准,旨在统一大型语言模型(LLM)与外部数据源和工具之间的通信协议。 Function Calling是AI模型调用函数的机制,MCP是一个标准协议,使AI模型与API无缝交互,而Al Agent是一个…

Cursor从入门到精通实战指南(一):开始使用Cursor

一、简介与核心优势 Cursor是一款基于VSCode开发的AI编程工具&#xff0c;集成了GPT-4、Claude 3.5等先进大语言模型&#xff0c;支持代码补全、生成、重构、调试等功能。其核心优势包括&#xff1a; 高效协作&#xff1a;通过自然语言对话实现代码开发&#xff0c;支持跨文件…

计算机组成原理——cache

3.4cache 出自up主Beokayy传送门 1.局部性原理 时间局部性&#xff1a; 在最近的未来要用到的信息&#xff0c;很可能是现在正在使用的信息&#xff0c;因为程序中存在循环。 空间局部性&#xff1a; 在最近的未来要用到的信息&#xff0c;很可能与现在正在使用的信息在存储…

EasyExcel使用导出模版后设置 CellStyle失效问题解决

EasyExcel使用导出模版后在CellWriteHandler的afterCellDispose方法设置 CellStyle失效问题解决方法 问题描述&#xff1a;excel 模版塞入数据后&#xff0c;需要设置单元格的个性化设置时失效&#xff0c;本文以设置数据格式为例&#xff08;设置列的数据展示时需要加上千分位…

Knife4j框架的使用

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

深兰科技陈海波率队考察南京,加速AI医诊大模型区域落地应用

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

【芯片设计中的交通网络革命:Crossbar与NoC架构的博弈C架构的博弈】

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

deepseek告诉您http与https有何区别?

有用户经常问什么是Http , 什么是Https &#xff1f; 两者有什么区别&#xff0c;下面为大家介绍一下两者的区别 一、什么是HTTP HTTP是一种无状态的应用层协议&#xff0c;用于在客户端浏览器和服务器之间传输网页信息&#xff0c;默认使用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 菜单“隧道列表” 》 编辑 自定义暴露的端口 再到在线列表中查看公网…

拓扑排序算法剖析与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…

罗马-华为

SPA应用:single-page application:单页应用SPA是一种网络应用程序或网站的模型,它通过动态重写当前页面来与用户交互,这种方法避免了页面之间切换打断用户体验在单页应用中 集成 ROMA Connect 主要包含四个组件:数据集成( FDI )、服务集成( APIC )、消息集成 ( MQS …

切片器导航-大量报告页查看的更好方式

切片器导航-大量报告页查看的更好方式 现在很多报告使用的是按钮导航&#xff0c;即使用书签按钮来制作页面导航的方式。但是当我们的报告有几十页甚至上百页的时候&#xff0c;使用书签按钮来制作页面导航&#xff0c;无论是对于报表制作者还是报告使用者来说都是一种很繁琐的…

ubuntu 22.04安装k8s高可用集群

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

使用java实现word转pdf,html以及rtf转word,pdf,html

word,rtf的转换有以下方案&#xff0c;想要免费最靠谱的是LibreOffice方案, LibreOffice 是一款 免费、开源、跨平台 的办公软件套件&#xff0c;旨在为用户提供高效、全面的办公工具&#xff0c;适用于个人、企业和教育机构。它支持多种操作系统&#xff08;Windows、macOS、…

使用LSTM进行时间序列分析

LSTM&#xff08;长短期记忆网络&#xff0c;Long Short-Term Memory&#xff09;是一种特殊的循环神经网络&#xff08;RNN&#xff09;&#xff0c;专门用于处理时间序列数据。由于其独特的结构设计&#xff0c;LSTM能够有效地捕捉时间序列中的长期依赖关系&#xff0c;这使得…

【密码学——基础理论与应用】李子臣编著 第十三章 数字签名 课后习题

题目 逐题解析 13.1 知道p83,q41,h2,g4,x57,y77。 我看到答案&#xff0c;“消息M56”的意思居然是杂凑值&#xff0c;也就是传统公式的H(M)。 选择k23&#xff0c;那么r(g^k mod p) mod q 51 mod 4110,sk(H(M)xr) mod q29 ws mod q17,u1(mw) mod q9&#xff0c;u2(rw) m…

k8s中kubeSphere的安装使用+阿里云私有镜像仓库配置完整步骤

一、实验目的 1、掌握kubeSphere 的安装部署 2、掌握kubesphere 使用外部镜像仓库&#xff1b; 2、熟悉图像化部署任务&#xff1a;产生pod---定义服务--验证访问 本次实验旨在通过 KubeSphere 平台部署基于自定义镜像&#xff08;nginx:1.26.0 &#xff09;的有状态副本集…

Agilent安捷伦Cary3500 UV vis光谱仪Cary60分光光度计Cary1003004000500060007000 UV visible

Agilent安捷伦Cary3500 UV vis光谱仪Cary60分光光度计Cary1003004000500060007000 UV visible

arcgis js 4.x 的geometryEngine计算距离、面积、缓冲区等报错、失败

在arcgis js 4.x版本中geometryEngine.geodesicArea计算面积时&#xff0c;有时会失败&#xff0c;失败的主要原因是&#xff0c;当前底图的坐标系不是WGS84大地坐标系&#xff08;代号4326&#xff09;或者web墨卡托投影&#xff08;代号102113, 102100, 3857这三种之一&#…