当前位置: 首页 > news >正文

C++日志 2——实现单线程日志系统

在上一篇《C++ 日志 1—— 日志系统基础设计》中,我们梳理了日志系统的核心需求(日志等级、输出格式、持久化)和基础架构。本篇将基于基础设计,从零实现一个轻量、可用的单线程 C++ 日志系统,兼顾实用性和可扩展性,代码可直接嵌入项目使用。

一、单线程日志系统核心定位

单线程日志系统是日志系统的基础版本,仅支持在单个线程中写入日志,无需考虑线程安全问题,实现简单、性能高效,适合小型项目、工具类程序、单线程服务等场景。

核心功能点:

  1. 支持分级日志(DEBUG/INFO/WARN/ERROR/FATAL);
  2. 自定义日志格式(时间戳、日志等级、文件名、行号、日志内容);
  3. 支持控制台输出 + 文件持久化输出;
  4. 自动创建日志文件,支持日志内容追加;
  5. 接口简洁易用,无第三方依赖。

二、整体架构设计

我们采用单例模式设计日志类(全局唯一日志实例,避免多次创建日志对象),核心模块拆分:

  1. 日志等级枚举:定义日志优先级,过滤低等级日志;
  2. 日志核心类:封装日志初始化、日志写入、格式拼接、文件 / 控制台输出;
  3. 易用宏封装:简化日志调用,自动捕获文件名和行号。

三、完整代码实现

1. 头文件(Logger.h)

#ifndef LOGGER_H #define LOGGER_H #include <iostream> #include <fstream> #include <string> #include <ctime> #include <cstdarg> // 可变参数支持 // 日志等级枚举 enum LogLevel { DEBUG, // 调试信息 INFO, // 普通信息 WARN, // 警告信息 ERROR, // 错误信息 FATAL // 致命错误 }; // 单线程日志类(单例模式) class Logger { public: // 获取单例实例 static Logger& getInstance(); // 初始化日志:设置日志文件路径、最低输出等级 void init(const std::string& logFile, LogLevel level = INFO); // 核心日志写入函数 void log(LogLevel level, const char* file, int line, const char* format, ...); // 关闭日志(关闭文件句柄) void close(); private: // 私有构造/析构(单例禁止外部创建) Logger(); ~Logger(); // 禁止拷贝和赋值 Logger(const Logger&) = delete; Logger& operator=(const Logger&) = delete; // 工具函数:获取当前时间字符串 std::string getCurrentTime(); // 工具函数:日志等级转字符串 std::string levelToString(LogLevel level); private: std::ofstream m_logFile; // 日志文件流 LogLevel m_level; // 最低输出日志等级 bool m_isInit; // 初始化标记 }; // 易用宏封装:自动传入文件名和行号 #define LOG_DEBUG(format, ...) Logger::getInstance().log(DEBUG, __FILE__, __LINE__, format, ##__VA_ARGS__) #define LOG_INFO(format, ...) Logger::getInstance().log(INFO, __FILE__, __LINE__, format, ##__VA_ARGS__) #define LOG_WARN(format, ...) Logger::getInstance().log(WARN, __FILE__, __LINE__, format, ##__VA_ARGS__) #define LOG_ERROR(format, ...) Logger::getInstance().log(ERROR, __FILE__, __LINE__, format, ##__VA_ARGS__) #define LOG_FATAL(format, ...) Logger::getInstance().log(FATAL, __FILE__, __LINE__, format, ##__VA_ARGS__) #endif // LOGGER_H

2. 源文件(Logger.cpp)

#include "Logger.h" // 私有构造函数 Logger::Logger() : m_level(INFO), m_isInit(false) {} // 析构函数:自动关闭文件 Logger::~Logger() { close(); } // 获取单例实例(静态局部变量,线程安全在单线程下无问题) Logger& Logger::getInstance() { static Logger instance; return instance; } // 初始化日志 void Logger::init(const std::string& logFile, LogLevel level) { if (m_isInit) return; // 打开日志文件:追加模式 + 清空缓冲区 m_logFile.open(logFile, std::ios::app | std::ios::out); if (m_logFile.is_open()) { m_level = level; m_isInit = true; LOG_INFO("日志系统初始化成功,日志文件:%s", logFile.c_str()); } else { std::cerr << "日志文件打开失败:" << logFile << std::endl; } } // 关闭日志文件 void Logger::close() { if (m_logFile.is_open()) { m_logFile.close(); m_isInit = false; } } // 获取当前时间字符串(格式:YYYY-MM-DD HH:MM:SS) std::string Logger::getCurrentTime() { time_t now = time(nullptr); char buf[64] = {0}; strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", localtime(&now)); return std::string(buf); } // 日志等级转字符串 std::string Logger::levelToString(LogLevel level) { switch (level) { case DEBUG: return "DEBUG"; case INFO: return "INFO"; case WARN: return "WARN"; case ERROR: return "ERROR"; case FATAL: return "FATAL"; default: return "UNKNOWN"; } } // 核心日志写入函数 void Logger::log(LogLevel level, const char* file, int line, const char* format, ...) { // 1. 过滤低等级日志 if (!m_isInit || level < m_level) return; // 2. 拼接日志前缀:时间 [等级] (文件名:行号) std::string prefix = "[" + getCurrentTime() + "] [" + levelToString(level) + "] (" + std::string(file) + ":" + std::to_string(line) + ") "; // 3. 处理可变参数(格式化日志内容) char content[1024] = {0}; va_list args; va_start(args, format); vsnprintf(content, sizeof(content), format, args); va_end(args); // 4. 拼接完整日志 std::string logMsg = prefix + content + "\n"; // 5. 同时输出到控制台和文件 std::cout << logMsg; m_logFile << logMsg; // 立即刷新缓冲区(防止程序崩溃丢失日志) m_logFile.flush(); }

3. 测试代码(main.cpp)

#include "Logger.h" int main() { // 1. 初始化日志:输出到app.log,最低等级为DEBUG Logger::getInstance().init("app.log", DEBUG); // 2. 使用宏打印不同等级日志 LOG_DEBUG("这是一条调试日志,参数:%d,%s", 123, "测试"); LOG_INFO("服务启动成功,端口:%d", 8080); LOG_WARN("磁盘空间不足,剩余:%d%%", 10); LOG_ERROR("数据库连接失败,错误码:%d", 500); LOG_FATAL("程序崩溃,即将退出"); return 0; }

四、代码核心解析

1. 单例模式实现

通过静态局部变量实现单例,保证全局只有一个日志实例,避免多次打开日志文件、重复初始化等问题:

Logger& Logger::getInstance() { static Logger instance; return instance; }

单线程下无需加锁,性能最优。

2. 日志等级过滤

初始化时设置最低日志等级,低于该等级的日志会直接被过滤,灵活控制日志输出量:

cpp

// 过滤低等级日志 if (!m_isInit || level < m_level) return;

3. 日志格式拼接

固定输出格式:[时间戳] [日志等级] (文件名:行号) 日志内容,包含调试必备信息,方便快速定位问题。

4. 可变参数支持

使用cstdarg库实现可变参数,支持printf风格的格式化输出,使用成本极低:

va_list args; va_start(args, format); vsnprintf(content, sizeof(content), format, args); va_end(args);

5. 宏封装简化调用

通过宏自动捕获__FILE__(当前文件名)和__LINE__(当前行号),调用时无需手动传入:

#define LOG_DEBUG(format, ...) Logger::getInstance().log(DEBUG, __FILE__, __LINE__, format, ##__VA_ARGS__)

五、编译与运行

1. 编译命令

使用 g++ 编译,直接链接三个文件:

g++ main.cpp Logger.cpp -o logger_demo

2. 运行结果

控制台输出:
[2025-12-29 15:30:00] [DEBUG] (main.cpp:9) 这是一条调试日志,参数:123,测试 [2025-12-29 15:30:00] [INFO] (main.cpp:10) 服务启动成功,端口:8080 [2025-12-29 15:30:00] [WARN] (main.cpp:11) 磁盘空间不足,剩余:10% [2025-12-29 15:30:00] [ERROR] (main.cpp:12) 数据库连接失败,错误码:500 [2025-12-29 15:30:00] [FATAL] (main.cpp:13) 程序崩溃,即将退出
日志文件(app.log):

与控制台输出完全一致,实现日志持久化。

六、优缺点分析

优点

  1. 轻量无依赖:纯 C++ 标准库实现,无需引入第三方库;
  2. 易用性高:宏封装后一行代码即可打印日志;
  3. 功能完整:支持分级、格式化、控制台 + 文件输出;
  4. 性能高效:单线程无锁操作,无性能损耗。

缺点

  1. 无线程安全:多线程同时写入会导致日志乱序、文件损坏;
  2. 无日志切割:日志文件会持续增大,不适合长期运行的高并发项目;
  3. 缓冲区固定:单次日志内容超过 1024 字节会被截断。

七、优化方向(为多线程版本铺垫)

当前单线程版本已满足基础需求,后续可针对性优化:

  1. 线程安全:添加互斥锁,支持多线程写入;
  2. 日志切割:按文件大小 / 时间切割日志,避免单文件过大;
  3. 动态缓冲区:取消固定大小限制,支持任意长度日志;
  4. 配置化:通过配置文件设置日志路径、等级、输出方式;
  5. 异步写入:将日志写入放入队列,异步消费,提升性能。

总结

本篇实现的单线程日志系统,是 C++ 日志系统的基础核心,代码简洁、功能实用,完全满足单线程项目的日志需求。通过单例模式、日志分级、格式封装等设计,为后续多线程、异步日志系统打下了坚实基础。

http://www.jsqmd.com/news/765513/

相关文章:

  • 终极指南:如何彻底移除Windows 10/11中的Microsoft Edge浏览器
  • fre:ac:7个简单技巧让你成为音频转换专家
  • 前端工程师转型AI大模型应用:收藏这份高薪跃迁指南,3步成为AI应用架构师!
  • 性价比高的六角钻尾丝到底哪个靠谱?一文为你揭晓答案 - 品牌企业推荐师(官方)
  • 特朗普孤注一掷,美国要最后的疯狂了!
  • Windows系统vbame.dll文件丢失找不到无法启动解决
  • 2026岩茶加盟全扶持与全国联保深度评测:低门槛真能成就高端生意吗? - 商业科技观察
  • semi-utils:摄影师的终极批量水印解决方案
  • 热门收藏:小白程序员必看!AI Agent将如何重塑未来工作?
  • LeetCode HOT100 - 二叉树的层序遍历
  • 从信息论到PyTorch代码:手把手拆解CrossEntropyLoss,理解它为何是分类任务的‘万金油’
  • 鸣潮智能剧情助手:5分钟实现后台自动跳过与多账号管理
  • STM32F407项目实战:用模拟IIC点亮0.96寸OLED,手把手教你显示字符和数字
  • 必看!2026潘家园眼镜店推荐口碑TOP5:柏兰眼镜领衔1次配镜终身无忧 - 品牌企业推荐师(官方)
  • AI辅助开发:探索在快马生成的编辑器中集成智能写作与补全功能
  • 新手入门CV:手把手教你下载和使用ADE20K数据集(附Python解析代码)
  • Android?基础UI控件!!!
  • 2026年户外新宠:免搭建充气帐篷,3秒自动撑开 - 品牌企业推荐师(官方)
  • 盛瀚的色谱柱怎么样?和进口品牌对比,差距or惊喜? - 品牌推荐大师1
  • 从手机充电头到主板供电:拆解3个实物,看NMOS和PMOS在真实电路里怎么选型
  • Windows系统VB6CHS.DLL文件丢失找不到无法启动程序解决
  • 别再写IF+HASONEVALUE了!Power BI中SELECTEDVALUE函数的3个实战用法(含动态标题)
  • 洛谷 P5149:会议座位 ← 归并排序 + 逆序对
  • 2026河北石家庄银元回收指南:素军奢品汇古钱币纸币纪念钞回收须知 - 品牌企业推荐师(官方)
  • 架构师技能图谱解析:从微服务到云原生的系统化成长路径
  • 3分钟拯救你的B站收藏:m4s-converter让你的缓存视频重获新生!
  • AD21信号线束实战:从原理图到PCB,如何用它简化复杂接口设计(以USB_PHY为例)
  • 长期主义者的选择:哪些品牌的激光扫描仪在恶劣环境下依然稳定? - 品牌推荐大师
  • 河北邯郸企业认定市级、省级、国家级企业技术中心有多少奖补?
  • 最新!2026 北京配眼镜推荐TOP5实测:高性价比之王+专业验光不踩雷 - 品牌企业推荐师(官方)