设计模式:饿汉式和懒汉式单例模式(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

山东大学软件工程应用与实践——GMSSL开源库(六)——SM9密钥交换协议

2021SC@SDUSC 文章目录基本概念符号及辅助算法SM9密钥交换协议 基本概念 1976年,Diffie和Hellman在其题为《密码学的新方向》的论文中首次给出了公钥密码学的定义,并提出了Diffie-Hellman密钥交换算法。该算法解决了对称密码体制中的密钥分发问题,使得通信双方可以通过公开信道安全地交换共享密钥。 使用SM9通信的双方通过对方的标识和自身的私钥,经过两次或者可选的三次信息传递过程,计算获取一个由双方共同决定的共享秘密密钥。该密钥可以作为对称密码算法的会话密钥,其中可选步骤用来实

m0_51588039的博客 1988

IEEE GRSL投稿全流程避坑指南:从Latex模板到校样缴费,新手必看的7个关键节点

本文详细解析了IEEE GRSL投稿全流程中的7个关键节点,从LaTeX模板选择到校样缴费,帮助新手避免常见陷阱。特别针对图像格式转换、审稿意见回复、版权转移等环节提供实用技巧,确保投稿过程高效顺利。

weixin_28720573的博客 570

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

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

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

射频工程师笔记---射频通信基础

回顾一下移动通信技术的发展,其实是互联网和通信技术的融合过程。在这个过程中,很多应用都在不断加入其中。比如计算机跟通信的融合产生了互联网、互联网跟手机的融合带来了移动互联网。手机可以看杂志、看视频、听音乐,于是又出现了内容服务产业;再后来,电视音箱等设备也能联网,于是网络跟家电又产生了融合。所以应用的出现总是在融合其他产业。在1995年,比尔·盖茨在《未来之路》一书中首次提出:在未来...

多年硬件从业经历,热爱学习,记录生活! 2万+

设计模式-单例模式

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

wesigj的博客 1254

1.2 固态变压器:传统工频变压器的局限与SST的技术起源

本小节明确传统工频变压器在电力系统中的基础定位与核心工作特性,系统分析其在新型电力系统场景下的固有技术瓶颈,梳理固态变压器(Solid State Transformer, SST,又称电力电子变压器Power Electronic Transformer, PET)从概念提出到工程化成熟的完整演进路径,为后续SST技术优势、设计需求与规范符合性要求的讲解建立底层逻辑,同时对应GB/T 40429-2021《电力电子变压器通用技术要求》的需求制定背景。

技术工作者 658

SAP 系统里生成 PDF 的几条主干路径:从 Spool 转换到 Adobe 原生渲染,再到 BTP 云服务

本文系统梳理了SAP生态中PDF生成的多种技术方案,包括Spool转换、OTF转换、Smart Forms/Adobe Forms原生生成以及云端服务等不同路径。文章通过实际案例和代码示例,详细分析了各方案的适用场景、技术要点和工程实践建议,特别强调了多语言支持、字体处理、性能优化等关键因素。针对不同业务需求,提供了从简单导出到复杂交互式PDF的完整解决方案框架,为SAP环境下的PDF处理提供了实用指南。

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 287

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

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

a_sen的博客 1262

SQL注入详解

SQL注入(SQL Injection)是一种常见的Web安全漏洞,主要形成的原因是在数据交互中,前端的数据传入到后台处理时,没有做严格的判断,导致其传入的“数据”拼接到SQL语句中后,被当作SQL语句的一部分执行。 从而导致数据库受损(被脱库、被删除、甚至整个服务器权限陷)。 即:注入产生的原因是后台服务器接收相关参数未经过滤直接带入数据库查询...

渗透之路,攻防之间皆学问 26万+

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

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

qq_41895640的博客 2259

Transformer 面试题及详细答案120道(1-10)-- 基础概念

本文总结了Transformer模型的10个常见面试问题及答案,涵盖了其核心结构、创新点、工作机制等关键知识点。主要内容包括:Transformer的自注意力机制和多头注意力机制创新点,相比RNN/CNN的优势,模型在《Attention Is All You Need》论文中的提出背景,以及Encoder-Decoder协同工作流程等。重点解析了位置编码的作用、长距离依赖处理能力、与传统注意力机制的区别等核心问题。每个问题都给出了简明扼要的专业解答,适合作为Transformer相关面试准备的参考资料。

大剑师兰特的GIS世界 849

visio安装包

Microsoft Office Visio 是一款专业的制图软件。利用经过更新的形状、协作工具和数据链接图表,创建专业级图表以简化复杂的信息。

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

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

李白 1039

WINCC7.3破解文件

WINCC7.3硬件够破解文件,有需要的下载!WINCC7.3硬件够破解文件,有需要的下载!

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

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

qq_44846784的博客 5330

华为的C语言编程规范.pdf

华为技术有限公司内部技术规范。本规范制定了编写C语言程序的基本原则、规则和建议。从代码的清晰、简洁、可测试、安全、程序效率、可移植各个方面对C语言编程作出了具体指导。

IEM模型,地表粗糙度模拟

利用微波反演土壤水分时候,利用IEM模型可以实现对随机地表的模拟

C++单例模式

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

2401_84784737的博客 981

深入理解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、付费专栏及课程。

余额充值