从零实现一个 C++日志库———策略模式 + RAII 实战
·
2026-05-28 日志库完整实现与详解
文件清单
Mutex.hpp — 互斥锁 + RAII 锁守卫
Logger.hpp — 日志系统(策略模式 + LogMessage 临时对象自动刷出)
一、Mutex.hpp
完整代码
#pragma once // 防止头文件被重复包含
#include <pthread.h> // POSIX 线程库:pthread_mutex_t 及相关函数
// ═══════════════════════════════════════════════
// Mutex — 互斥锁的 C++ 封装
// 把 C 风格的 pthread_mutex_t 包装成 C++ 对象
// 构造时 init,析构时 destroy,自动管理锁的生命周期
// ═══════════════════════════════════════════════
class Mutex
{
private:
pthread_mutex_t _lock; // 内核中真正的互斥锁变量
public:
// 构造:初始化锁
// pthread_mutex_init 第一个参数是锁的地址,第二个 nullptr = 默认属性(普通锁)
Mutex()
{
pthread_mutex_init(&_lock, nullptr);
}
// 析构:销毁锁
// 保证 Mutex 对象销毁时,内核中的锁资源也被回收
~Mutex()
{
pthread_mutex_destroy(&_lock);
}
// 加锁
// 如果锁空闲 → 立刻拿到锁,继续执行
// 如果锁被别人持有 → 阻塞等待,直到对方解锁,内核把自己唤醒
void Lock()
{
pthread_mutex_lock(&_lock);
}
// 解锁
// 释放锁。如果有其他线程在 lock() 里等着,内核会挑一个唤醒
void Unlock()
{
pthread_mutex_unlock(&_lock);
}
// 获取原始 pthread_mutex_t 指针
// 用途:某些 API(如 pthread_cond_wait)需要 pthread_mutex_t*,通过这个方法暴露出去
pthread_mutex_t *Get() { return &_lock; }
};
// ═══════════════════════════════════════════════
// LockGuard — RAII 锁守卫
// 构造时自动加锁,析构时自动解锁
// 保证"不管函数怎么退出,锁一定会被释放"
// ═══════════════════════════════════════════════
class LockGuard
{
private:
Mutex *_mutex; // 指向要管理的锁(不拥有所有权,只是借用)
public:
// 构造:保存锁的指针,然后立刻加锁
// 初始化列表先存指针,函数体再 Lock,保证顺序正确
LockGuard(Mutex *mutex) : _mutex(mutex)
{
_mutex->Lock();
}
// 析构:自动释放锁
// 不管函数是正常 return 还是抛异常,C++ 保证局部对象必定析构 → 锁必定释放
~LockGuard()
{
_mutex->Unlock();
}
// 禁止拷贝
// 如果允许拷贝:两个 LockGuard 指向同一把锁 → 第一个析构 Unlock → 第二个析构再 Unlock
// → 重复解锁,未定义行为
LockGuard(const LockGuard&) = delete;
LockGuard& operator=(const LockGuard&) = delete;
};
设计要点
为什么要封装 pthread_mutex_t?
C 风格的原生用法容易出错:
// C 风格 — 容易忘记 init/destroy/lock/unlock 中的一个
pthread_mutex_t lock;
pthread_mutex_init(&lock, nullptr); // 容易忘
pthread_mutex_lock(&lock);
// ... 临界区 ...
pthread_mutex_unlock(&lock); // 容易忘
pthread_mutex_destroy(&lock); // 容易忘
封装后用 C++ 对象生命周期自动管理:
Mutex lock; // 构造 → init
{
LockGuard guard(&lock); // 构造 → lock
// ... 临界区 ...
} // guard 析构 → unlock
// lock 析构 → destroy
为什么有两个类(Mutex + LockGuard)?
这两个类职责不同:
| Mutex | LockGuard | |
|---|---|---|
| 角色 | 锁本身(资源) | 锁的临时持有者(管家) |
| 生命周期 | 和拥有它的对象一样长 | 一次临界区的作用域 |
| 负责 | init / destroy | lock / unlock |
类比:
- Mutex = 你家的大门锁,一直装在门上
- LockGuard = 拿钥匙开门进去、出来锁门的那个人
RAII 的本质
构造 → 获取资源(init / lock / new)
析构 → 释放资源(destroy / unlock / delete)
C++ 保证:局部对象离开作用域必定析构
→ 资源必定释放,不会泄露
二、Logger.hpp
完整代码
#pragma once
// ====== 头文件 ======
#include <string> // std::string
#include <iostream> // std::cout, std::cerr, std::endl
#include <filesystem> // C++17 文件系统:exists(), create_directories()
#include <fstream> // std::ofstream 文件输出流
#include <memory> // std::unique_ptr, std::make_unique
#include <unistd.h> // getpid() 获取进程ID
#include <ctime> // time_t, localtime_r 时间处理
#include <sstream> // std::stringstream 字符串拼接
#include "Mutex.hpp" // 我们自己的锁封装
// ═══════════════════════════════════════════════
// LogLevel — 日志等级(强类型枚举)
// ═══════════════════════════════════════════════
enum class LogLevel
{
DEBUG, // 调试信息:开发阶段用的详细日志
INFO, // 正常运行信息:服务启动、新连接等
WARNING, // 警告:非致命但需关注的情况
ERROR, // 错误:功能受影响但程序还能跑
FATAL // 致命错误:程序即将崩溃
};
// ═══════════════════════════════════════════════
// Level2String — 把枚举等级转成人类可读的字符串
// ═══════════════════════════════════════════════
std::string Level2String(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";
}
}
// ═══════════════════════════════════════════════
// GetCurrentTime — 获取当前时间,格式化成字符串
// 输出格式:"2026-05-28 19:45:30"
// ═══════════════════════════════════════════════
std::string GetCurrentTime()
{
// 1. 拿到 Unix 时间戳(从 1970-01-01 到现在的秒数)
time_t currtime = time(nullptr);
// 2. 把时间戳拆成年月日时分秒
// localtime_r 是线程安全版本(_r = reentrant 可重入)
struct tm currtm;
localtime_r(&currtime, &currtm);
// 3. 格式化成字符串
// %4d:占 4 位整数(年份)
// %02d:占 2 位,不足前面补 0(月份、日期、时、分、秒)
// tm_year 从 1900 起算 → +1900
// tm_mon 从 0 起算(0=一月)→ +1
char timebuffer[64];
snprintf(timebuffer, sizeof(timebuffer),
"%4d-%02d-%02d %02d:%02d:%02d",
currtm.tm_year + 1900,
currtm.tm_mon + 1,
currtm.tm_mday,
currtm.tm_hour,
currtm.tm_min,
currtm.tm_sec);
return timebuffer;
}
// ═══════════════════════════════════════════════
// LogStrategy — 抽象基类(策略模式)
// 定义"日志刷到哪去"的接口
// 纯虚函数 SyncLog 强制子类必须实现
// ═══════════════════════════════════════════════
class LogStrategy
{
public:
// 虚析构:父类指针 delete 子类对象时,能正确调用子类的析构函数
virtual ~LogStrategy() = default;
// 纯虚函数:子类必须重写。参数是已经拼好的完整日志字符串
virtual void SyncLog(const std::string &logmessage) = 0;
};
// ═══════════════════════════════════════════════
// ConsoleLogStrategy — 控制台输出策略
// 把日志刷到标准输出(显示器)
// ═══════════════════════════════════════════════
class ConsoleLogStrategy : public LogStrategy
{
private:
Mutex _lock; // 多线程同时写控制台时需要加锁
// 否则两条日志会交错在一起,变成乱码
public:
void SyncLog(const std::string &logmessage) override
{
// 构造加锁,出作用域自动解锁
LockGuard lockguard(&_lock);
std::cout << logmessage << std::endl;
}
};
// ═══════════════════════════════════════════════
// FileLogStrategy — 文件输出策略
// 把日志追加写入磁盘文件
// ═══════════════════════════════════════════════
class FileLogStrategy : public LogStrategy
{
private:
Mutex _lock; // 保护文件操作(多线程同时写同一文件)
std::string _dir_path_name; // 日志目录路径,如 "/var/log/"
std::string _filename; // 日志文件名,如 "test.log"
public:
// 构造:指定日志目录和文件名(有默认值)
FileLogStrategy(const std::string &dir = "/var/log/",
const std::string &filename = "test.log")
: _dir_path_name(dir), _filename(filename)
{
// 创建目录时需要加锁,防止多线程同时 mkdir 导致竞态
LockGuard lockguard(&_lock);
// 如果目录已存在,什么都不做
if (std::filesystem::exists(_dir_path_name))
{
return;
}
// 递归创建目录(等价于 mkdir -p)
// 用 try-catch 包住,创建失败不会崩,只是文件日志功能不可用
try
{
std::filesystem::create_directories(_dir_path_name);
}
catch (const std::filesystem::filesystem_error &e)
{
std::cerr << "创建日志目录失败: " << e.what() << std::endl;
}
}
// 线程安全的文件写入
void SyncLog(const std::string &logmessage) override
{
// 加锁保护:防止多线程同时打开/写入/关闭同一文件
LockGuard lockguard(&_lock);
// 拼出完整路径:目录 + "/" + 文件名
std::string target = _dir_path_name + "/" + _filename;
// std::ios::app = append 模式:每次写入追加到文件末尾,不覆盖已有内容
std::ofstream out(target, std::ios::app);
if (!out.is_open()) // 打开失败就返回,别崩
{
return;
}
out << logmessage << std::endl; // 写入 + 换行
out.close(); // 显式关闭(不写也会在析构时关)
}
};
// ═══════════════════════════════════════════════
// Logger — 日志主类(策略模式的上下文)
// 持有输出策略,通过 operator() 创建临时 LogMessage
// ═══════════════════════════════════════════════
class Logger
{
public:
Logger() {}
// 切换到控制台输出
void EnableConsoleLogStrategy()
{
// make_unique 在堆上分配 ConsoleLogStrategy,所有权交给 _strategy
_strategy = std::make_unique<ConsoleLogStrategy>();
}
// 切换到文件输出
void EnableFileLogStrategy()
{
_strategy = std::make_unique<FileLogStrategy>();
}
// ═══════════════════════════════════════════════
// LogMessage — 日志消息临时对象
// 生命周期 = 一行日志的拼接过程
// 构造时拼日志头 → operator<< 拼内容 → 析构时刷出
// ═══════════════════════════════════════════════
class LogMessage
{
public:
// 构造:生成日志头并存入 _loginfo
LogMessage(LogLevel level, std::string filename, int line, Logger &logger)
: _curr_time(GetCurrentTime()), // 拿到当前时间
_level(level), // 日志等级
_pid(getpid()), // 当前进程 PID
_filename(filename), // 源文件名(通过 __FILE__ 传入)
_line(line), // 行号(通过 __LINE__ 传入)
_logger(logger) // 记住属于哪个 Logger(引用,必须初始化列表绑定)
{
// 拼出日志头:
// [2026-05-28 19:45:30][Info][12345][server.cc][42] -
// 时间 等级 进程号 文件 行号 分隔符
std::stringstream ss;
ss << "[" << _curr_time << "]"
<< "[" << Level2String(_level) << "]"
<< "[" << _pid << "]"
<< "[" << _filename << "]"
<< "[" << _line << "] - ";
_loginfo = ss.str();
}
// operator<< 模板:接受任意类型,转成字符串追加到日志后面
// 返回 LogMessage&(自己的引用)以支持链式调用
template <typename T>
LogMessage &operator<<(const T &info)
{
std::stringstream ss;
ss << info; // int/double/string 等自动转字符串
_loginfo += ss.str(); // 追加到日志尾部
return *this; // 返回自己,让下一个 << 继续拼接
}
// 析构:整条日志拼完了,刷出去!
// RAII 的应用:不管怎么退出,日志一定被刷出
~LogMessage()
{
// _strategy 可能为空(没调 EnableXxx()),检查一下
if (_logger._strategy)
{
_logger._strategy->SyncLog(_loginfo);
}
// 如果 _strategy 是 nullptr,这条日志就丢了(静默丢弃,不崩)
}
private:
std::string _curr_time; // 日志产生的时间
LogLevel _level; // 日志等级
pid_t _pid; // 进程 PID(区分多进程)
std::string _filename; // 源文件名
int _line; // 行号
std::string _loginfo; // 已拼好的完整日志字符串(头 + 体)
Logger &_logger; // 指向 Logger 的引用(析构时用它刷出)
};
// operator() — 仿函数
// 让 Logger 对象可以被"调用",返回一个临时 LogMessage
// 用法:logger(LogLevel::INFO, "server.cc", 42)
LogMessage operator()(LogLevel level, std::string filename, int line)
{
return LogMessage(level, filename, line, *this); // *this = Logger 自己
}
private:
// 输出策略:指向 ConsoleLogStrategy 或 FileLogStrategy 或 nullptr(未设置)
// unique_ptr 保证 Logger 析构时自动 delete 策略对象
std::unique_ptr<LogStrategy> _strategy;
};
// ═══════════════════════════════════════════════
// 全局 Logger 声明 + 便捷宏
// ═══════════════════════════════════════════════
// extern 声明:告诉编译器"logger 存在,但定义在某个 .cpp 文件中"
// 这样多个 .cpp include 此头文件时不会重复定义
extern Logger logger;
// LOG(等级) << "内容";
// 展开后:logger(LogLevel::INFO, __FILE__, __LINE__) << "内容";
// __FILE__:编译器替换为当前源文件名
// __LINE__:编译器替换为当前行号
#define LOG(level) logger(level, __FILE__, __LINE__)
// 快捷设置策略的宏
#define EnableConsoleLogStrategy() logger.EnableConsoleLogStrategy()
#define EnableFileLogStrategy() logger.EnableFileLogStrategy()
三、完整执行流程(一行日志的生命周期)
以这一行为例:
EnableConsoleLogStrategy();
LOG(LogLevel::INFO) << "服务端启动, 端口: " << 8080;
时序分解
步骤0:初始化(程序启动时,只做一次)
─────────────────────────────────────────
EnableConsoleLogStrategy();
└→ logger.EnableConsoleLogStrategy()
└→ _strategy = make_unique<ConsoleLogStrategy>()
ConsoleLogStrategy 在堆上创建,_strategy 持有它
步骤1:LOG 宏展开
─────────────────────────────────────────
LOG(LogLevel::INFO) << "服务端启动, 端口: " << 8080;
│
└→ logger(LogLevel::INFO, "server.cc", 42) << "服务端启动, 端口: " << 8080;
步骤2:operator() 调用 → 创建临时 LogMessage
─────────────────────────────────────────
logger(LogLevel::INFO, "server.cc", 42)
└→ return LogMessage(INFO, "server.cc", 42, *this);
LogMessage 构造:
_curr_time = GetCurrentTime() // "2026-05-28 19:45:30"
_level = LogLevel::INFO
_pid = getpid() // 12345
_filename = "server.cc"
_line = 42
_logger = *this // Logger 的引用
拼日志头 _loginfo =
"[2026-05-28 19:45:30][Info][12345][server.cc][42] - "
步骤3:operator<< 链式拼接内容
─────────────────────────────────────────
<< "服务端启动, 端口: "
└→ operator<<(const char*)
_loginfo += "服务端启动, 端口: "
return *this; // 返回自己引用
<< 8080
└→ operator<<(const int&)
_loginfo += "8080"
return *this; // 返回自己引用
此时 _loginfo =
"[2026-05-28 19:45:30][Info][12345][server.cc][42] - 服务端启动, 端口: 8080"
步骤4:分号 → LogMessage 析构 → 刷出
─────────────────────────────────────────
";" 到了 → 临时 LogMessage 对象析构
~LogMessage():
if (_logger._strategy) // 不是 nullptr ✓
_logger._strategy->SyncLog(_loginfo)
│
└→ ConsoleLogStrategy::SyncLog()
LockGuard lockguard(&_lock); // 加锁
std::cout << _loginfo << std::endl; // 输出到控制台
// lockguard 析构 → 解锁
控制台输出:
[2026-05-28 19:45:30][Info][12345][server.cc][42] - 服务端启动, 端口: 8080
四、为什么要加锁
不加锁会发生什么
假设两个线程同时写日志:
线程A: LOG(INFO) << "用户登录: " << user_id;
线程B: LOG(INFO) << "连接断开: " << fd;
时间线(不加锁):
A 写入 "[2026-05-28...][Info] 用户登录: "
B 切入,写入 "[2026-05-28...][Info] 连接断开: "
A 继续写入 "42"
B 继续写入 "5"
结果(两条日志搅在一起,无法阅读):
[2026-05-28][Info] 用户登录: [2026-05-28][Info] 连接断开: 425
加锁后
线程A: LockGuard(&_lock) → 拿到锁 → 写入完整日志 → 解锁
线程B: LockGuard(&_lock) → 阻塞等待 → A 解锁后拿到锁 → 写入 → 解锁
结果(两条完整日志,先后输出):
[2026-05-28][Info][12345][s.cc][10] - 用户登录: 42
[2026-05-28][Info][12345][s.cc][15] - 连接断开: 5
锁在哪里
ConsoleLogStrategy::SyncLog() — 保护 std::cout,多线程不会交叉输出
FileLogStrategy::SyncLog() — 保护文件写入,多线程不会交叉写入
FileLogStrategy 构造函数 — 保护 mkdir,多线程不会同时创建目录
三个锁是独立的(每个策略对象有自己的 Mutex),不会互相阻塞。
五、知识点清单
| 序号 | 知识点 | 出现的类/函数 |
|---|---|---|
| 1 | enum class 强类型枚举 |
LogLevel |
| 2 | 虚函数 + = 0 纯虚函数 |
LogStrategy |
| 3 | override 显式重写 |
ConsoleLogStrategy, FileLogStrategy |
| 4 | 虚析构函数 | LogStrategy |
| 5 | RAII(资源获取即初始化) | LockGuard, LogMessage |
| 6 | unique_ptr + make_unique |
Logger::_strategy |
| 7 | 策略模式(Strategy Pattern) | LogStrategy 体系 |
| 8 | template<typename T> 模板函数 |
LogMessage::operator<< |
| 9 | operator() 重载(仿函数) |
Logger::operator() |
| 10 | operator<< 重载(链式调用) |
LogMessage::operator<< |
| 11 | 析构函数自动触发 | LogMessage::~LogMessage |
| 12 | = default / = delete |
LogStrategy ~, LockGuard 拷贝 |
| 13 | extern 全局变量 |
extern Logger logger |
| 14 | __FILE__ / __LINE__ 预定义宏 |
LOG 宏 |
| 15 | stringstream 字符串拼接 |
LogMessage 构造 + operator<< |
| 16 | snprintf 安全格式化 |
GetCurrentTime |
| 17 | std::filesystem(C++17) |
FileLogStrategy 构造 |
| 18 | std::ofstream 文件输出流 |
FileLogStrategy::SyncLog |
| 19 | 类成员引用必须初始化列表绑定 | LogMessage::_logger |
| 20 | pthread_mutex_t 互斥锁 |
Mutex |
| 21 | localtime_r 线程安全时间转换 |
GetCurrentTime |
| 22 | getpid() 获取进程 PID |
LogMessage 构造 |
六、内存生命周期图
程序启动
│
├─ Logger logger 构造(全局,程序全程存活)
│ _strategy = nullptr
│
├─ EnableConsoleLogStrategy()
│ _strategy → new ConsoleLogStrategy(堆上,随 Logger 一起死亡)
│
├─ LOG(INFO) << "hello";
│ │
│ ├─ LogMessage 临时对象(栈上,这一行结束析构)
│ │ _logger → 指回 Logger
│ │ _loginfo = "拼好的日志字符串"
│ │
│ └─ ; → 析构 → SyncLog → 控制台输出
│
├─ LOG(WARNING) << "world";
│ │ 另一个临时 LogMessage(新的,和上面那个无关)
│ └─ ; → 析构 → SyncLog → 控制台输出
│
程序结束
│
└─ logger 析构
└─ _strategy(unique_ptr)析构 → delete ConsoleLogStrategy
关键:每个 LOG(xxx) << ... 创建一个全新的临时 LogMessage 对象,拼完就析构刷出。Logger 是唯一的长命对象。
七、测试代码
#include "Logger.hpp"
// 全局 Logger 的定义(整个程序只能有一处!)
Logger logger;
int main()
{
// 1. 设置输出策略(必须调,否则 _strategy 为空不会输出)
EnableConsoleLogStrategy();
// 2. 写日志
LOG(LogLevel::INFO) << "服务端启动";
LOG(LogLevel::DEBUG) << "调试信息: sockfd=" << 3;
LOG(LogLevel::WARNING) << "内存使用率超过 80%";
LOG(LogLevel::ERROR) << "连接超时: " << 30 << "秒";
LOG(LogLevel::FATAL) << "无法分配内存";
// 3. 切换到文件输出
EnableFileLogStrategy();
LOG(LogLevel::INFO) << "这条会写到 /var/log/test.log";
return 0;
}
编译:
g++ test.cc -o test -std=c++17 -lstdc++fs -lpthread
-lpthread 是因为 Mutex.hpp 用了 pthread 库。
八、与项目的关系
这个日志库会用在后续所有模块中,把 std::cout << ... 和 printf(...) 替换成 LOG(LogLevel::INFO) << ...。
好处:
- 统一格式:每条日志都有时间、等级、PID、文件、行号
- 可以切输出:调试时刷控制台,上线时刷文件
- 线程安全:多线程 Server 不会日志交叉
- 可以按等级过滤:后续可以加
SetLogLevel(LogLevel::WARNING)只输出 WARNING 及以上
更多推荐


所有评论(0)