设计模式:饿汉式和懒汉式单例模式(C++实现)以及日志系统的实现

C++ 单例模式(饿汉模式、懒汉模式) 文章目录前言一、单例模式的应用场景二、单例模式的实现1.饿汉式(Eager Singleton)2.懒汉模式-单线程(Lazy Singleton)2.1 简易版2.2 私有嵌套类-回收专用3、懒汉模式 -多线程版本4、懒汉模式 - 现代c++11-优雅的多线程版本 前言 单例模式是23种设计模式中最常用的一种模式。一个类只能创建一个对象,让类自身负责保存它的唯一实例,并提供一个访问它的全局访问接口,这就是单例模式。 Singleton类 单例模式 -instance 私有 -Singe 阅读详情

1. 背景

在一个项目的日志系统里面,我们常常会发现日志模块的实现是使用单例模式。单例模式的特点和它的名字一样,就是一个类能且只能实例化出一个唯一的对象。那么这样做有什么好处呢?

比如,对于日志模块,我们可以先思考它的功能是什么。作为日志,最主要的功能是把系统在运行过程中产生的一些debug、info、warning、error、critical信息给刷盘到磁盘中。既然是刷盘,那么我们一般会创建出一个子线程来完成刷盘的功能。当提及子线程,我们就需要注意线程安全的问题了。如果有多个日志对象,都可以进行刷盘,但是线程都是抢占式的,可能一个线程记录一半信息,就由于CPU的调度切换到另一个负责刷盘的子线程去了。

结果就是,日志的内容都变成一段一段的不连续信息,刷盘刷得乱七八糟。有同学可能会问:那我加互斥锁!加锁确实可以,但是互斥锁得争夺和之后得阻塞需要陷入内核态,十分消耗时间。此外,就算加了互斥锁,可以保证信息是连续的,但是无法保证信息的时序是一致的。可能一个线程存了10点到12点的日志信息(夸张一点),另一个存了9点到11点的信息,那么在线程刷盘的过程中,时序是相互交叉的。这样也是一种混乱。

因此,从这个角度出发,日志系统通常被设计成单例模式,只有一个对象,按顺序拿到需要记录的日志,再按顺序刷到磁盘中。

2. 单例模式实现

单例模式的实现有饿汉式和懒汉式。对于饿汉式,是在用户还没有使用这个对象之前,这个对象就已经存在了。而懒汉式是等到用户调用,才急急忙忙的产生。

下面实现了两种不同的单例模式。

2.1 饿汉式的单例模式

// 1. 饿汉式的单例
class Singleton {
public:
	Singleton* getInstance() { return &instance; }
private:
	static Singleton instance;
	Singleton() {}
	Singleton(const Singleton&) = delete;
	Singleton& operator=(const Singleton&) = delete;
};
Singleton Singleton::instance;

对于上面这个饿汉式的单例模式,有几点需要注意的地方:

  • 为什么得到这个对象的外部接口函数getInstance需要是static的?

  • 答:因为对于单例模式,你得到对象的唯一方式就是这个接口。那在得到对象之前,你没有对象,怎么调用一个类的函数呢?我们知道,static函数属于一个类,而不属于任何特定的对象,因此,需要使用static类型来实现这个接口,以便在没有对象的时候,调用一个类的函数。

  • 为什么这个唯一的对象需要设计成static类型的?

  • 答:因为前面我们设计一个static的函数来获取对象,而static成员函数只能访问static类型的成员。因此,我们不得已只能将这个对象设计成static类型的了。

如果设计成非static,编译器就会报错了:

在这里插入图片描述

  • 为什么需要把拷贝构造函数和拷贝赋值函数设计成delete的?
    答:因为我们只需要一个对象。允许使用两个函数来生产出其他的对象。
  • 其余需要注意的是static对象需要类外初始化。类内只是声明。

补充:双重检测的懒汉式:

#include <iostream>
#include <mutex>

using namespace std;

class Singleton {
public:
private:
	static Singleton* instance;
	Singleton() {};
	Singleton(const Singleton&) = delete;
	Singleton& operator=(const Singleton&) = delete;

public:
	static Singleton* getInstance() {
		mutex mtx_;
		if (instance == nullptr) {
			unique_lock<mutex> lock(mtx_);
			if (instance == nullptr) {
				instance = new Singleton();
			}
			lock.unlock();
		}
		return instance;
	}
};
Singleton* Singleton::instance = nullptr;

2.2 懒汉式的单例模式

class Singleton {
public:
	static Singleton* getInstance() {
		static Singleton instance;  //在函数里面定义局部对象,运行到这一句才产生对象
		return &instance;
	}
private:
	Singleton() {};
	Singleton(const Singleton&) = delete;
	Singleton& operator=(const Singleton&) = delete;
};

对于懒汉式的单例模式,我们只需要注意把这个static对象放入函数,成为一个静态的局部对象就可以了,这样在运行到这个语句的时候才进行对象的初始化。

此外,由于饿汉式的单例不是局部的静态变量,因此在main函数执行之前,对象就已经初始化完成了,所以不需要考虑线程安全的问题。而对于懒汉式的单例,是运行到定义的语句才进行初始化,那么,有没一种可能:两个线程同时进入getInstance函数,把我们的对象初始化两次?

回答:有可能,但static对象由编译器保证不会初始化两次,第二次初始化不做操作。因此,这种写法是线程安全的。

3. 基于单例模式实现线程安全的日志模块

在这里插入图片描述

  1. 我们首先设计了一个线程安全的队列。在入队的时候,需要用互斥锁来进行加锁。这把互斥锁使用了C++11中的std::lock_guard<std::mutex> lock(mutex)。主要是用来互斥其他线程(写日志线程拿数据Pop、入队Push),而在离开作用域时可以自动释放这把锁。在写入数据后,就应该通知写日志的线程来拿数据,因此,在入队完成之后,我们还会调用notify_one函数把写日志的线程唤醒。而对于写日志的线程来说,需要用到C++11中的 std::unique_lock<std::mutex> lock(mutex)函数。 不用lock_guard的原因是unique_lock函数提供了锁的lock和unlock操作,而lock_guard没有。如果队列为空的话,我们就使用m_condvariable.wait(lock)把这个写日志的线程阻塞休眠,等到入队线程消息的到来。

  2. 我们把日志系统设置成为了单例的模式,因为写日志只需要一个专门的对象完成就可以了。在日志系统的构造函数中,首先会开启一个线程,这个线程会不断的Pop出消息,把日志信息写到磁盘IO中。

头文件logger.h:

#pragma once
#include<string>
#include "lockqueue.h"
#include <iostream>

enum LogLevel{
    INFO, // 普通信息
    ERRO  // 错误信息
};

class Logger {
public:
    // 获取日志的单例
    static Logger& GetLoggerInstance();
    // 设置日志级别
    void setLogLevel(LogLevel level);
    // 写日志
    void Log(const std::string msg);

private:
    // 日志级别
    int m_loglevel;
    // 日志缓冲队列
    LockQueue<std::string> m_lckQue;

    // 设置成单例模式
    Logger();
    Logger(const Logger&) = delete;
    Logger& operator=(const Logger&) = delete;
};

#define LOG_ERR(logmsgformat, ...) \
    do \
    {  \
        Logger &logger = Logger::GetLoggerInstance(); \
        logger.setLogLevel(ERRO); \
        char c[1024] = {0}; \
        snprintf(c, 1024, logmsgformat, ##__VA_ARGS__); \
        logger.Log(c); \
    } while(0) \

// 定义宏
#define LOG_INFO(logmsgformat, ...) \
    do \
    { \
        Logger& logger = Logger::GetLoggerInstance(); \
        logger.setLogLevel(INFO); \
        char c[1024] = {0}; \
        snprintf(c, 1024, logmsgformat, ##__VA_ARGS__); \
        logger.Log(c); \
    } while (0);

源文件logger.cpp:

#include "logger.h"
#include "time.h"
#include <iostream>

 // 获取日志的单例
Logger& Logger::GetLoggerInstance() {
    static Logger logger;
    return logger;
}

// 启动专门的写日志线程
Logger::Logger() {
    std::thread writeLogTask(
        [&](){
            for (;;) {
                // 获取日期 从队列中获取日志信息 追加到文件中
                time_t now = time(nullptr);
                tm* nowtm = localtime(&now);

                char filename[128] = {0};
                sprintf(filename, "%d-%d-%d-log.txt", 
                                    nowtm->tm_year + 1900, nowtm->tm_mon + 1, nowtm->tm_mday);

                FILE* pf = fopen(filename, "a+");
                if (nullptr == pf) {
                    std::cout << "logger file " << filename <<  " open error" << std::endl; 
                    exit(EXIT_FAILURE);
                }
                // 插入时间前缀
                char time_buf[128] = {0};
                sprintf(time_buf, "%d:%d:%d => [%s]", 
                                    nowtm->tm_hour, 
                                    nowtm->tm_min, 
                                    nowtm->tm_sec,
                                    (m_loglevel == INFO) ? "info" : "error");
                std::string msg = m_lckQue.Pop();
                msg.insert(0, time_buf);
                msg.append("\n");
                fputs(msg.c_str(), pf);
                fclose(pf);
            }
        }
    );
    // 设置线程分离
    writeLogTask.detach();
}
// 设置日志级别
void Logger::setLogLevel(LogLevel level) {
    m_loglevel = level;
}

// 写日志 将日志放到lockqueue缓冲队列中
void Logger::Log(const std::string msg) { 
    m_lckQue.Push(msg);
}    

我们还实现了一个线程安全的队列lockqueue.h:

#pragma once
#include <queue>
#include <mutex>
#include <thread>
#include <condition_variable>

// 异步写日志的缓冲队列
template<typename T>
class LockQueue {
public:
    // 多个线程都会把日志写入缓冲队列
    void Push(const T& data) {
        std::lock_guard<std::mutex> lock(m_mutex);
        m_queue.push(data);
        m_condvariable.notify_one();
    }
    // 一个线程负责取出缓冲队列的日志 写入磁盘I/O中
    T Pop() {
        std::unique_lock<std::mutex> lock(m_mutex);
        while (m_queue.empty()) {
            // 日志队列为空 进入等待状态
            m_condvariable.wait(lock);
        }
        T data = m_queue.front();
        m_queue.pop();
        return data;
    }
private:
    std::queue<T> m_queue;
    std::mutex m_mutex;
    std::condition_variable m_condvariable;
};
C++常用设计模式:单例设计模式饿汉式和懒汉式详细说明 C++常用设计模式:单例设计模式饿汉式和懒汉式详细介绍 阅读详情

相关推荐

C++单例模式深度解析:懒汉式与饿汉式的实现与线程安全

单例模式是软件设计中确保类只有一个实例并提供全局访问点的创建型模式。其核心原理是通过私有化构造函数和静态成员控制实例化过程,实现资源的统一管理与状态共享。该模式的技术价值在于优化资源使用、避免重复创建开销,并保证全局状态的一致性。在C++工程实践中,单例模式广泛应用于配置管理、日志系统、线程池及数据库连接池等场景。针对多线程环境下的线程安全挑战,开发者需在懒汉式的延迟初始化与饿汉式的启动即加载之间权衡,并合理运用互斥锁、双重检查锁定及std::call_once等同步机制,以规避静态初始化顺序与对象析构难题

weixin_30599769的博客 381

日志系统的设计模式

从事服务器程序开发的工程师们,几乎不可避免地需要使用到日志系统(Logging System)。如果从头开发一个服务器程序,我们有若干开源的日志系统可供选择,例如:Java语言中,Log4j是个不错的选择;Python语言中,有logging类;C语言中,有:Log4c、libqb等。这些开源的日志系统,几乎都使用到相同的设计模式,不同之处在于实现语言、实现方法,以及附加的一些额外功能。今天,我想

Chinainvent的专栏 1万+

C++实现饿汉式单例模式的完整教程

总结:饿汉式单例模式是一个简单而优雅的解决方案,它在程序启动时就已经创建了单例对象,避免了线程安全问题,且使用方便。但同时需要注意可能存在的空间浪费问题(即在某些情况下,对象不一定需要被创建),这个问题也可以通过懒汉式单例模式进行解决。在面向对象编程中,单例模式是一种经常使用的设计模式之一,它确保一个类只能创建一个对象。在C++中,实现单例模式有多种方法,其中最常见的是饿汉式和懒汉式。由于我们要实现饿汉式单例模式,因此在类被加载时就需要初始化该指针变量,这可以通过在类定义外部初始化静态成员变量来完成。

CodeWhizZ的博客 506

C++常用设计模式:单例设计模式(饿汉式、懒汉式)

介绍了饿汉式单例设计模式和懒汉式单例设计模式,有简单的代码演示及结果、会出现的问题及解决方式,有需要的可以参考下。

weixin_57535837的博客 5916

告别激活烦恼:Lattice Diamond 3.13 永久免费License申请与配置全攻略(Windows 64位)

本文详细介绍了Lattice Diamond 3.13永久免费License的申请与配置全流程,特别针对Windows 64位系统用户。从官网导航的隐藏路径到Host ID的获取方式,再到License文件的高级处理技巧和环境变量的深度配置,提供了全面的安装教程和疑难问题解决方案,帮助开发者彻底告别激活烦恼。

weixin_28736335的博客 425

从原理图到PCB:基于Allegro的8槽PXIe背板高速设计实战

本文详细介绍了基于Allegro软件进行8槽PXIe背板高速设计的完整实战流程。从理解规范、原理图设计、PCB布局规划,到高速PCIe差分对与精密时钟的布线艺术,再到电源与地处理以及最终的设计验证与生产文件输出,系统性地阐述了确保背板信号完整性、电源完整性和系统稳定性的关键技术与工程实践。

weixin_28312391的博客 203

设计模式实战:C++ 中工厂模式与单例模式的深度应用

本章深入探讨了C++中工厂模式与单例模式的核心应用。工厂模式部分重点讲解了简单工厂、工厂方法和抽象工厂三种实现方式,通过计算器运算器和日志系统等案例,展示了如何实现对象创建与使用的分离。单例模式部分分析了饿汉式和懒汉式两种实现,并讨论了线程安全优化方案。文章强调设计模式的核心原则(如开放-封闭原则、单一职责原则),并指出实际应用中需避免过度设计和线程安全问题。这些模式能有效解决代码复用、扩展性和维护性问题,是编写高质量C++代码的重要工具。

今天比昨天更努力一点! 1万+

设计模式-单例模式

在 C++中,单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来访问这个实例。

wesigj的博客 1254

从0到1:.NET 9 OpenAPI全攻略!Swagger vs Scalar UI谁更强?

OpenAPI:.NET 9的智能文档革命 本文深入解析.NET 9的OpenAPI支持,对比Swagger UI与Scalar UI的优劣,提供实战代码示例: 一键生成:AddOpenApi()+MapOpenApi()快速生成标准文档 UI工具对决: Swagger UI功能全面但复杂 Scalar UI简约现代,原生支持cURL/HTTPie 进阶技巧: XML注释增强文档可读性 自定义Transformer实现文档动态修改 🔥核心价值:.NET 9的OpenAPI方案显著降低API文档维护成本,开

java专栏 1427

Vivado 2023.1及更早版本永久License全功能解锁:HLS、System Generator等IP核使用详解

本文深入解析Vivado 2023.1及更早版本的全功能许可证,详细指导如何解锁HLS、System Generator等高级IP核。通过许可证类型对比、功能验证方法和实战应用指南,帮助FPGA开发者充分利用Vivado工具套件,提升复杂系统设计和算法加速的效率。

weixin_31286719的博客 379

C++奇迹之旅:C++的单例模式

单例模式在 C++ 中通过控制实例化过程确保全局唯一性。现代 C++ 推荐使用局部静态变量实现(Meyers’ Singleton),既简洁又线程安全。需根据实际需求权衡懒汉式与饿汉式,并注意避免滥用单例导致代码耦合性增加。

a_sen的博客 1262

STM32 使用IO口模拟I2C时序

上一篇《I2C协议详解》 我们了解了I2C的操作流程,这一篇,我们就使用I2C,来对EEPROM进行操作吧。 我们做两种选择: 1.时序由IO口模拟高低电平,需要了解协议并按照协议操作相应的IO口。 2.时序由硬件自行产生,不需要人工干预; 由硬件产生的I2C时序,我们借助Stm32Cube配置实现便可,我们这一篇,抛开Stm32Cube,手撕代码,根据I2C的时序,一步步地实现I2C对...

8937

单例模式与阻塞队列实现异步的日志系统

单例模式与阻塞队列实现异步的日志系统阻塞队列定义实现(c++11)单例模式定义类型懒汉式(编程使用)饿汉式C++中static对象的初始化non-local static对象(函数外)local static 对象(函数内)异步写日志 阻塞队列 定义 在多线程编程中阻塞队列是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出 在c++标准库STL中常用

qq_41895640的博客 2259

Claude Code 完整详解(Anthropic 官方 AI 编程 Agent)

它是运行在终端(Terminal)的 AI 编程代理,不是普通 IDE 插件;你用自然语言发指令,它自主规划步骤、读写项目文件、执行系统命令、排查 Bug、处理 Git 工作流,是能真正动手干活的 AI 程序员。真正的AI 开发代理人,不止写代码,还能执行开发全流程超长全局上下文,大型项目理解碾压传统补全工具终端原生 + IDE 双接入,开发流程无割裂代码质量、可读性、工程规范性普遍偏高权限安全机制完善,修改前预览、可随时中断回滚。

wange6906的博客 3156

工程师必收藏,Juniper(Junos)网络设备命令大全

不要直接删除配置 → 使用deactivate配置前先修改大配置用出现风险命令一律用写自动化时一律用格式真正会用 Juniper 的工程师,不是命令多,而是命令用得精。

网络技术联盟站 403

设计模式:单例模式(使用c++分别实现饿汉式单例、带双重检查锁定机制的懒汉式单例以及IoDH技术的单例)

设计模式:单例模式(使用c++分别实现饿汉式单例、带双重检查锁定机制的懒汉式单例以及IoDH技术的单例)

李白 1039

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

[设计模式]C++单例模式的几种写法以及通用模板

本文详细介绍了C++单例模式从C++98到C++11的演进过程,对比分析了不同实现方式的优缺点。首先阐述了单例模式的核心价值:控制实例数量和提供全局访问点。然后重点讲解了四种实现方式:局部静态变量(C++11推荐)、饿汉式、懒汉式+双重检查锁定(存在缺陷)以及基于C++11 once_flag和智能指针的现代方案。最后展示了基于CRTP的通用单例模板这一企业级最佳实践,通过模板元编程实现代码复用。文章为开发者提供了从基础到高级的单例模式实现指南,特别强调了线程安全和内存管理的重要性。

qq_44846784的博客 5330

Devexpress16.2.6破解工具

Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具

efm32 入门.pdf

本PDF共计22页,介绍的是操作驱动API,下载与否自己斟酌。

C++单例模式

本文介绍单例模式,包括饿汉式、懒汉式、线程安全的懒汉式,以及扩展了模板单例模式的写法与测试

2401_84784737的博客 981

大中型超市收银系统(可直接运行)

光步前台收银系统系统功能:商品销售、商品退货、挂单、收银员权限、会员销售、单品取消、整单取消、商品查询、脱机销售、脱机数据上传,基本资料下载,开钱箱、小票打印、收款结算,支持多币种付款方式(现金、储值卡、礼券、微信等),支持多种促销。运行环境:.net 4.0登陆信息用户名:1001 密码:123456内置商品信息,商品编码可输入00001000 - 00010000,测试效果相关功能键F5 结算 F9挂单 F11商品查询 F8退货 Insert单品取消 Del整单取消 F12开钱箱F2 会员 Ctrl+Z退出系统 Home 锁屏 相关源码待公开

深入理解C++单例模式:实现、线程安全与应用场景

确保类在整个系统中仅有一个实例(唯一性),以及提供一个全局统一的访问入口(访问性)。要实现这两个目标,单例模式通过两大核心机制构建其独特结构,同时在类设计中体现出清晰的成员职责划分。单例模式并非“银弹”,其适用场景需满足资源独占且生命周期与应用一致的条件(如配置管理器、硬件控制器)。对于需要频繁扩展、多状态测试或低耦合设计的场景,应优先考虑依赖注入、工厂模式等替代方案。记住:设计模式的价值在于解决特定问题,而非盲目套用——理解其优缺点,才能让单例模式真正成为开发助力而非负担。

qq_40343899的博客 1272

C++ 设计模式实践的完整详解与实战指南,重点聚焦于工厂模式(简单工厂、工厂方法、抽象工厂)和 单例模式(饿汉式、懒汉式、线程安全优化)

适用场景:需要同时创建一组相关对象(如 UI 组件族:按钮 + 文本框 + 窗口)Qt 项目真实案例:跨平台 UI 组件族完整代码(简版)public:public:public :public :} };();();项目阶段推荐模式组合为什么推荐小型工具单例 + 简单工厂实现最快中型桌面应用工厂方法 + 策略 + 单例扩展性好,代码清晰大型框架/插件抽象工厂 + 插件 + 单例支持跨平台、动态加载。

专注于前沿技术的探索与实践,热衷于从代码逻辑、架构设计到行业应用等多维度深挖技术本质。期待在技术之路上与你共同成长~ 968

单例模式 C++

懒汉,饿汉,解决内存泄漏,双检锁,原子,C++11 magic static,模板类

IT OR 考公? 2204

设计模式:单例模式

单例模式是一种确保类仅有一个实例并全局可访问的设计模式,常用于资源共享场景。常见实现方式包括:懒汉式(线程不安全或加锁保证安全)、饿汉式(启动即初始化)、Meyers'单例(C++11静态局部变量线程安全)、双重检查锁定(DCLP)。线程安全可通过加锁、原子操作或C++11的静态局部变量机制实现,其中Meyers'单例因其简洁性和安全性被推荐为最佳实践。该模式通过私有构造函数和静态访问方法保证实例唯一性,在多线程环境下需特别注意初始化竞态问题

weixin_43414962的博客 702

c++单例模式总结

单例模式(Singleton Pattern)是一种创建型设计模式,它的核心思想是确保一个类在任何时刻都只有一个实例,并提供一个全局访问点来获取这个唯一的实例。简单来说,当你的程序中某个对象只需要存在一份,并且所有地方都需要访问同一个对象时,单例模式就派上用场了。

weixin_45878585的博客 755

设计模式之单例模式

单例模式是一种创建型模式,它确保一个类在整个程序运行期间只有一个实例,并提供一个全局访问点。

m0_46967529的博客 825

c++ 单例模式

单例模式(SingletonPattern)是一种常用的程序设计模式,属于创建型设计模式的一种。它的核心思想在于控制类的实例化过程,确保在任何情况下,对于特定类来说,在整个应用程序中只存在一个实例,并提供一个全局访问点来获取这个实例。这样做可以确保共享资源的有效管理,减少系统开销,以及在需要全局访问和协调控制时提供便利。唯一性:确保一个类只有一个实例。全局访问点:提供一个全局访问的方法,让其他对象可以轻松地获取到这个唯一的实例。

weixin_45672157的博客 1324
上一篇: epoll比select和poll究竟高效在哪儿(剖析及实现)
下一篇: shared_ptr的线程安全性探讨
zsiming
博客等级 码龄7年 43粉丝 74原创
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值