日志系统设计 与 策略模式实现
目录
6、注意:关于多态(Polymorphism)和智能指针(std::unique_ptr)的使用
一、设计模式概述
在IT行业快速发展的背景下,软件开发领域涌现出大量开发者,技术水平参差不齐。为了帮助初级开发者更好地理解和实现常见问题的解决方案,资深开发者总结了一系列经典的设计模式。
设计模式是软件设计中常见问题的可重用解决方案,它们不是具体的代码实现,而是解决特定问题的模板或策略。设计模式主要分为三大类:
-
创建型模式:关注对象的创建机制(如工厂模式、单例模式)
-
结构型模式:处理对象间的组合关系(如适配器模式、装饰器模式)
-
行为型模式:描述对象间的通信方式(如策略模式、观察者模式)
本案例将重点讲解策略模式在日志系统设计中的应用。
二、日志系统基础
1、日志系统的重要性
计算机日志是记录系统和软件运行事件的专用文件,主要用于实时监控运行状态、追踪异常信息,便于快速定位故障并辅助程序员进行调试修复。作为系统维护的核心工具,日志在故障排查和安全管理中发挥着关键作用,其主要作用包括:
-
运行监控:实时跟踪系统状态
-
问题诊断:记录异常信息帮助快速定位问题
-
安全审计:追踪安全相关事件
-
性能分析:记录操作耗时等性能数据
2、日志格式规范
必须包含的字段
-
时间戳:精确到秒的事件发生时间(推荐格式:YYYY-MM-DD HH:MM:SS)
-
日志等级:表示日志严重程度(DEBUG/INFO/WARNING/ERROR/FATAL)
-
日志内容:实际记录的信息
可选字段
-
源代码位置:文件名和行号(FILE, LINE)
-
进程/线程信息:进程ID、线程ID
-
模块/功能标识:区分不同业务模块
-
会话/请求ID:追踪特定请求链路
3、现有日志解决方案
业界已有多种成熟的日志库:
-
C++: spdlog, glog, Boost.Log, Log4cxx
-
Java: Log4j, Logback, java.util.logging
-
Python: logging模块
本案例选择自定义实现,主要为了:
-
第一,深入理解日志系统内部机制
-
第二,演示策略模式的具体应用
-
第三,根据特定需求定制功能
三、策略模式详解
1、策略模式定义
策略模式(Strategy Pattern)是一种行为设计模式,它定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户端。
核心思想:
-
定义算法家族(抽象策略接口)
-
封装每个算法(具体策略实现)
-
客户端通过组合方式使用策略(上下文类)
2、在日志系统中的应用
在日志系统中,不同的日志输出方式(控制台、文件、网络等)可以视为不同的策略:
// 策略接口
class LogStrategy {
public:
virtual ~LogStrategy() = default;
virtual void SyncLog(const std::string &message) = 0;
};
// 具体策略:控制台输出
class ConsoleLogStrategy : public LogStrategy {
// 实现细节...
};
// 具体策略:文件输出
class FileLogStrategy : public LogStrategy {
// 实现细节...
};
3、预期日志格式
为优化日志设计,我们采用了策略模式。关于策略模式的详细介绍,请参考相关代码和博客内容。我们期望的日志格式如下:
[可读性很好的时间] [⽇志等级] [进程pid] [打印对应⽇志的⽂件名][⾏号] - 消息内容,⽀持可变参数
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [16] - hello world
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [17] - hello world
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [18] - hello world
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [20] - hello world
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [21] - hello world
[2024-08-04 12:27:03] [WARNING] [202938] [main.cc] [23] - hello world
四、日志系统实现解析
1、头文件结构
#ifndef __LOG_HPP__
#define __LOG_HPP__
// 系统头文件
#include <iostream>
#include <cstdio>
#include <string>
#include <filesystem> // C++17
#include <sstream>
#include <fstream>
#include <memory>
#include <ctime>
#include <unistd.h>
// 自定义模块
#include "Mutex.hpp"
namespace LogModule {
// 实现细节...
}
#endif
2、策略模式实现
1. 策略基类
class LogStrategy {
public:
virtual ~LogStrategy() = default;
virtual void SyncLog(const std::string &message) = 0;
};
这段代码定义了一个名为 LogStrategy 的抽象基类(Abstract Base Class),用于实现策略模式(Strategy Pattern)中的策略接口。
1. 类定义与目的
class LogStrategy {
public:
virtual ~LogStrategy() = default;
virtual void SyncLog(const std::string &message) = 0;
};
-
作用:定义日志记录的策略接口,允许不同的日志记录方式(如文件日志、控制台日志、网络日志等)通过继承该类实现多态行为。
-
设计模式:这是策略模式的典型应用,将算法(这里是日志记录方式)封装为对象,便于动态切换。
2. 成员函数解析
(1) 虚析构函数
virtual ~LogStrategy() = default;
关键点:
-
声明为
virtual,确保通过基类指针删除派生类对象时,能正确调用派生类的析构函数(避免资源泄漏)。 -
= default表示使用编译器生成的默认析构函数(简洁且高效)。
为什么需要:如果基类析构函数非虚,通过基类指针 delete 派生类对象时,只会调用基类的析构函数,导致派生类部分未被正确释放。(重点!!!)
(2) 纯虚函数 SyncLog
virtual void SyncLog(const std::string &message) = 0;
关键点:
-
= 0表示这是一个纯虚函数,LogStrategy成为抽象类,不能直接实例化。 -
派生类必须实现此函数,提供具体的日志记录逻辑。
参数:接收一个 const std::string& 类型的消息,表示待记录的日志内容。
设计意图:强制所有派生策略实现统一的日志接口,但内部行为可以不同(如写入文件、发送到服务器等)。
2. 具体策略实现
控制台日志策略:
class ConsoleLogStrategy : public LogStrategy {
public:
ConsoleLogStrategy() = default;
void SyncLog(const std::string &message) override {
LockGuard lockguard(_mutex); // 线程安全
std::cout << message << gsep;
}
private:
Mutex _mutex; // 互斥锁保证线程安全
};
这段代码定义了一个具体的日志策略类 ConsoleLogStrategy,继承自 LogStrategy,用于将日志输出到控制台。
1. 类定义与继承关系
class ConsoleLogStrategy : public LogStrategy {
public:
ConsoleLogStrategy() = default;
void SyncLog(const std::string &message) override;
private:
Mutex _mutex;
};
-
继承:通过
public LogStrategy继承基类的纯虚接口,表明这是一个具体的策略实现。 -
构造函数:
= default表示使用编译器生成的默认构造函数,无需额外初始化逻辑。 -
线程安全:通过
Mutex _mutex和LockGuard实现多线程环境下的安全输出。
2. 关键实现:SyncLog 方法
void SyncLog(const std::string &message) override {
LockGuard lockguard(_mutex); // 加锁
std::cout << message << gsep; // 输出日志
}
(1) 线程安全设计
-
Mutex _mutex:成员变量,用于保护共享资源(这里是std::cout)。 -
LockGuard(我们可以想象是std::lock_guard<Mutex>的别名,但是我这里使用的是自己已经封装好了的Mutex.hpp):-
在构造时自动加锁(
_mutex.lock())。 -
在作用域结束时(如函数退出)自动解锁(通过 RAII 机制)。
-
避免手动管理锁导致的死锁或忘记解锁问题。
-
(2) 日志输出
-
std::cout:标准输出流,将日志打印到控制台。 -
gsep(在代码中已经定义,是全局变量):-
为分隔符(如
"\r\n"),用于格式化日志。 -
也可以改为局部常量或通过参数配置,减少全局状态依赖。
-
文件日志策略:
// 文件打印日志的策略 : 子类
const std::string defaultpath = "./log";
const std::string defaultfile = "my.log";
class FileLogStrategy : public LogStrategy {
public:
// 构造函数
FileLogStrategy(const std::string &path = defaultpath,
const std::string &file = defaultfile)
: _path(path), _file(file) {
// 初始化日志目录
if (!std::filesystem::exists(_path)) {
try {
std::filesystem::create_directories(_path);
} catch (const std::filesystem::filesystem_error &e) {
std::cerr << e.what() << '\n';
}
}
}
// 实现基类的纯虚函数
void SyncLog(const std::string &message) override {
LockGuard lockguard(_mutex);
std::string filename = _path + (_path.back() == '/' ? "" : "/") + _file;
std::ofstream out(filename, std::ios::app); // 追加模式
if (out.is_open()) {
out << message << gsep;
out.close();
}
}
private:
std::string _path; // 日志文件所在路径
std::string _file; // 日志文件名
Mutex _mutex; // 互斥锁,保证线程安全
};
FileLogStrategy 是日志模块中实现文件日志策略的类,继承自 LogStrategy 基类。
成员变量
-
_path: 存储日志文件的目录路径:默认值为"./log"(由defaultpath常量定义) -
_file: 存储日志文件名:默认值为"my.log"(由defaultfile常量定义) -
_mutex: 互斥锁对象:用于保证多线程环境下文件写入的线程安全
构造函数
FileLogStrategy(const std::string &path = defaultpath,
const std::string &file = defaultfile)
: _path(path), _file(file) {
// 初始化日志目录
if (!std::filesystem::exists(_path)) {
try {
std::filesystem::create_directories(_path);
} catch (const std::filesystem::filesystem_error &e) {
std::cerr << e.what() << '\n';
}
}
}
参数:
-
path: 日志目录路径(可选,默认defaultpath) -
file: 日志文件名(可选,默认defaultfile)
功能:
-
初始化成员变量
_path和_file -
检查日志目录是否存在,如果不存在则尝试创建
-
使用
try-catch捕获可能的文件系统异常并输出错误信息
构造函数这里使用了 C++17 引入的 <filesystem> 库来处理文件系统操作。下面讲解std::filesystem 的相关功能以及这段代码的作用:
1. std::filesystem 简介
std::filesystem 是 C++17 标准中引入的库,用于处理文件和目录路径、遍历文件系统、查询文件信息等操作。它提供了跨平台的文件系统操作接口,替代了之前的平台相关 API(如 Windows 的 <windows.h> 或 POSIX 的 <dirent.h>)。

2. 代码功能解析
这段代码是一个构造函数,用于初始化一个文件日志策略类 FileLogStrategy,主要功能是确保日志目录存在:
FileLogStrategy(const std::string &path = defaultpath,
const std::string &file = defaultfile)
: _path(path), _file(file) { // 初始化成员变量
// 检查目录是否存在
if (!std::filesystem::exists(_path)) { // 如果路径不存在
try {
// 创建目录(包括所有必要的父目录)
std::filesystem::create_directories(_path);
} catch (const std::filesystem::filesystem_error &e) {
// 捕获并处理文件系统错误
std::cerr << e.what() << '\n';
}
}
}
3. 关键函数说明
(1) std::filesystem::exists()

-
作用:检查给定路径是否存在(文件或目录)
-
参数:接受一个路径对象(可以是
std::string、std::filesystem::path或字符数组) -
返回值:
bool,存在返回true,否则false -
示例:
bool exists = std::filesystem::exists("/path/to/check");
(2) std::filesystem::create_directories()

-
作用:创建目录及其所有不存在的父目录(类似
mkdir -p) -
参数:路径对象
-
异常:如果创建失败会抛出
std::filesystem::filesystem_error -
与
create_directory()的区别:-
create_directory()只能创建单级目录,父目录必须已存在 -
create_directories()可以创建多级目录
-
-
示例:
std::filesystem::create_directories("path/to/log/dir");
(3) std::filesystem::filesystem_error

-
作用:文件系统操作失败时抛出的异常类型
-
常用方法:
-
what():返回错误描述字符串 -
path1():返回第一个相关路径 -
path2():返回第二个相关路径(如果有) -
code():返回std::error_code对象
-
4. 完整工作流程
-
构造函数接收日志路径
_path和文件名_file -
使用
exists()检查路径是否存在 -
如果不存在,尝试用
create_directories()创建目录 -
如果创建过程中出错(如权限不足),捕获异常并输出错误信息
5. 现代 C++ 改进建议
-
可以使用
std::filesystem::path类型代替std::string表示路径
-
C++17 允许使用结构化绑定处理异常:
catch (const std::filesystem::filesystem_error& e) { auto [ec, msg] = std::make_pair(e.code(), e.what()); std::cerr << "Error " << ec << ": " << msg << '\n'; }
6. 编译注意事项
使用 std::filesystem 需要:
-
C++17 或更高版本支持(编译选项如
-std=c++17) -
链接文件系统库(如 g++ 需要
-lstdc++fs,但较新版本已集成)
这个构造函数展示了文件系统库的典型用法:路径检查 + 目录创建 + 错误处理,是日志系统初始化的常见模式。
SyncLog 方法
void SyncLog(const std::string &message) override {
LockGuard lockguard(_mutex);
std::string filename = _path + (_path.back() == '/' ? "" : "/") + _file;
std::ofstream out(filename, std::ios::app); // 追加模式
if (out.is_open()) {
out << message << gsep;
out.close();
}
}
参数:message - 要写入的日志消息
功能:
-
使用
LockGuard获取互斥锁,保证线程安全 -
构造完整的日志文件路径:
-
如果路径已以 '/' 结尾,直接拼接文件名
-
否则添加 '/' 后再拼接文件名
-
-
以追加模式 (
std::ios::app) 打开文件流 -
如果文件成功打开:
-
写入日志消息和分隔符 (
gsep,定义为"\r\n") -
关闭文件流
-
这个函数实现了一个线程安全的日志同步写入功能,使用了 std::ofstream 进行文件操作。下面讲解 std::ofstream 的相关用法以及整个函数的逻辑。
1. std::ofstream 详解
std::ofstream 是 C++ 标准库 <fstream> 提供的输出文件流类,用于向文件写入数据。它是 std::ostream 的派生类,支持 << 操作符,类似于 std::cout,但写入文件而非控制台。

(1) 构造函数
std::ofstream out(filename, std::ios::app);
-
作用:打开文件
filename,准备写入。 -
参数:
-
filename:文件路径(std::string或const char*)。 -
std::ios::app:打开模式,表示追加模式(append),新数据写入文件末尾,不会覆盖原有内容。
-
-
其他常见打开模式(可组合使用):

-
std::ios::out:默认模式,写入文件(覆盖原有内容)。 -
std::ios::binary:二进制模式。 -
std::ios::trunc:如果文件存在,清空内容(默认行为,除非指定app或ate)。 -
std::ios::ate:打开后定位到文件末尾(但写入时仍可移动指针)。
-
(2) is_open()

if (out.is_open())
-
作用:检查文件是否成功打开。
-
返回值:
bool,成功打开返回true,否则false(如权限不足、路径不存在等)。
(3) << 操作符
out << message << gsep;
-
作用:向文件写入数据,类似于
std::cout。 -
支持类型:字符串、数字、自定义类型(需重载
<<)。
(4) close()

out.close();
-
作用:关闭文件流,释放资源。
-
注意:析构时会自动调用,但显式关闭是良好实践。
2. 代码逻辑解析
void SyncLog(const std::string &message) override {
LockGuard lockguard(_mutex); // 1. 加锁,保证线程安全
// 2. 构造完整文件路径(确保路径以 '/' 结尾)
std::string filename = _path + (_path.back() == '/' ? "" : "/") + _file;
// 3. 以追加模式打开文件
std::ofstream out(filename, std::ios::app);
// 4. 检查文件是否成功打开
if (out.is_open()) {
out << message << gsep; // 5. 写入日志消息和分隔符
out.close(); // 6. 关闭文件
}
}
(1) 线程安全
LockGuard lockguard(_mutex);
-
使用
LockGuard(可能是std::lock_guard<std::mutex>的封装)保护共享资源(日志文件),防止多线程同时写入导致数据混乱。
(2) 路径拼接
std::string filename = _path + (_path.back() == '/' ? "" : "/") + _file;
确保路径格式正确:
-
如果
_path已经以/结尾(如"/var/log/"),直接拼接_file。 -
否则,补上
/(如_path="logs",_file="app.log"→"logs/app.log")。
(3) 文件打开模式
std::ofstream out(filename, std::ios::app);
std::ios::app:
-
每次写入都在文件末尾追加,不会覆盖原有内容。
-
适合日志场景,避免丢失历史记录。
(4) 写入日志
out << message << gsep;
-
message:要写入的日志内容。 -
gsep:可能是全局定义的日志分隔符(如"\n"或"\r\n")。
(5) 关闭文件
out.close();
-
显式关闭文件,确保数据刷新到磁盘(虽然析构时也会自动关闭)。
特点分析
-
线程安全:
-
使用
Mutex和LockGuard确保多线程环境下文件写入的原子性 -
每个文件操作都在互斥锁保护下进行
-
-
路径处理:
-
智能处理路径结尾的斜杠,确保路径拼接正确
-
使用 C++17 的
std::filesystem进行目录操作
-
-
文件操作:
-
使用追加模式 (
std::ios::app) 打开文件,确保新日志追加到文件末尾 -
检查文件是否成功打开后再进行写入
-
-
错误处理:
-
捕获并处理目录创建可能抛出的异常
-
静默处理文件打开失败的情况(仅不写入,不抛出异常)
-
3、日志等级与时间戳处理
// 日志等级枚举
enum class LogLevel {
DEBUG,
INFO,
WARNING,
ERROR,
FATAL
};
// 日志等级转字符串
std::string Level2Str(LogLevel level) {
switch (level) {
case LogLevel::DEBUG: return "DEBUG";
case LogLevel::INFO: return "INFO";
case LogLevel::WARNING: return "WARNING";
case LogLevel::ERROR: return "ERROR";
case LogLevel::FATAL: return "FATAL";
default: return "UNKNOWN";
}
}
// 获取格式化时间戳
std::string GetTimeStamp() {
time_t curr = time(nullptr);
struct tm curr_tm;
localtime_r(&curr, &curr_tm); // 线程安全的时间转换
char timebuffer[128];
snprintf(timebuffer, sizeof(timebuffer),
"%4d-%02d-%02d %02d:%02d:%02d",
curr_tm.tm_year+1900,
curr_tm.tm_mon+1,
curr_tm.tm_mday,
curr_tm.tm_hour,
curr_tm.tm_min,
curr_tm.tm_sec);
return timebuffer;
}
这段代码实现了一个线程安全的日志系统,支持控制台输出和文件写入两种策略,并采用了策略模式设计。下面我将从日志等级、时间戳、策略模式、线程安全、宏定义等方面详细解析。
1. 日志等级 (LogLevel)
enum class LogLevel {
DEBUG,
INFO,
WARNING,
ERROR,
FATAL
};
-
作用:定义日志的级别,用于区分日志的重要性。
-
优点:
-
enum class避免命名冲突(强类型枚举)。 -
扩展性强,可轻松添加新级别。
-
| 枚举值 | 日志等级 | 典型用途 |
|---|---|---|
DEBUG |
调试级 | 用于开发调试的详细信息,通常在生产环境中关闭。 |
INFO |
信息级 | 记录程序正常运行时的关键信息(如服务启动、配置加载等)。 |
WARNING |
警告级 | 表示潜在问题,程序仍能运行但可能需要关注(如磁盘空间不足)。 |
ERROR |
错误级 | 记录影响部分功能的错误,但程序仍可继续运行(如请求参数无效)。 |
FATAL |
致命级 | 导致程序无法继续运行的严重错误(如数据库连接失败),通常触发进程终止。 |
补充说明:
-
严重程度递增:
DEBUG→INFO→WARNING→ERROR→FATAL(从低到高,通常高严重等级会触发通知机制) -
生产环境建议:
-
开发环境:可开启
DEBUG -
生产环境:通常只记录
INFO及以上等级(WARNING/ERROR/FATAL)
-
-
常见应用场景:
-
FATAL错误可能伴随日志文件输出和系统告警。 -
DEBUG日志可能包含敏感信息,需谨慎使用。
-
在C++中,enum class(也称为强类型枚举或作用域枚举)是C++11引入的一种改进的枚举类型定义方式。相比于传统的C风格枚举(enum),enum class具有以下优势和特性:
1. 作用域隔离(Scoped Enumeration)
-
传统
enum的问题:枚举值会暴露在外部作用域中,可能导致命名冲突。enum LogLevel { DEBUG, INFO, WARNING, ERROR, FATAL }; enum Color { RED, GREEN, BLUE }; // 错误:DEBUG与RED冲突(如果值相同) -
enum class的解决方案:枚举值的作用域限定在枚举类内部,必须通过枚举类型名访问。enum class LogLevel { DEBUG, INFO, WARNING, ERROR, FATAL }; enum class Color { RED, GREEN, BLUE }; LogLevel level = LogLevel::DEBUG; // 正确 Color c = Color::RED; // 正确
2. 强类型(Strong Typing)
-
传统
enum的问题:枚举值可以隐式转换为整数(如int),可能导致误用。enum LogLevel { DEBUG, INFO }; int x = DEBUG; // 隐式转换为int,可能引发逻辑错误 -
enum class的解决方案:枚举值不会隐式转换为整数,必须显式转换。enum class LogLevel { DEBUG, INFO }; // int x = LogLevel::DEBUG; // 错误:不能隐式转换 int x = static_cast<int>(LogLevel::DEBUG); // 必须显式转换
3. 避免命名污染
-
传统枚举的枚举值(如
DEBUG)可能与其他全局标识符冲突,而enum class的枚举值必须通过EnumType::Value访问,避免了污染外部命名空间。
4. 可以指定底层类型
-
enum class允许显式指定枚举的底层整数类型(默认为int),节省内存或匹配接口要求。enum class LogLevel : uint8_t { DEBUG, INFO, WARNING, ERROR, FATAL }; // 每个枚举值仅占用1字节
代码示例解析
enum class LogLevel {
DEBUG,
INFO,
WARNING,
ERROR,
FATAL
};
-
含义:定义了一个强类型枚举
LogLevel,包含5个枚举值,分别表示日志级别。 -
用途:
-
在日志系统中限制日志级别的取值范围。
-
避免直接使用整数或字符串表示级别,提高代码可读性和类型安全性。
-
例如:
void log(LogLevel level, const std::string& message) { if (level == LogLevel::FATAL) { // 处理严重错误 } }
-
对比总结
| 特性 | 传统enum |
enum class |
|---|---|---|
| 作用域 | 全局 | 枚举类内部 |
| 类型安全 | 弱(可隐式转换) | 强(需显式转换) |
| 命名冲突风险 | 高 | 低 |
| 底层类型指定 | 不支持 | 支持(如: uint8_t) |
何时使用?
-
优先使用
enum class:需要类型安全、避免命名冲突或明确底层类型时。 -
传统
enum:需要与C代码交互或兼容旧代码时(但现代C++应尽量避免)。
通过enum class,C++提供了更安全、更清晰的枚举类型,是推荐的做法。
日志等级转字符串 (Level2Str)
std::string Level2Str(LogLevel level) {
switch (level) {
case LogLevel::DEBUG: return "DEBUG";
case LogLevel::INFO: return "INFO";
case LogLevel::WARNING: return "WARNING";
case LogLevel::ERROR: return "ERROR";
case LogLevel::FATAL: return "FATAL";
default: return "UNKNOWN";
}
}
-
作用:将枚举值转换为可读的字符串,便于日志输出。
-
示例:
LogLevel level = LogLevel::ERROR; std::cout << Level2Str(level); // 输出 "ERROR"
2. 时间戳 (GetTimeStamp)
std::string GetTimeStamp() {
time_t curr = time(nullptr);
struct tm curr_tm;
localtime_r(&curr, &curr_tm); // 线程安全的时间转换
char timebuffer[128];
snprintf(timebuffer, sizeof(timebuffer),
"%4d-%02d-%02d %02d:%02d:%02d",
curr_tm.tm_year+1900,
curr_tm.tm_mon+1,
curr_tm.tm_mday,
curr_tm.tm_hour,
curr_tm.tm_min,
curr_tm.tm_sec);
return timebuffer;
}
-
作用:获取当前时间,格式化为
YYYY-MM-DD HH:MM:SS。 -
关键点:
-
localtime_r是线程安全的(localtime是非线程安全的)。 -
tm_year是从 1900 开始的年数,需+1900。 -
tm_mon是 0~11,需+1。
-
-
输出示例:2023-10-05 14:30:45
这段代码定义了一个函数 GetTimeStamp(),用于获取当前系统时间并格式化为易读的字符串(如 "2023-10-05 14:30:45")。以下是逐行解析,重点讲解涉及时间的函数和用法:
1. 获取当前时间戳

time_t curr = time(nullptr);
time() 函数:
-
作用:获取当前系统的日历时间(Calendar Time),即从 1970年1月1日00:00:00 UTC(Unix 纪元)开始的秒数。
-
参数:
nullptr表示不存储结果,直接返回时间值。 -
返回值:
time_t类型(通常是整数或浮点数,表示秒数)。 -
示例:如果当前时间是 1970年1月1日00:00:01 UTC,
time()返回1。
2. 转换为本地时间结构体
struct tm curr_tm;
localtime_r(&curr, &curr_tm); // 线程安全的时间转换
struct tm:
-
是一个结构体,用于存储分解时间(Broken-down Time),包含年、月、日、时、分、秒等字段:
struct tm { int tm_sec; // 秒 [0, 60](含闰秒) int tm_min; // 分 [0, 59] int tm_hour; // 时 [0, 23] int tm_mday; // 日 [1, 31] int tm_mon; // 月 [0, 11](0表示1月!) int tm_year; // 年(实际年份 - 1900) // 其他字段(如星期、夏令时标志等) }; -
注意:
tm_year是从 1900 年开始的偏移量,tm_mon是 0-based(0=1月)。
localtime_r() 函数:

-
作用:将
time_t转换为本地时间的struct tm,线程安全(_r表示 reentrant,可重入)。 -
对比非线程安全版本:
struct tm* localtime(const time_t* timer); // 返回静态内存,多线程不安全! -
参数:
-
&curr:输入的time_t时间戳。 -
&curr_tm:输出的struct tm结构体。
-
下翻手册,我们可以看到struct tm 结构体的定义:

3. 格式化时间字符串
char timebuffer[128];
snprintf(timebuffer, sizeof(timebuffer),
"%4d-%02d-%02d %02d:%02d:%02d",
curr_tm.tm_year + 1900, // 年份需要 +1900
curr_tm.tm_mon + 1, // 月份需要 +1(0=1月)
curr_tm.tm_mday,
curr_tm.tm_hour,
curr_tm.tm_min,
curr_tm.tm_sec);
snprintf() 函数:
-
作用:将格式化数据写入字符缓冲区,避免缓冲区溢出(比
sprintf()更安全)。 -
格式化字符串:
-
%4d:年份(4位宽度,如2023)。 -
%02d:月份/日期/时间(2位宽度,不足补零,如05)。
-
-
字段调整:
-
tm_year + 1900(因为tm_year是从 1900 年开始的偏移量):将tm_year(如123)转换为实际年份(如2023)。 -
tm_mon + 1(tm_mon是 0-based(0=1月)):将tm_mon(0-based)转换为自然月(1-12)。
-
4. 返回字符串
return timebuffer;
-
将格式化后的字符串(如
"2023-10-05 14:30:45")返回。
完整代码逻辑总结
-
获取时间戳:
time(nullptr)返回当前时间的time_t值。 -
转换为本地时间:
localtime_r()将time_t分解为年、月、日等字段。 -
格式化字符串:将
struct tm的字段拼接为YYYY-MM-DD HH:MM:SS格式。 -
返回结果:返回格式化后的字符串。
4、核心Logger类实现
class Logger {
public:
Logger() {
EnableConsoleLogStrategy(); // 默认使用控制台策略
}
// 策略切换方法
void EnableFileLogStrategy() {
_fflush_strategy = std::make_unique<FileLogStrategy>();
}
void EnableConsoleLogStrategy() {
_fflush_strategy = std::make_unique<ConsoleLogStrategy>();
}
// 日志消息类(内部类)
class LogMessage {
public:
LogMessage(LogLevel &level, std::string &src_name, int line_number, Logger &logger)
: _curr_time(GetTimeStamp()),
_level(level),
_pid(getpid()),
_src_name(src_name),
_line_number(line_number),
_logger(logger) {
// 构建日志前缀
std::stringstream ss;
ss << "[" << _curr_time << "] "
<< "[" << Level2Str(_level) << "] "
<< "[" << _pid << "] "
<< "[" << _src_name << "] "
<< "[" << _line_number << "] "
<< "- ";
_loginfo = ss.str();
}
// 流式输出支持
template <typename T>
LogMessage &operator<<(const T &info) {
std::stringstream ss;
ss << info;
_loginfo += ss.str();
return *this;
}
// 析构时自动输出日志
~LogMessage() {
if (_logger._fflush_strategy) {
_logger._fflush_strategy->SyncLog(_loginfo);
}
}
private:
std::string _curr_time;
LogLevel _level;
pid_t _pid;
std::string _src_name;
int _line_number;
std::string _loginfo;
Logger &_logger;
};
// 日志入口操作符
LogMessage operator()(LogLevel level, std::string name, int line) {
return LogMessage(level, name, line, *this);
}
private:
std::unique_ptr<LogStrategy> _fflush_strategy; // 当前日志策略
};
Logger 类是日志系统的核心类,负责协调日志的生成和输出策略。下面从多个方面详细解析这个类的设计:
1. 类结构概述
Logger 类采用了策略模式,将日志输出的具体实现与日志生成逻辑分离。主要包含:
-
策略管理(切换控制台/文件输出)
-
日志消息生成(内部类
LogMessage) -
流式输出支持(通过
operator<<)
2. 构造函数与默认行为
Logger() {
EnableConsoleLogStrategy(); // 默认使用控制台策略
}
-
构造函数中默认启用控制台日志策略
-
这种设计确保即使不显式配置,日志系统也能正常工作
3. 策略管理方法
void EnableFileLogStrategy() {
_fflush_strategy = std::make_unique<FileLogStrategy>();
}
void EnableConsoleLogStrategy() {
_fflush_strategy = std::make_unique<ConsoleLogStrategy>();
}
-
使用
std::unique_ptr管理策略对象,确保自动内存管理 -
通过这两个方法可以动态切换日志输出目标
-
体现了策略模式的核心思想:运行时切换算法
4. 内部类 LogMessage
这是日志系统的关键部分,负责:
-
构建完整的日志消息
-
支持流式输出
-
在析构时自动输出日志
4.1 构造函数
LogMessage(LogLevel &level, std::string &src_name, int line_number, Logger &logger)
: _curr_time(GetTimeStamp()),
_level(level),
_pid(getpid()),
_src_name(src_name),
_line_number(line_number),
_logger(logger) {
// 构建日志前缀
std::stringstream ss;
ss << "[" << _curr_time << "] "
<< "[" << Level2Str(_level) << "] "
<< "[" << _pid << "] "
<< "[" << _src_name << "] "
<< "[" << _line_number << "] "
<< "- ";
_loginfo = ss.str();
}
-
收集日志所需的所有元信息:时间戳、日志级别、进程ID、源文件名、行号
-
构建格式化的日志前缀,例如:
[2023-05-20 14:30:45] [INFO] [1234] [main.cpp] [42] -
这段代码定义了一个构造函数,用于初始化日志消息对象(可能是 LogMessage 类)。它使用了 std::stringstream 来构建日志前缀,并通过 Logger &logger 参数关联日志记录器。
1. 构造函数参数分析
LogMessage(LogLevel &level, std::string &src_name, int line_number, Logger &logger)
: _curr_time(GetTimeStamp()),
_level(level),
_pid(getpid()),
_src_name(src_name),
_line_number(line_number),
_logger(logger) { ... }
(1) 参数说明
|
参数 |
类型 |
作用 |
|---|---|---|
|
|
|
日志级别(如 DEBUG、INFO、ERROR),用于分类日志重要性。 |
|
|
|
源文件名或模块名,标识日志来源。 |
|
|
|
代码行号,用于精确定位日志位置。 |
|
|
|
日志记录器对象的引用,用于后续实际输出日志。 |
(2) 成员变量初始化
-
_curr_time(GetTimeStamp()):调用GetTimeStamp()函数获取当前时间戳(可能是字符串或时间对象),记录日志时间。 -
_level(level):保存日志级别,后续可能用于过滤或格式化。 -
_pid(getpid()):调用系统函数getpid()获取进程 ID,用于区分多进程日志。 -
_src_name(src_name):保存源文件名或模块名。 -
_line_number(line_number):保存代码行号。 -
_logger(logger):保存日志记录器的引用,后续通过_logger输出日志。
2. std::stringstream 的作用

std::stringstream ss;
ss << "[" << _curr_time << "] "
<< "[" << Level2Str(_level) << "] "
<< "[" << _pid << "] "
<< "[" << _src_name << "] "
<< "[" << _line_number << "] "
<< "- ";
_loginfo = ss.str();
(1) 功能
-
构建日志前缀:将日志的元信息(时间、级别、进程 ID、源文件、行号)格式化为字符串,存储到成员变量
_loginfo中。 -
示例输出:
[2023-01-01 12:00:00] [INFO] [1234] [main.cpp] [42] - This is a log message.
(2) 关键操作
-
std::stringstream:C++ 标准库中的字符串流,支持流式操作(<<),用于高效拼接字符串。 -
Level2Str(_level):假设是将LogLevel枚举转换为字符串(如DEBUG→"DEBUG")。 -
ss.str():将流内容转换为std::string,赋值给_loginfo。
(3) 优势
-
可读性:结构化前缀使日志易于阅读和分析。
-
灵活性:可轻松调整格式(如添加/删除字段)。
-
性能:
stringstream比多次字符串拼接更高效。
3. Logger &logger 的作用和意义
(1) 为什么传递 Logger 引用?
-
解耦:
LogMessage只负责构建日志内容,不关心具体输出方式(文件、控制台、网络等)。实际输出由Logger实现,遵循单一职责原则。 -
灵活性:可通过不同
Logger实现切换日志行为(如FileLogger、ConsoleLogger)。 -
避免拷贝:传递引用避免
Logger对象的拷贝开销。
(2) 典型用法
后续可能通过 _logger 输出完整日志:
void LogMessage::Output(const std::string &message) {
_logger.Log(_loginfo + message); // 组合前缀和实际消息
}
(3) 设计意义
-
策略模式:
Logger是日志输出策略,LogMessage是日志内容构建器,二者分离。 -
可扩展性:新增日志输出方式只需实现新的
Logger子类,无需修改LogMessage。
4.2 流式输出支持
template <typename T>
LogMessage &operator<<(const T &info) {
std::stringstream ss;
ss << info;
_loginfo += ss.str();
return *this;
}
-
支持链式调用,如
LOG(INFO) << "Message" << 123 << 3.14 -
使用模板支持任意类型的输出
-
将输入内容转换为字符串(ss.str())并追加到日志消息中
4.3 自动输出机制
~LogMessage() {
if (_logger._fflush_strategy) {
_logger._fflush_strategy->SyncLog(_loginfo);
}
}
-
利用RAII机制,在对象析构时自动输出日志
-
确保即使发生异常也能记录日志
-
检查策略对象(可以通过Logger构造选择哪个策略)是否存在,避免空指针解引用

5. 日志入口操作符
LogMessage operator()(LogLevel level, std::string name, int line) {
return LogMessage(level, name, line, *this);
}
这段代码定义了一个函数调用运算符重载(operator()),用于快速创建 LogMessage 对象。它通常出现在 Logger 类中,目的是提供一种简洁的语法来生成日志消息,重载函数调用操作符,提供简洁的API。
作用
-
这是一个运算符重载,允许
Logger对象像函数一样被调用(即Logger()语法)。 -
它构造并返回一个
LogMessage对象,将当前Logger对象(*this)传递给LogMessage的构造函数。 -
返回
LogMessage临时对象,支持流式输出。
参数:
-
level: 日志级别 -
name: 源文件名(通常通过宏传递__FILE__) -
line: 行号(通常通过宏传递__LINE__)
典型使用场景
假设在 Logger 类中定义了此运算符,用户可以这样使用:
Logger logger; // 假设已初始化
logger(LogLevel::INFO, "main.cpp", 42) << "This is a log message.";
这行代码会:
-
调用
Logger::operator(),生成一个临时的LogMessage对象。 -
通过
LogMessage的流式操作(如operator<<)追加日志内容。 -
在
LogMessage析构时(如语句结束时),自动将日志输出到logger。
临时对象的析构时机:
-
临时对象会在这个完整表达式结束时被析构。
-
在
logger(...) << "message"中:-
logger(...)返回LogMessage临时对象。 -
<< "message"调用LogMessage::operator<<,返回LogMessage&(通常是*this)。 -
完整表达式是
logger(...) << "message",因此临时LogMessage会在整个语句结束时(即;处)被析构。
-
参数与返回值
(1) 参数
|
参数 |
类型 |
作用 |
|---|---|---|
|
|
|
日志级别(如 INFO、ERROR)。 |
|
|
|
源文件名或模块名。 |
|
|
|
代码行号。 |
(2) 返回值
-
返回一个
LogMessage对象(通常是临时对象,支持链式调用)。 -
LogMessage的构造函数接受*this(当前Logger对象的引用),用于后续日志输出。
5、全局访问与宏定义
// 全局日志对象
Logger logger;
// 简化使用的宏定义
#define LOG(level) logger(level, __FILE__, __LINE__)
#define Enable_Console_Log_Strategy() logger.EnableConsoleLogStrategy()
#define Enable_File_Log_Strategy() logger.EnableFileLogStrategy()
这段代码定义了一个全局日志对象 logger,并通过宏简化日志调用和日志策略配置。以下是详细解析:
1. 全局日志对象
Logger logger;
-
作用:声明一个全局的
Logger实例,供整个程序使用。 -
特点:
-
全局可见,任何地方都可以直接使用
logger记录日志。 -
通常在程序启动时初始化(如
main()开头),并在程序结束时自动析构。
-
-
潜在问题:
-
全局变量可能导致依赖耦合,不利于单元测试。
-
如果程序是多线程的,需要确保
Logger的线程安全(如加锁或使用线程安全的数据结构)。
-
2. 宏定义解析
(1) LOG(level) 宏
#define LOG(level) logger(level, __FILE__, __LINE__)
-
作用:简化日志调用,自动填充文件名和行号。
-
展开后:
LOG(LogLevel::INFO) << "This is a log message."; // 等价于: logger(LogLevel::INFO, __FILE__, __LINE__) << "This is a log message."; -
关键点:(详细回顾C语言的章节)
-
__FILE__是预定义宏,表示当前源文件名(如"main.cpp")。 -
__LINE__是预定义宏,表示当前行号(如42)。 -
这样日志会自动记录代码位置,便于调试。
-
使用示例
LOG(LogLevel::DEBUG) << "x = " << x;
LOG(LogLevel::ERROR) << "File not found: " << filename;
(2) 日志策略配置宏
#define Enable_Console_Log_Strategy() logger.EnableConsoleLogStrategy()
#define Enable_File_Log_Strategy() logger.EnableFileLogStrategy()
-
作用:简化日志输出策略的配置。
-
展开后:
Enable_Console_Log_Strategy(); // 等价于 logger.EnableConsoleLogStrategy(); Enable_File_Log_Strategy(); // 等价于 logger.EnableFileLogStrategy(); -
典型用途:
-
EnableConsoleLogStrategy():启用控制台输出(如std::cout)。 -
EnableFileLogStrategy():启用文件输出(如写入log.txt)。
-
-
设计意图:
-
通过宏隐藏具体实现,使代码更简洁。
-
如果未来
Logger的策略配置方法改名,只需修改宏定义,无需改动所有调用处。
-
使用示例
int main() {
Enable_Console_Log_Strategy(); // 启用控制台日志
Enable_File_Log_Strategy(); // 启用文件日志
LOG(LogLevel::INFO) << "Application started.";
return 0;
}
6、注意:关于多态(Polymorphism)和智能指针(std::unique_ptr)的使用
具体来说, _fflush_strategy 是 LogStrategy* 类型,但被赋值为 FileLogStrategy 或 ConsoleLogStrategy 的实例时,是否能正确访问派生类的成员,而不会发生对象截断(Object Slicing)。
1. 这份代码是否安全?
是的,代码是安全的,不会发生对象截断,原因如下:
std::unique_ptr<LogStrategy> 存储的是派生类对象
-
std::make_unique<FileLogStrategy>()会在堆上创建一个FileLogStrategy对象,并返回std::unique_ptr<FileLogStrategy>,但可以隐式转换为std::unique_ptr<LogStrategy>(因为FileLogStrategy继承自LogStrategy)。 -
此时
_fflush_strategy仍然指向完整的FileLogStrategy对象,不会发生截断。
多态调用(Virtual Functions)
-
只要
LogStrategy定义了虚函数(如void Log(const std::string& message)),并且FileLogStrategy和ConsoleLogStrategy覆盖了这些虚函数,那么通过_fflush_strategy->Log(...)调用时,会正确执行派生类的实现。 -
如果没有虚函数,则会发生静态绑定(早期绑定),可能调用到基类的实现(但不会截断对象)。
2. 对象截断(Object Slicing)何时发生?
对象截断通常发生在以下情况:
LogStrategy strategy = FileLogStrategy(); // ❌ 错误!派生类部分被截断
-
这里
FileLogStrategy对象被拷贝构造到LogStrategy对象,但LogStrategy无法存储派生类的额外数据,导致派生类部分被丢弃。 -
std::unique_ptr方式不会发生这种情况,因为它是通过指针管理完整对象。
3. 代码示例分析
class Logger {
public:
Logger() {
EnableConsoleLogStrategy(); // 默认使用控制台策略
}
void EnableFileLogStrategy() {
_fflush_strategy = std::make_unique<FileLogStrategy>(); // 安全,存储完整 FileLogStrategy 对象
}
void EnableConsoleLogStrategy() {
_fflush_strategy = std::make_unique<ConsoleLogStrategy>(); // 安全,存储完整 ConsoleLogStrategy 对象
}
private:
std::unique_ptr<LogStrategy> _fflush_strategy; // 基类指针,指向派生类对象
};
_fflush_strategy 可以安全地指向 FileLogStrategy 或 ConsoleLogStrategy,因为:
-
它们都是
LogStrategy的派生类。 -
std::unique_ptr会正确管理对象的生命周期(自动释放内存)。 -
如果
LogStrategy有虚函数,并且派生类正确覆盖它们,那么调用时会执行正确的派生类逻辑。
4. 如何确保多态调用?
(1) 基类必须定义虚函数
class LogStrategy {
public:
virtual ~LogStrategy() = default; // 虚析构函数,确保派生类对象能正确释放
virtual void Log(const std::string& message) = 0; // 纯虚函数,强制派生类实现
};
(2) 派生类必须覆盖虚函数
class FileLogStrategy : public LogStrategy {
public:
void Log(const std::string& message) override {
std::cout << "FileLog: " << message << std::endl;
}
};
class ConsoleLogStrategy : public LogStrategy {
public:
void Log(const std::string& message) override {
std::cout << "ConsoleLog: " << message << std::endl;
}
};
(3) 通过基类指针调用
Logger logger;
logger._fflush_strategy->Log("Hello, world!"); // 根据当前策略调用 FileLogStrategy 或 ConsoleLogStrategy 的实现
5. 总结
-
代码是安全的,
std::unique_ptr<LogStrategy>可以正确管理FileLogStrategy或ConsoleLogStrategy对象,不会发生截断。 -
必须使用虚函数,否则
_fflush_strategy->Log(...)会调用LogStrategy的默认实现(如果存在),而不是派生类的实现。 -
对象截断只发生在值拷贝(非指针/引用)的情况下,而代码使用的是智能指针,因此不会发生。
6. 额外建议
-
如果
LogStrategy是一个接口类(只有纯虚函数),可以将其析构函数设为= default并标记为override:virtual ~LogStrategy() = default; -
考虑使用
std::move优化策略切换(避免不必要的内存分配):void EnableFileLogStrategy() { _fflush_strategy = std::make_unique<FileLogStrategy>(); // 已有优化 }
7、完整日志系统实现代码
#ifndef __LOG_HPP__
#define __LOG_HPP__
#include <iostream>
#include <cstdio>
#include <string>
#include <filesystem> //C++17
#include <sstream>
#include <fstream>
#include <memory>
#include <ctime>
#include <unistd.h>
#include "Mutex.hpp"
namespace LogModule
{
using namespace MutexModule;
const std::string gsep = "\r\n";
// 策略模式,C++多态特性
// 2. 刷新策略 a: 显示器打印 b:向指定的文件写入
// 刷新策略基类
class LogStrategy
{
public:
~LogStrategy() = default;
virtual void SyncLog(const std::string &message) = 0;
};
// 显示器打印日志的策略 : 子类
class ConsoleLogStrategy : public LogStrategy
{
public:
ConsoleLogStrategy()
{
}
void SyncLog(const std::string &message) override
{
LockGuard lockguard(_mutex);
std::cout << message << gsep;
}
~ConsoleLogStrategy()
{
}
private:
Mutex _mutex;
};
// 文件打印日志的策略 : 子类
const std::string defaultpath = "./log";
const std::string defaultfile = "my.log";
class FileLogStrategy : public LogStrategy
{
public:
FileLogStrategy(const std::string &path = defaultpath, const std::string &file = defaultfile)
: _path(path),
_file(file)
{
LockGuard lockguard(_mutex);
if (std::filesystem::exists(_path))
{
return;
}
try
{
std::filesystem::create_directories(_path);
}
catch (const std::filesystem::filesystem_error &e)
{
std::cerr << e.what() << '\n';
}
}
void SyncLog(const std::string &message) override
{
LockGuard lockguard(_mutex);
std::string filename = _path + (_path.back() == '/' ? "" : "/") + _file; // "./log/" + "my.log"
std::ofstream out(filename, std::ios::app); // 追加写入的 方式打开
if (!out.is_open())
{
return;
}
out << message << gsep;
out.close();
}
~FileLogStrategy()
{
}
private:
std::string _path; // 日志文件所在路径
std::string _file; // 日志文件本身
Mutex _mutex;
};
// 形成一条完整的日志&&根据上面的策略,选择不同的刷新方式
// 1. 形成日志等级
enum class LogLevel
{
DEBUG,
INFO,
WARNING,
ERROR,
FATAL
};
std::string Level2Str(LogLevel level)
{
switch (level)
{
case LogLevel::DEBUG:
return "DEBUG";
case LogLevel::INFO:
return "INFO";
case LogLevel::WARNING:
return "WARNING";
case LogLevel::ERROR:
return "ERROR";
case LogLevel::FATAL:
return "FATAL";
default:
return "UNKNOWN";
}
}
std::string GetTimeStamp()
{
time_t curr = time(nullptr);
struct tm curr_tm;
localtime_r(&curr, &curr_tm);
char timebuffer[128];
snprintf(timebuffer, sizeof(timebuffer),"%4d-%02d-%02d %02d:%02d:%02d",
curr_tm.tm_year+1900,
curr_tm.tm_mon+1,
curr_tm.tm_mday,
curr_tm.tm_hour,
curr_tm.tm_min,
curr_tm.tm_sec
);
return timebuffer;
}
// 1. 形成日志 && 2. 根据不同的策略,完成刷新
class Logger
{
public:
Logger()
{
EnableConsoleLogStrategy();
}
void EnableFileLogStrategy()
{
_fflush_strategy = std::make_unique<FileLogStrategy>();
}
void EnableConsoleLogStrategy()
{
_fflush_strategy = std::make_unique<ConsoleLogStrategy>();
}
// 表示的是未来的一条日志
class LogMessage
{
public:
LogMessage(LogLevel &level, std::string &src_name, int line_number, Logger &logger)
: _curr_time(GetTimeStamp()),
_level(level),
_pid(getpid()),
_src_name(src_name),
_line_number(line_number),
_logger(logger)
{
// 日志的左边部分,合并起来
std::stringstream ss;
ss << "[" << _curr_time << "] "
<< "[" << Level2Str(_level) << "] "
<< "[" << _pid << "] "
<< "[" << _src_name << "] "
<< "[" << _line_number << "] "
<< "- ";
_loginfo = ss.str();
}
// LogMessage() << "hell world" << "XXXX" << 3.14 << 1234
template <typename T>
LogMessage &operator<<(const T &info)
{
// a = b = c =d;
// 日志的右半部分,可变的
std::stringstream ss;
ss << info;
_loginfo += ss.str();
return *this;
}
~LogMessage()
{
if (_logger._fflush_strategy)
{
_logger._fflush_strategy->SyncLog(_loginfo);
}
}
private:
std::string _curr_time;
LogLevel _level;
pid_t _pid;
std::string _src_name;
int _line_number;
std::string _loginfo; // 合并之后,一条完整的信息
Logger &_logger;
};
// 这里故意写成返回临时对象
LogMessage operator()(LogLevel level, std::string name, int line)
{
return LogMessage(level, name, line, *this);
}
~Logger()
{
}
private:
std::unique_ptr<LogStrategy> _fflush_strategy;
};
// 全局日志对象
Logger logger;
// 使用宏,简化用户操作,获取文件名和行号
#define LOG(level) logger(level, __FILE__, __LINE__)
#define Enable_Console_Log_Strategy() logger.EnableConsoleLogStrategy()
#define Enable_File_Log_Strategy() logger.EnableFileLogStrategy()
}
#endif
五、线程安全实现
互斥锁封装
#pragma once
#include <iostream>
#include <pthread.h>
namespace MutexModule {
class Mutex {
public:
Mutex() {
pthread_mutex_init(&_mutex, nullptr);
}
void Lock() {
pthread_mutex_lock(&_mutex);
}
void Unlock() {
pthread_mutex_unlock(&_mutex);
}
~Mutex() {
pthread_mutex_destroy(&_mutex);
}
pthread_mutex_t *Get() {
return &_mutex;
}
private:
pthread_mutex_t _mutex;
};
// RAII锁守卫
class LockGuard {
public:
LockGuard(Mutex &mutex) : _mutex(mutex) {
_mutex.Lock();
}
~LockGuard() {
_mutex.Unlock();
}
private:
Mutex &_mutex;
};
}
六、使用示例与分析
1、基本使用示例
#include "Log.hpp"
using namespace LogModule;
void fun() {
int a = 10;
LOG(LogLevel::FATAL) << "hello world" << 1234 << ", 3.14" << 'c' << a;
}
int main() {
// 使用控制台日志
LOG(LogLevel::DEBUG) << "hello world";
// 切换到文件日志
Enable_File_Log_Strategy();
LOG(LogLevel::WARNING) << "hello world";
fun();
return 0;
}
2、输出结果分析
控制台输出示例 和 文件日志内容(my.log)如下:

3、设计优势
-
策略模式灵活性:运行时切换日志输出方式、易于扩展新的输出策略(如网络日志、数据库日志)
-
线程安全性:每个策略实现内部使用互斥锁、RAII模式的LockGuard确保异常安全
-
易用性:宏定义简化调用、流式接口支持多种数据类型
-
性能考虑:日志格式化与输出分离、析构时自动触发输出,确保异常情况也能记录
七、改进与扩展建议
1、性能优化方向
-
异步日志:引入生产者-消费者模型、使用队列缓冲日志消息
-
日志级别过滤:在Logger类中添加级别过滤、避免不必要日志的格式化开销
-
批量写入:文件日志策略积累一定量后批量写入、减少IO操作次数
2、功能扩展建议
-
日志滚动策略:按文件大小或时间分割日志、自动清理旧日志文件
-
多输出目标:支持同时输出到控制台和文件、不同级别输出到不同目标
-
格式化定制:支持自定义日志格式模板、添加颜色输出(控制台)
-
上下文信息:支持添加请求ID等跨调用链信息、线程本地存储优化上下文传递
八、总结
本日志系统实现展示了策略模式在软件开发中的典型应用:
-
分离变与不变:不变:日志记录的核心流程;变:不同的输出方式(策略)
-
开闭原则:对扩展开放(新增策略)、对修改关闭(无需改动Logger类)
-
组合优于继承:通过组合方式使用策略、避免继承带来的强耦合
这种设计使得系统既保持了核心功能的稳定性,又具备了足够的灵活性来应对变化的需求。在实际项目中,可以根据具体需求在此基础上进行扩展和优化。
更多推荐

所有评论(0)