
🔥草莓熊Lotso: 个人主页
❄️个人专栏: 《C++知识分享》 《Linux 入门到实践:零基础也能懂》
✨生活是默默的坚持,毅力是永久的享受!
🎬 博主简介:

文章目录
- 前言:
- [一. 日志系统的核心设计理念](#一. 日志系统的核心设计理念)
-
- [1.1 日志的核心组成要素](#1.1 日志的核心组成要素)
- [1.2 日志系统的两大核心阶段](#1.2 日志系统的两大核心阶段)
- [1.3 为什么选择策略模式?](#1.3 为什么选择策略模式?)
- [二. 前置基础模块实现](#二. 前置基础模块实现)
-
- [2.1 RAII 风格互斥锁封装(线程安全基石)](#2.1 RAII 风格互斥锁封装(线程安全基石))
- [2.2 格式化时间戳模块(附对应模块测试)](#2.2 格式化时间戳模块(附对应模块测试))
- [2.3 类型安全的日志等级模块](#2.3 类型安全的日志等级模块)
- [三. 基于策略模式的日志刷新核心实现](#三. 基于策略模式的日志刷新核心实现)
-
- [3.1 抽象策略基类 LogStrategy](#3.1 抽象策略基类 LogStrategy)
- [3.2 控制台日志策略 ConsoleLogStrategy](#3.2 控制台日志策略 ConsoleLogStrategy)
- [3.3 文件日志策略 FileLogStrategy](#3.3 文件日志策略 FileLogStrategy)
- [四. 日志主体类与流式输出设计](#四. 日志主体类与流式输出设计)
-
- [4.1 Logger 主类的整体架构](#4.1 Logger 主类的整体架构)
- [4.2 LogMessage 内部类:RAII 实现日志自动刷新](#4.2 LogMessage 内部类:RAII 实现日志自动刷新)
- [4.3 完整的 Logger 类实现](#4.3 完整的 Logger 类实现)
- [五. 日志系统的线程安全与可重入性深度解析](#五. 日志系统的线程安全与可重入性深度解析)
- [六. 实战:日志系统完整使用示例(附带完整Logger.hpp代码呈现)](#六. 实战:日志系统完整使用示例(附带完整Logger.hpp代码呈现))
-
- [6.1 完整Logger.hpp代码](#6.1 完整Logger.hpp代码)
- [6.2 完整测试代码](#6.2 完整测试代码)
- [6.3 进阶优化方向](#6.3 进阶优化方向)
- 结尾:
前言:
在 Linux 后端开发、多线程服务端编程的场景中,日志系统是定位问题、监控服务状态的核心基础设施。很多初学者习惯用
std::cout直接打印调试信息,但在多线程环境下,会出现日志内容交错、输出乱序的问题;同时,硬编码的输出方式无法灵活切换日志目的地(控制台 / 文件 / 网络),也不支持日志分级、问题定位等工业级需求。市面上已有成熟的日志库如 spdlog、glog、Boost.Log,但从零实现一个线程安全的日志系统,能让我们深度理解设计模式、线程互斥同步、RAII 资源管理、可重入函数等 Linux 系统编程的核心知识点。本文将基于策略模式,从零实现一个兼容 glog 使用风格、支持多线程安全、可灵活扩展的 C++ 日志系统,所有代码与设计均贴合工业级开发规范。
一. 日志系统的核心设计理念
1.1 日志的核心组成要素
一条合格的工业级日志,必须包含必选字段 和可选扩展字段,确保问题可追溯、状态可监控:
- 必选核心字段 :
- 时间戳:可读性强的年月日时分秒格式,精准定位事件发生时间
- 日志等级:区分事件严重程度,支持分级过滤与告警
- 日志内容:用户自定义的业务 / 调试信息
- 可选扩展字段 :
- 进程 PID / 线程 ID:多进程 / 多线程环境下定位执行流
- 文件名与行号:精准定位日志打印的代码位置
- 自定义扩展字段:如模块名、用户 ID 等业务信息
本文实现的日志格式如下,完全兼容主流日志库的规范:
bash
[2026-04-16 21:33:18] [DEBUG] [1030871] [Main.cc] [10] - hello world hello Lotso, 3.14


1.2 日志系统的两大核心阶段
日志的生命周期可拆分为两个完全解耦的阶段,这是我们设计的核心依据:
- 日志形成阶段:将时间戳、等级、文件名、行号、用户内容等信息,拼接成一条完整的格式化字符串,与日志输出目的地无关
- 日志刷新阶段:将格式化完成的日志字符串,写入到指定目的地(控制台、文件、数据库、网络等),仅关注写入逻辑
两个阶段解耦后,我们可以独立扩展刷新逻辑,而无需修改日志格式化的核心代码,这正是策略模式的最佳应用场景。

可以看看图中的初始化代码框架,我们使用了命名空间,所以后面的代码中可能大家看起来会有缩进啥的,有的我有给带上,有的没有,大家可以最后再看看整体代码,里面包含完整的头文件和其他注意的地方
1.3 为什么选择策略模式?
策略模式是行为型设计模式的一种,核心思想是定义一系列算法(策略),将每个算法封装起来,并让它们可以互相替换。
在日志系统中,不同的日志刷新方式就是不同的策略:控制台输出、文件持久化、网络上报都是独立的刷新算法。使用策略模式带来了这些核心优势:
- 开闭原则:新增日志刷新目的地(如数据库),只需新增一个策略类,无需修改原有代码
- 解耦:日志格式化核心逻辑与刷新逻辑完全分离,代码职责单一
- 动态切换:程序运行时可随时切换日志策略,比如调试阶段用控制台输出,生产环境用文件持久化
二. 前置基础模块实现
日志系统的核心前提是线程安全,同时需要时间戳、日志等级等基础能力支撑,我们先实现这些底层模块。
2.1 RAII 风格互斥锁封装(线程安全基石)
多线程环境下,控制台、日志文件都是临界资源,多个线程同时写入会导致内容交错、乱序,必须通过互斥量保证临界区的原子性。
我们基于 Linux 原生的pthread_mutex封装互斥锁,并通过 RAII 机制管理锁的生命周期,避免手动解锁导致的死锁、内存泄漏问题,这也是 C++11 std::lock_guard的核心实现原理。这个工作我们之前就做过了,直接拿过来就可以了
Mutex.hpp
cpp
#ifndef MUTEX_HPP
#define MUTEX_HPP
#include <iostream>
#include <pthread.h>
// 互斥锁封装类:提供加锁/解锁及获取原始锁的接口
class Mutex
{
public:
// 构造函数:初始化互斥锁
Mutex()
{
pthread_mutex_init(&_lock, nullptr);
}
// 析构函数:销毁互斥锁
~Mutex()
{
pthread_mutex_destroy(&_lock);
}
// 加锁操作
void Lock()
{
pthread_mutex_lock(&_lock);
}
// 解锁操作
void UnLock()
{
pthread_mutex_unlock(&_lock);
}
// 获取原始互斥锁指针,用于需要原生 pthread_mutex_t 的接口
pthread_mutex_t* Origin()
{
return &_lock;
}
private:
pthread_mutex_t _lock; // POSIX 互斥锁
};
// RAII 风格的锁守卫类:构造时加锁,析构时解锁,自动管理锁的生命周期
class LockGuard
{
public:
// 构造函数:接收一个 Mutex 指针,并立即加锁
LockGuard(Mutex* lockptr) : _lockptr(lockptr)
{
_lockptr->Lock();
}
// 析构函数:自动解锁
~LockGuard()
{
_lockptr->UnLock();
}
private:
Mutex* _lockptr; // 指向被管理的互斥锁
};
#endif
核心设计解析:
- 禁用拷贝:互斥量是系统资源,不允许拷贝和赋值,避免重复释放、死锁等问题(这个里面没有,大家可以自己加上)
- RAII 机制 :
LockGuard在对象构造时加锁,析构时自动解锁,即使代码中途抛出异常,也能保证锁被释放,彻底避免手动解锁的遗漏 - 接口封装 :屏蔽原生
pthread库的接口细节,提供更符合 C++ 面向对象的使用方式
2.2 格式化时间戳模块(附对应模块测试)
时间戳是日志的核心字段,我们需要实现秒级、可重入、格式化 的时间戳获取功能。
这里重点注意:C 标准库的localtime函数是不可重入的,多线程环境下会出现数据错乱,因此必须使用可重入版本localtime_r,它由调用者提供结构体缓冲区,避免了全局静态变量的竞态问题。
时间戳实现代码(在命名空间里面,我没带上)
cpp
// 1. 获取时间
std::string GetTimeStamp()
{
time_t currentTime = time(nullptr); // 默认获取当前时区的时间(获取到的是秒级时间戳)
// 我们希望把这个时间转换成年-月-日 时:分:秒
struct tm dataTime;
// 使用带 _r (Reentrant) 后缀的版本 localtime_r 而不是普通的 localtime。
// 因为普通版本内部使用静态全局变量保存结果,在多线程写日志时极易发生数据覆盖错乱;
// _r 版本要求我们自己传入存放结果的地址(&dataTime),保证了多线程环境下的绝对安全。
localtime_r(¤tTime, &dataTime);
// 准备一个足够大的字符数组作为格式化字符串的缓冲区
char dataTimeStr[128];
// 使用 snprintf 将时间结构体安全地格式化为类似 [2026-04-16 19:21:32] 的排版
// %4d: 4位数字对齐; %02d: 2位数字,不足两位的在高位自动补0(如 4月 显示为 04)
snprintf(dataTimeStr, sizeof(dataTimeStr), "%4d-%02d-%02d %02d:%02d:%02d",
dataTime.tm_year + 1900, // 坑点修复:tm_year 表示的是自 1900 年起经过的年数,必须加上 1900
dataTime.tm_mon + 1, // 坑点修复:tm_mon 范围是 [0, 11](0代表1月),必须加上 1
dataTime.tm_mday, // 日([1, 31])
dataTime.tm_hour, // 时
dataTime.tm_min, // 分
dataTime.tm_sec // 秒
);
// 字符数组会自动隐式转换为 std::string 对象返回
return dataTimeStr;
}
核心细节解析:
- 可重入性保障 :使用
localtime_r替代localtime,确保多线程环境下时间转换不会出现数据竞争 - 格式化补零 :通过
%02d确保月、日、时、分、秒始终是两位数字,保证日志格式的一致性 - 时间偏移修正 :
tm_year需要 + 1900 得到真实年份,tm_mon需要 + 1 得到真实月份,这是tm结构体的标准规范


测试代码
cpp
#include <iostream>
#include <memory>
#include <unistd.h>
#include "Logger.hpp"
using namespace LogModule;
// 测试时间戳模块
void testTime()
{
for(int i = 0; i < 5; i++)
{
std::cout << GetTimeStamp() << std::endl;
sleep(1);
}
}
int main()
{
// 1. 测试时间
testTime();
return 0;
}

2.3 类型安全的日志等级模块
日志等级用于区分事件的严重程度,我们使用 C++11 的enum class实现类型安全的日志等级,避免普通枚举的隐式类型转换问题,同时提供枚举到字符串的转换能力。
日志等级实现代码(在命名空间里面,我没带上)
cpp
// 2. 日志等级 -- 枚举类型(整数)转换成字符串类型
// 使用 enum class (强类型枚举) 而不是普通 enum
// 优势:1. 具有独立的作用域,避免命名冲突;2. 不允许隐式类型转换,更加类型安全
enum class LogLevel
{
DEBUG, // 调试信息:用于开发过程中输出详细状态,帮助定位问题。生产环境通常关闭
INFO, // 常规信息:记录程序的关键运行节点,用于了解系统正常运行的状态
WARNING, // 警告信息:出现了预期之外的情况,但系统仍能继续运行,需要引起关注
ERROR, // 错误信息:发生了运行时错误,导致当前操作失败,但主程序依然存活
FATAL // 致命错误:最高严重级别。出现了无法恢复的问题,程序即将崩溃或被迫退出
};
/**
* @brief 将日志等级枚举转换为可读的字符串
* @param level 日志等级枚举值
* @return 对应的字符串表示。由于 cout 不直接支持打印强类型枚举,必须进行此类映射转换
*/
std::string LogLevel2String(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";
}
}
核心设计解析:
- 类型安全 :
enum class不会隐式转换为整型,避免了错误的等级赋值,编译期即可发现类型问题 - 等级分层:遵循业界通用的 5 级日志规范,覆盖从调试到致命错误的全场景
- 字符串转换 :通过
switch语句实现枚举到字符串的映射,确保日志中输出可读性强的等级名称,而非整型数字


测试代码
cpp
#include <iostream>
#include <memory>
#include <unistd.h>
#include "Logger.hpp"
using namespace LogModule;
// 测试日志类枚举类型转字符类型模块
void testEnum()
{
std::cout << LogLevel2String(LogLevel::DEBUG) << std::endl;
std::cout << LogLevel2String(LogLevel::INFO) << std::endl;
std::cout << LogLevel2String(LogLevel::WARNING) << std::endl;
std::cout << LogLevel2String(LogLevel::ERROR) << std::endl;
std::cout << LogLevel2String(LogLevel::FATAL) << std::endl;
}
int main()
{
// 2. 测试枚举类转字符串类型
testEnum();
return 0;
}

三. 基于策略模式的日志刷新核心实现
基于策略模式的设计,我们先定义抽象的刷新策略基类,再分别实现控制台和文件两种具体的刷新策略,后续可无限扩展其他策略。
3.1 抽象策略基类 LogStrategy
抽象基类定义了所有刷新策略必须实现的纯虚接口,同时使用虚析构函数确保子类对象能正确析构。
cpp
namespace LogModule
{
// 3. 刷新策略
// 基类: 策略模式 (Strategy Pattern)
// 核心思想:将"日志的产生"与"日志的刷新目的地"解耦。
// 通过定义统一的接口,使得程序可以在运行时动态决定将日志输出到控制台、文件、数据库或网络。
class LogStrategy
{
public:
// 虚析构函数:在多态体系中,基类必须拥有虚析构函数。
// 这样当我们通过基类指针删除派生类对象时,才能确保调用到子类的析构函数,防止内存泄漏。
virtual ~LogStrategy() = default; // 不在这里析构
// 核心刷新接口:这是一个纯虚函数。
// 纯虚函数的核心作用是定义一种"契约",强制派生类(子类)必须实现具体的逻辑。
// 不同的子类可以根据自己的策略(如 ConsoleStrategy 或 FileStrategy)来实现不同的刷新行为。
virtual void SyncLog(const std::string &message) = 0; // 强制子类对其进行重写
};
}
设计说明:
- 纯虚函数
SyncLog定义了策略的统一接口,入参是格式化完成的日志字符串,子类只需关注具体的写入逻辑 - 虚析构函数是 C++ 多态的基础规范,避免通过基类指针释放子类对象时出现内存泄漏


3.2 控制台日志策略 ConsoleLogStrategy
控制台策略负责将日志输出到标准错误流(stderr),核心是保证多线程环境下的输出原子性,避免日志交错。用到了我们自己的互斥锁记得包含对应头文件,我这里就不写了
cpp
namespace LogModule
{
// 策略1: 控制台日志策略
// 子类:继承自策略基类,用于将日志直接刷新到标准输出(显示器),常用于本地开发与调试
class ConsoleLogStrategy: public LogStrategy
{
public:
// 构造函数与析构函数:当前策略不涉及复杂资源申请,故使用默认实现即可
ConsoleLogStrategy(){}
~ConsoleLogStrategy(){}
/**
* @brief 实现具体的日志同步逻辑------刷新到控制台
* @param message 组装好的完整日志字符串
*/
void SyncLog(const std::string &message) override // 检查重写的错误
{
// 【核心原理】显示器(stdout)在多线程环境下属于"临界资源"。
// 如果不加保护,多个线程同时调用 std::cout 会导致各条日志的字符在屏幕上发生"交织"或乱码 。
// 使用自定义的 LockGuard 配合互斥锁,确保这一系列操作的原子性 。
LockGuard logGuard(&_mutex);
std::cout << message << std::endl;
}
private:
// 互斥锁:专门用于保护当前控制台输出的原子性,防止并发打印时消息错乱
Mutex _mutex;
};
}
核心细节解析:
- 线程安全保障:控制台是全局临界资源,通过互斥锁保证同一时刻只有一个线程能执行输出操作,彻底避免多线程日志交错
- stderr 输出 :使用
std::cerr而非std::cout,因为 stderr 无缓冲区,日志会实时输出,避免程序崩溃时缓冲区日志丢失 - RAII 锁管理 :使用
LockGuard自动管理锁,无需手动解锁,代码更健壮


3.3 文件日志策略 FileLogStrategy
文件策略负责将日志持久化到磁盘文件,核心功能包括:自动创建日志目录、追加模式写入、线程安全保障,使用 C++17 的filesystem库处理目录和文件操作(记得带上对应头文件)。
cpp
#include <fstream>
#include <filesystem>
cpp
namespace LogModule
{
// 定义全局默认路径与文件名常量
const static std::string gdefaultlogdir = "./log/";
const static std::string gdefaultlogfilename = "log.txt";
// 策略2:文件类日志策略
// 子类:继承自策略基类,实现将日志持久化到磁盘文件的逻辑
class FileLogStrategy: public LogStrategy
{
public:
/**
* @brief 构造函数:初始化日志路径并确保目录环境就绪
* @param logdir 日志存储目录
* @param logfilename 日志文件名称
*/
FileLogStrategy(const std::string &logdir = gdefaultlogdir, const std::string &logfilename = gdefaultlogfilename)
:_logdir(logdir),
_logfilename(logfilename)
{
// 【重点】构造阶段即进行加锁保护。
// 理由:判断目录是否存在并创建目录属于"先检查再执行(Check-Then-Act)"模式,
// 必须保证这一系列操作的原子性,防止多线程同时创建导致竞态冲突 。
LockGuard lockGuard(&_mutex);
// 使用 C++17 的 <filesystem> 库进行跨平台路径检查
if(std::filesystem::exists(_logdir))
{
return;
}
else
{
try
{
// 递归创建目录(类似于 Linux 命令 mkdir -p),如果路径中包含多级不存在的目录会一并创建
std::filesystem::create_directories(_logdir);
}
catch (std::filesystem::filesystem_error &e)
{
// 捕获文件系统异常(如权限不足、磁盘空间不足等)并输出错误信息
std::cerr << e.what() << std::endl;
}
}
}
// 析构函数:由于不涉及手动管理的堆内存或特殊文件句柄(使用局部变量流管理),故使用默认实现
~FileLogStrategy(){}
/**
* @brief 执行具体的日志落盘操作
* @param message 待写入的完整日志字符串
*/
void SyncLog(const std::string &message) override
{
// 加锁保护:防止多线程同时写入同一文件导致内容交织(Interleaving)乱码
LockGuard logGuard(&_mutex);
// 构造完整的目标文件路径
std::string target = _logdir + _logfilename;
// 以追加模式(std::ios::app)打开文件流:
// 核心逻辑:保证每条新日志都写在文件末尾,不会覆盖已有日志内容 。
std::ofstream out(target, std::ios::app); // 追加
if(!out.is_open()) // 打开文件检查
{
return; // 如果因权限或路径问题打开失败,则放弃本次写入,防止程序崩溃
}
// 将消息流式写入文件,并手动添加换行符以符合日志排版规范
out << message << "\n"; // 流式写入
// 文件流离开作用域或显式调用 close 会自动触发刷新并关闭文件
out.close();
}
private:
std::string _logdir; // 存储目录路径
std::string _logfilename; // 存储文件名称
Mutex _mutex; // 用于保障当前策略类实例在多线程环境下的线程安全
};
}
核心设计解析:
- 自动目录创建 :构造函数中检查日志目录是否存在,不存在则通过
create_directories递归创建,避免手动创建目录的繁琐 - 追加模式写入 :使用
std::ios::app打开文件,所有日志都会追加到文件末尾,不会覆盖历史日志,符合日志系统的通用规范 - 异常处理 :目录创建时捕获
filesystem的异常,避免目录创建失败导致程序崩溃 - 线程安全:文件写入全程加锁,保证多线程环境下不会出现半行日志、内容交错的问题


测试代码
cpp
#include <iostream>
#include <memory>
#include <unistd.h>
#include "Logger.hpp"
using namespace LogModule;
// 测试刷新策略
void testStrategy()
{
std::string message1 = "console: hello Log, hello Lotso!";
std::string message2 = "file: hello Log, hello Lotso!";
std::unique_ptr<LogStrategy> strategy = std::make_unique<ConsoleLogStrategy>(); // 父类指针指向子类对象
strategy->SyncLog(message1);
strategy->SyncLog(message1);
strategy->SyncLog(message1);
strategy->SyncLog(message1);
strategy->SyncLog(message1);
strategy = std::make_unique<FileLogStrategy>(); // 父类指针指向子类对象
strategy->SyncLog(message2);
strategy->SyncLog(message2);
strategy->SyncLog(message2);
strategy->SyncLog(message2);
strategy->SyncLog(message2);
}
int main()
{
// 3. 测试策略
testStrategy();
return 0;
}

四. 日志主体类与流式输出设计
完成基础模块和策略模式的实现后,我们来实现日志系统的主体类,核心目标是:兼容 glog 的流式调用风格、自动拼接日志元信息、RAII 自动触发日志刷新。
4.1 Logger 主类的整体架构
Logger类是日志系统的对外入口,核心职责包括:
- 管理当前使用的日志刷新策略,支持动态切换
- 提供仿函数接口,生成日志消息对象
- 封装策略切换的便捷接口
4.2 LogMessage 内部类:RAII 实现日志自动刷新
LogMessage是Logger的内部类,是整个日志系统最巧妙的设计:
- 构造函数中完成日志元信息(时间、等级、PID、文件名、行号)的拼接
- 重载
<<运算符,支持流式拼接任意类型的日志内容 - 析构函数中自动触发日志刷新,利用临时对象的生命周期实现 "写完即刷新"
4.3 完整的 Logger 类实现
cpp
#include <memory>
#include <sstream>
#include <unistd.h>
cpp
namespace LogModule // 定义日志模块命名空间
{
/**
* @brief Logger 类:日志系统的核心统筹管理者
* 负责维护日志刷新策略(显示器/文件)并作为产生日志消息的入口
*/
class Logger
{
public:
// 构造函数:初始化时默认开启控制台刷新策略
Logger()
{
UseConsoleLogStrategy();
}
// 切换策略:动态更换为控制台输出策略
void UseConsoleLogStrategy()
{
_strategy = std::make_unique<ConsoleLogStrategy>();
}
// 切换策略:动态更换为文件输出策略
void UseFileLogStrategy()
{
_strategy = std::make_unique<FileLogStrategy>();
}
~Logger(){};
/**
* @brief LogMessage 内部类:代表单条日志消息的生命周期管理
* 核心设计思想:利用临时对象的生命周期(RAII)实现日志的自动组装与刷新
*/
class LogMessage
{
public:
/**
* @brief 构造函数:构建日志的"左半部分"(前缀信息)
* 包括时间、等级、PID、文件名和行号,并预置到流中
*/
LogMessage(LogLevel level, const std::string &filename, int line, Logger &self)
: _currenttime(GetTimeStamp()) // 获取当前格式化时间
, _loglevel(LogLevel2String(level)) // 等级转字符串
, _pid(getpid()) // 获取进程 PID
, _filename(filename)
, _line(line)
, _logger(self) // 持有外部统帅类的引用,用于后续刷新
{
std::stringstream ss;
// 像拼积木一样组装固定格式的前缀
ss << "[" << _currenttime << "] "
<< "[" << _loglevel << "] "
<< "[" << _pid << "] "
<< "[" << _filename << "] "
<< "[" << _line << "] "
<< "- ";
_loginfo = ss.str(); // 直接拼上去
}
/**
* @brief 析构函数:整个设计的灵魂(RAII 自动刷新)
* 当这一行日志代码执行完毕(临时对象销毁)时,自动触发物理刷新
*/
~LogMessage()
{
if(_logger._strategy)
{
// 走到尽头了,调用刷新策略刷新出来
_logger._strategy->SyncLog(_loginfo);
}
}
/**
* @brief 模板重载 <<:构建日志的"右半部分"(用户自定义内容)
* 利用模板接纳任意类型,并通过 stringstream 实现安全的字符串转换与链式拼接
*/
template <typename T>
LogMessage& operator << (const T& info)
{
std::stringstream ss;
ss << info; // 自动处理 int, double, string 等类型转换
_loginfo += ss.str(); // 将内容追加到完整日志字符串中
return *this; // 返回自身引用,支持像 cout 一样的连续 << 调用
}
private:
std::string _currenttime;
std::string _loglevel;
int _pid;
std::string _filename;
int _line;
std::string _loginfo; // 存储整条待刷新的日志字符串
Logger &_logger; // 外部类引用:让消息知道自己隶属于哪个 Logger
};
/**
* @brief 仿函数重载:作为"桥梁"连接宏调用与内部消息对象
* Logger对象打印日志的时候,故意返回一个LogMessage的临时对象
*/
LogMessage operator() (LogLevel level, const std::string filename, int line)
{
// 创建并返回临时对象,开启后续的流式 << 操作
return LogMessage(level, filename, line, *this);
}
private:
std::unique_ptr<LogStrategy> _strategy; // 多态策略指针:决定日志去向
};
// 定义一个全局模块的Logger对象, 方便后续的使用
Logger logger;
/**
* @brief LOG 宏:对外提供的极简调用接口
* 自动捕获当前代码的 __FILE__ (文件名) 和 __LINE__ (行号)
*/
#define LOG(level) logger(level, __FILE__, __LINE__)
// 还是使用宏: 提供动态调整日志策略的便捷开关
#define ENABLE_CONSOLE_LOG_STRATEGY() logger.UseConsoleLogStrategy()
#define ENABLE_FILE_LOG_STRATEGY() logger.UseFileLogStrategy()
}
核心设计深度解析:
- 仿函数机制 :重载
operator()让Logger对象可以像函数一样调用,返回一个LogMessage临时对象,这是实现流式调用的核心 - RAII 自动刷新 :
LogMessage是临时对象,当整条LOG(xxx) << "xxx"语句执行完毕后,临时对象会被析构,析构函数中自动调用策略的刷新接口,无需用户手动触发刷新 - 模板化流式运算符 :通过模板重载
<<运算符,支持 int、double、string、char 等任意可流输出的类型,实现和std::cout一致的使用体验



- 预定义宏封装 :
__FILE__:编译期自动替换为当前源文件名__LINE__:编译期自动替换为当前代码行号LOG宏将繁琐的参数传递封装为极简的调用方式,完全对齐 glog 的使用风格
- 全局单例 :定义全局的
logger对象,整个程序共用一个日志实例,避免重复创建,同时保证策略切换全局生效


测试代码
cpp
#include <iostream>
#include <memory>
#include <unistd.h>
#include "Logger.hpp"
using namespace LogModule;
// 总体测试
void test()
{
// 开启控制台策略
ENABLE_CONSOLE_LOG_STRATEGY();
LOG(LogLevel::DEBUG) << "CONSOLE: hello Lotso " << 7.9 << " bd";
LOG(LogLevel::INFO) << "CONSOLE: hello Lotso " << 7.9 << " bd";
LOG(LogLevel::WARNING) << "CONSOLE: hello Lotso " << 7.9 << " bd";
LOG(LogLevel::ERROR) << "CONSOLE: hello Lotso " << 7.9 << " bd";
LOG(LogLevel::FATAL) << "CONSOLE: hello Lotso " << 7.9 << " bd";
// 开启文件策略
ENABLE_FILE_LOG_STRATEGY();
LOG(LogLevel::DEBUG) << "FILE: hello Lotso " << 7.9 << " bd";
LOG(LogLevel::INFO) << "FILE: Lotso " << 7.9 << " bd";
LOG(LogLevel::WARNING) << "FILE: hello Lotso " << 7.9 << " bd";
LOG(LogLevel::ERROR) << "FILE: hello Lotso " << 7.9 << " bd";
LOG(LogLevel::FATAL) << "FILE: hello Lotso " << 7.9 << " bd";
}
int main()
{
// 4. 整体测试
test();
return 0;
}

五. 日志系统的线程安全与可重入性深度解析
多线程环境下,日志系统的线程安全是重中之重,我们从多个维度做了全面保障:
- 临界资源的互斥保护:控制台、日志文件都是全局临界资源,所有写入操作都通过互斥锁保证原子性,同一时刻只有一个线程能执行写入操作,彻底避免内容交错、乱序。
- 可重入函数的使用 :时间戳获取使用
localtime_r而非不可重入的localtime,避免多线程同时调用时出现时间数据错乱;所有函数均不使用全局静态变量,所有状态都保存在对象内部,保证重入安全。 - 锁的粒度与 RAII 管理 :锁的粒度严格控制在写入操作的最小范围,避免长时间持有锁导致性能下降;同时通过
LockGuard的 RAII 机制保证锁一定会被释放,即使写入过程中出现异常,也不会出现死锁。 - 无锁的日志格式化阶段 :日志的格式化(拼接头部、用户内容)是在每个线程的
LogMessage临时对象中完成的,每个线程的日志格式化完全独立,没有共享资源竞争,无需加锁,最大化提升了并发性能。
六. 实战:日志系统完整使用示例(附带完整Logger.hpp代码呈现)
我们通过一个完整的示例,展示日志系统的基础使用、策略切换、多线程安全验证。
6.1 完整Logger.hpp代码
cpp
#ifndef LOGGER_HPP
#define LOGGER_HPP
#include <fstream>
#include <iostream>
#include <ctime>
#include <cstdio>
#include <memory>
#include <sstream>
#include <string>
#include <filesystem>
#include <unistd.h>
#include "Mutex.hpp"
namespace LogModule
{
// 1. 获取时间
std::string GetTimeStamp()
{
time_t currentTime = time(nullptr); // 默认获取当前时区的时间
// 我们希望把这个时间转换成年-月-日 时:分:秒
struct tm dataTime;
// 使用线程安全的版本 localtime_r,防止在多线程并发获取时间时
// 因为共享静态全局变量而导致的时间数据覆盖错乱。
localtime_r(¤tTime, &dataTime);
char dataTimeStr[128];
// 使用 snprintf 保证缓冲区不溢出,%02d 确保时间位宽不足时自动补0(如09秒)
snprintf(dataTimeStr, sizeof(dataTimeStr), "%4d-%02d-%02d %02d:%02d:%02d",
dataTime.tm_year + 1900, // tm_year 是从1900年开始计算的偏移量
dataTime.tm_mon + 1, // tm_mon 范围是 [0, 11],需加1修正
dataTime.tm_mday,
dataTime.tm_hour,
dataTime.tm_min,
dataTime.tm_sec
);
return dataTimeStr;
}
// 2. 日志等级 -- 枚举类型(整数)转换成字符串类型
// 使用 enum class 强类型枚举,避免命名污染,提高类型检查的严谨性
enum class LogLevel
{
DEBUG,
INFO,
WARNING,
ERROR,
FATAL
};
/**
* @brief 辅助函数:将枚举常量映射为可读字符串
* 解决强类型枚举无法直接通过 std::cout 打印的问题
*/
std::string LogLevel2String(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";
}
}
// 3. 刷新策略
// 基类: 策略模式
// 设计意图:将"日志消息的生成"与"日志消息的输出去向"解耦,方便后续扩展网络、数据库等输出端
class LogStrategy
{
public:
// 虚析构函数:确保通过基类指针释放子类对象时,子类的资源(如文件句柄)能被正确释放
virtual ~LogStrategy() = default; // 不在这里析构
// 纯虚函数:定义统一的刷新接口规范
virtual void SyncLog(const std::string &message) = 0; // 强制子类对其进行重写
};
// 策略1: 控制台日志策略
// 子类
class ConsoleLogStrategy: public LogStrategy
{
public:
ConsoleLogStrategy(){}
~ConsoleLogStrategy(){}
void SyncLog(const std::string &message) override // 检查重写的错误
{
// 显示器在多线程下是"临界资源",加锁防止多线程输出字符交织(Interleaving)
LockGuard logGuard(&_mutex);
std::cout << message << std::endl;
}
private:
Mutex _mutex;
};
const static std::string gdefaultlogdir = "./log/";
const static std::string gdefaultlogfilename = "log.txt";
// 策略2:文件类日志策略
// 子类
class FileLogStrategy: public LogStrategy
{
public:
// 构造函数:初始化路径并利用 C++17 库确保目录环境就绪
FileLogStrategy(const std::string &logdir = gdefaultlogdir, const std::string &logfilename = gdefaultlogfilename)
:_logdir(logdir),
_logfilename(logfilename)
{
// 创建目录前加锁,防止多线程同时执行判断与创建操作引发的竞态条件
LockGuard lockGuard(&_mutex);
if(std::filesystem::exists(_logdir))
{
return;
}
else
{
try
{
// 递归创建目录(mkdir -p),若权限不足或磁盘满会抛出异常
std::filesystem::create_directories(_logdir);
}
catch (std::filesystem::filesystem_error &e)
{
std::cerr << e.what() << std::endl;
}
}
}
~FileLogStrategy(){}
void SyncLog(const std::string &message) override
{
// 文件 I/O 是昂贵的临界资源操作,加锁保证单条日志写入的原子性
LockGuard logGuard(&_mutex);
std::string target = _logdir + _logfilename;
// 使用 std::ios::app (append) 追加模式,保证新旧日志共存而不被覆盖
std::ofstream out(target, std::ios::app); // 追加
if(!out.is_open()) // 打开文件
{
return;
}
out << message << "\n"; // 流式写入并换行
out.close(); // 关闭文件流,触发缓冲区刷新
}
private:
std::string _logdir;
std::string _logfilename;
Mutex _mutex;
};
/**
* @brief Logger 类:日志系统的中央控制器
* 内部嵌套了 LogMessage 类来实现精妙的 RAII 自动刷新机制
*/
class Logger
{
public:
Logger()
{
UseConsoleLogStrategy(); // 默认策略
}
void UseConsoleLogStrategy()
{
_strategy = std::make_unique<ConsoleLogStrategy>();
}
void UseFileLogStrategy()
{
_strategy = std::make_unique<FileLogStrategy>();
}
~Logger(){};
// 内部类:负责单条日志的组装和析构刷新
class LogMessage
{
public:
// 构造函数:预组装日志"前缀"部分
LogMessage(LogLevel level, const std::string &filename, int line, Logger &self)
: _currenttime(GetTimeStamp())
, _loglevel(LogLevel2String(level))
, _pid(getpid())
, _filename(filename)
, _line(line)
, _logger(self) // 保存引用,以便在析构时找到所属的 Logger 进行刷新
{
std::stringstream ss;
ss << "[" << _currenttime << "] "
<< "[" << _loglevel << "] "
<< "[" << _pid << "] "
<< "[" << _filename << "] "
<< "[" << _line << "] "
<< "- ";
_loginfo = ss.str(); // 此时前缀已拼入缓冲区
}
/**
* @brief 核心设计:RAII 机制触发刷新
* 当 LOG(...) << "msg"; 这行语句执行完毕,临时对象生命周期结束,
* 在析构函数中调用策略接口,保证日志在写完即刻、必然被刷出。
*/
~LogMessage()
{
if(_logger._strategy)
{
// 走到尽头了,调用刷新策略刷新出来
_logger._strategy->SyncLog(_loginfo);
}
}
// 用模版重载 << 运算符:接纳各种类型(int, string, double等)
template <typename T>
LogMessage& operator << (const T& info)
{
std::stringstream ss;
ss << info; // 自动完成类型转换
_loginfo += ss.str(); // 追加到内容主体中
return *this; // 返回引用支持链式调用,如 LOG << a << b << c;
}
private:
std::string _currenttime;
std::string _loglevel;
int _pid;
std::string _filename;
int _line;
std::string _loginfo;
Logger &_logger; // 外部类引用:用于访问具体刷新策略
};
/**
* @brief 重载仿函数 operator()
* 这是桥梁:将宏参数传入,并返回一个持有 Logger 权限的临时消息对象
*/
LogMessage operator() (LogLevel level, const std::string filename, int line)
{
return LogMessage(level, filename, line, *this);
}
private:
// 使用 unique_ptr 配合策略基类实现运行时多态
std::unique_ptr<LogStrategy> _strategy; // 策略
};
// 定义一个全局模块的Logger对象, 方便后续的使用
Logger logger;
// 定义宏:捕获编译器内置变量 __FILE__ 和 __LINE__,简化用户调用 API
#define LOG(level) logger(level, __FILE__, __LINE__)
// 便捷切换输出目的地的宏定义
#define ENABLE_CONSOLE_LOG_STRATEGY() logger.UseConsoleLogStrategy()
#define ENABLE_FILE_LOG_STRATEGY() logger.UseFileLogStrategy()
}
#endif
6.2 完整测试代码
cpp
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include "Logger.hpp" // 我们实现的日志头文件
using namespace LogModule;
// 多线程测试函数:10个线程同时打印日志
void *thread_log_test(void *arg)
{
char *thread_name = (char *)arg;
for (int i = 0; i < 5; i++)
{
LOG(LogLevel::INFO) << thread_name << " 执行日志打印, 循环次数: " << i;
usleep(1000);
}
return nullptr;
}
int main()
{
// 1. 基础控制台日志输出
std::cout << "===== 控制台日志测试 =====" << std::endl;
ENABLE_CONSOLE_LOG_STRATEGY();
LOG(LogLevel::DEBUG) << "这是DEBUG调试日志, 数值: " << 3.14159;
LOG(LogLevel::INFO) << "这是INFO常规日志, 服务启动成功";
LOG(LogLevel::WARNING) << "这是WARNING警告日志, 配置缺失, 使用默认值";
LOG(LogLevel::ERROR) << "这是ERROR错误日志, 文件读取失败";
LOG(LogLevel::FATAL) << "这是FATAL致命日志, 内存耗尽, 服务退出";
// 2. 切换为文件日志策略
std::cout << "\n===== 文件日志测试 =====" << std::endl;
ENABLE_FILE_LOG_STRATEGY();
LOG(LogLevel::INFO) << "切换为文件日志策略, 日志将持久化到./log/log.txt";
LOG(LogLevel::DEBUG) << "文件日志测试, 支持链式拼接: " << "字符串 " << 1234 << " 浮点数 " << 2.71828;
// 3. 多线程线程安全测试
std::cout << "\n===== 多线程日志测试 =====" << std::endl;
pthread_t t1, t2, t3, t4;
pthread_create(&t1, nullptr, thread_log_test, (void *)"thread-1");
pthread_create(&t2, nullptr, thread_log_test, (void *)"thread-2");
pthread_create(&t3, nullptr, thread_log_test, (void *)"thread-3");
pthread_create(&t4, nullptr, thread_log_test, (void *)"thread-4");
// 等待所有线程执行完毕
pthread_join(t1, nullptr);
pthread_join(t2, nullptr);
pthread_join(t3, nullptr);
pthread_join(t4, nullptr);
LOG(LogLevel::INFO) << "多线程日志测试完成, 无乱序、无交错";
return 0;
}

6.3 进阶优化方向
本篇博客实现的日志系统已满足基础的工业级需求,还可以从以下方向做进阶优化,适配更高性能、更复杂的业务场景:
- 异步日志机制:当前是同步写入,磁盘 IO 会阻塞业务线程。可实现双缓冲区异步日志,业务线程将日志写入内存缓冲区,后台线程专门负责磁盘写入,彻底消除 IO 阻塞。
- 日志滚动与分片:支持按文件大小、按天 / 小时切割日志文件,避免单个日志文件过大,同时支持过期日志自动清理。
- 日志分级过滤:支持设置全局日志等级,比如生产环境关闭 DEBUG 级日志,减少日志量和 IO 开销。
- 更多策略扩展:实现网络日志策略(上报到日志中心)、数据库日志策略、syslog 系统日志策略等,基于策略模式可无缝扩展。
- 线程 ID 打印:新增线程 ID 字段,多线程环境下问题定位更精准。
- 日志格式化优化:支持用户自定义日志格式,适配不同的日志采集规范。
结尾:
html
🍓 我是草莓熊 Lotso!若这篇技术干货帮你打通了学习中的卡点:
👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长
❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量
⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用
💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑
🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解
技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标!
结语:从零实现一个线程安全的日志系统,不仅是造轮子的过程,更是对 Linux 多线程编程、设计模式、C++RAII 机制、可重入函数等核心知识点的深度实践。本文基于策略模式实现的日志系统,做到了格式化与刷新逻辑解耦、线程安全、使用便捷、可无限扩展,完全兼容 glog 的流式使用风格,同时通过源码级的拆解,让我们理解了成熟日志库背后的底层实现原理。在实际开发中,我们可以直接使用 spdlog 等成熟的开源库,但只有理解了底层实现,才能在遇到日志乱序、程序崩溃日志丢失、多线程性能瓶颈等问题时,精准定位并解决问题,这也是底层能力的核心价值。
✨把这些内容吃透超牛的!放松下吧✨ ʕ˘ᴥ˘ʔ づきらど
