单例模式
单例模式是一种常用的设计模式,其核心是确保一个类在全局只有唯一实例,并提供一个全局访问点。
一、核心原理
- 限制实例化:通过私有化类的构造函数、拷贝构造函数和赋值运算符,禁止外部直接创建实例或复制实例。
- 唯一实例:在类内部维护一个静态的自身实例指针,确保全局只有一个实例。
- 全局访问:提供一个静态的公开接口(如
getInstance()
),让外部通过该接口获取唯一实例。
二、常见的单例模式实现方式
1. 懒汉式(Lazy Initialization)
实例在第一次被使用时才创建(延迟初始化),节省资源。
#include <QMutex>
#include <QScopedPointer>
class Singleton {
public:
// 全局访问点:获取唯一实例
static Singleton& getInstance() {
// 双重检查锁定(DCLP),避免多线程下重复创建
if (m_instance.isNull()) {
QMutexLocker locker(&m_mutex); // 加锁,确保线程安全
if (m_instance.isNull()) {
m_instance.reset(new Singleton()); // 首次调用时创建实例
}
}
return *m_instance;
}
// 示例:单例提供的功能方法
void doSomething() {
// ... 业务逻辑 ...
}
private:
// 私有化构造函数:禁止外部创建实例
Singleton() {}
// 私有化拷贝构造和赋值运算符:禁止复制
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
// 静态成员:存储唯一实例
static QScopedPointer<Singleton> m_instance;
static QMutex m_mutex; // 互斥锁,确保多线程安全
};
// 初始化静态成员(类外定义)
QScopedPointer<Singleton> Singleton::m_instance(nullptr);
QMutex Singleton::m_mutex;
2. 饿汉式(Eager Initialization)
实例在程序启动时(类加载时)就创建,避免多线程同步问题,但可能提前占用资源。
class Singleton {
public:
// 全局访问点:直接返回预创建的实例
static Singleton& getInstance() {
static Singleton instance; // 静态局部变量,程序启动时初始化
return instance;
}
void doSomething() {
// ... 业务逻辑 ...
}
private:
// 私有化构造函数
Singleton() {}
// 禁止复制
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
三、关键实现细节解析
私有化构造函数
private
权限的构造函数阻止外部通过new Singleton()
或Singleton obj
创建实例,确保实例只能在类内部创建。禁止复制和赋值
通过= delete
显式删除拷贝构造函数和赋值运算符,避免外部通过Singleton obj = Singleton::getInstance()
复制实例,保证唯一性。静态实例与全局访问
类内部的静态成员(m_instance
或静态局部变量)存储唯一实例,getInstance()
静态方法提供全局访问入口,确保任何地方都能获取同一个实例。线程安全处理
- 懒汉式中使用
QMutex
加锁,避免多线程同时调用getInstance()
时创建多个实例(双重检查锁定进一步优化性能)。 - 饿汉式依赖静态变量的初始化特性(C++11 后静态局部变量初始化是线程安全的),无需额外加锁。
- 懒汉式中使用
四、单例模式的适用场景与特点
使用场景
- 全局配置管理(如程序的配置类)。
- 设备管理器(如硬件设备的唯一控制实例)。
- 缓存管理器(避免重复创建缓存对象)。
日志工具(确保日志写入的唯一性)。
//ErrorLogger.h文件
#ifndef ERRORLOGGER_H
#define ERRORLOGGER_H
#include <QString>
#include <QMutex>
// 错误日志工具类(单例模式)
class ErrorLogger
{
public:
// 获取单例实例
static ErrorLogger& getInstance();
// 禁止拷贝和赋值
ErrorLogger(const ErrorLogger&) = delete;
ErrorLogger& operator=(const ErrorLogger&) = delete;
// 写入错误日志
void writeLog(const QString& errorMessage);
// 设置日志文件路径(默认当前目录下的error.log)
void setLogFilePath(const QString& path);
private:
// 私有构造函数(单例模式)
ErrorLogger();
QString m_logFilePath; // 日志文件路径
QMutex m_mutex; // 互斥锁,确保多线程安全
};
#endif // ERRORLOGGER_H
//ErrorLogger.cpp文件
#include "ErrorLogger.h"
#include <QFile>
#include <QTextStream>
#include <QDateTime>
#include <QDir>
#include <QDebug>
ErrorLogger::ErrorLogger()
{
// 默认日志路径:当前程序目录下的error.log
m_logFilePath = QDir::currentPath() + "/error.log";
}
ErrorLogger& ErrorLogger::getInstance()
{
static ErrorLogger instance;
return instance;
}
void ErrorLogger::setLogFilePath(const QString& path)
{
m_logFilePath = path;
}
void ErrorLogger::writeLog(const QString& errorMessage)
{
// 多线程加锁,避免日志写入冲突
QMutexLocker locker(&m_mutex);
// 获取当前时间戳(格式:yyyy-MM-dd hh:mm:ss)
QString timeStamp = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
// 构建日志内容(时间 + 错误信息)
QString logContent = QString("[%1] Error: %2\n").arg(timeStamp).arg(errorMessage);
// 打开文件(以追加模式,不存在则创建)
QFile file(m_logFilePath);
if (!file.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text)) {
qDebug() << "无法写入日志文件:" << file.errorString();
return;
}
// 写入日志
QTextStream out(&file);
out << logContent;
file.close();
}
//main.cpp文件
// 写入错误日志(包含系统错误信息)
ErrorLogger::getInstance().writeLog(
QString("文件打开失败: %1(路径:%2)")
.arg(file.errorString())
.arg(file.fileName())
);
// 可选:设置自定义日志路径(如程序数据目录)
ErrorLogger::getInstance().setLogFilePath(
QDir::homePath() + "/myapp/logs/error.log"
);
spdlog第三方库实现
链接: C++日志记录库SPDLog简介
spdlog学习—安装及基本使用
spdlog是一个高性能、超快速、零配置的C++日志库,它旨在提供简洁的API和丰富的功能,同时保持高性能的日志记录。它支持多种输出目标、格式化选项、线程安全以及异步日志记录。
- 高性能:spdlog专为速度而设计,即使在高负载情况下也能保持良好的性能
- 零配置:无需复杂的配置,只需包含头文件即可在项目中使用
- 异步日志:支持异步日志记录,减少对主线程的影响
- 格式化:支持自定义日志消息的格式化,包括时间戳、线程ID、日志级别等
- 多平台:跨平台兼容,支持Windows、Linux、MacOS等操作系统
- 丰富的API:提供丰富的日志级别和操作符重载,方便记录各种类型的日志
- 多目标输出:可以将日志输出到控制台、普通文本文件、循环写入文件(rotating log files)、每日生成新文件(daily logs)、系统日志等目标,同时也支持异步写入以提高性能。
- 丰富的日志级别:Spdlog 支持常见的日志级别,如 TRACE、DEBUG、INFO、WARN、ERROR、CRITICAL 等,用户可以根据需要选择不同级别的日志输出。
- 条件日志:根据预定义的条件开关,可以动态启用或禁用特定级别的日志输出。
使用步骤
spdlog库的使用也非常简单,只需要下载源代码,然后把根目录下的include目录下的文件拷贝到我们的工程下,在工程中包含相应的头文件即可。
- 控制台打印
#include <spdlog/spdlog.h>
#include <string.h>
#include <iostream>
int main()
{
// 普通打印
spdlog::info("Welcome to info spdlog!");
// 格式化打印
// 打印字符串
spdlog::info("Hello World {}", "spdlog!");
// 打印数字
spdlog::error("spdlog errCode : {}", -10020);
// 指定打印数字的占位符
spdlog::warn("spdlog format char {:08d}", 12);
// 格式化打印不同进制的数据
spdlog::critical("Support for int:{0:d} hex:{0:x} oct:{0:o} bin:{0:b}", 42);
// 打印浮点型数据
spdlog::info("float args are {:03.2f}", 1.23456);
// 打印多个参数
spdlog::info("string args are {0} {1}..", "too", "supported");
spdlog::info("number args are {0} {1} {2}..", 10020, 10040, -100);
system("pause");
}
- 在文件中打印日志
#include <spdlog/spdlog.h>
#include <spdlog/sinks/basic_file_sink.h>
#include <string.h>
#include <iostream>
int main()
{
try
{
// 参数1 日志标识符, 参数2 日志文件名
std::shared_ptr<spdlog::logger> mylogger = spdlog::basic_logger_mt("spdlog", "spdlog.log");
// 设置日志格式. 参数含义: [日志标识符] [日期] [日志级别] [线程号] [数据]
mylogger->set_pattern("[%n][%Y-%m-%d %H:%M:%S.%e] [%l] [%t] %v");
mylogger->set_level(spdlog::level::debug);
spdlog::flush_every(std::chrono::seconds(5)); // 定期刷新日志缓冲区
mylogger->trace("Welcome to info spdlog!");
mylogger->debug("Welcome to info spdlog!");
mylogger->info("Welcome to info spdlog!");
mylogger->warn("Welcome to info spdlog!");
mylogger->error("Welcome to info spdlog!");
mylogger->critical("Welcome to info spdlog!");
// 刷新
mylogger->flush_on(spdlog::level::debug);
}
catch (const spdlog::spdlog_ex& ex)
{
std::cout << "Log initialization failed: " << ex.what() << std::endl;
}
system("pause");
}
执行结果。执行程序后就会在当前目录下生成一个spdlog.log文件,看下打印内容
特点
优点:
确保全局唯一实例,减少资源消耗(如频繁创建销毁实例的开销),提供统一的访问点。缺点:
单例本质是全局变量,可能导致代码耦合度升高;测试困难(单例状态难以隔离);在多线程环境下需谨慎处理同步问题。
通过上述实现,单例模式能有效控制类的实例数量,在需要全局唯一访问点的场景中非常实用。