简介这份资源面向使用 QT 框架进行 C 开发的初中级开发者聚焦日志记录这一常见但易被忽视的工程需求提供一套可复用的日志模块实现思路。压缩包共 6 个文件约 6KB以 cpp 与 h 源码为主辅以 pro 工程文件和说明文档结构精简便于直接导入 QT 工程阅读与调试。内容围绕日志路径设置、按文件大小滚动创建新日志文件、写入前检查文件体积、异常处理等核心环节展开并涉及日志级别、时间戳等可扩展特性帮助读者理解如何借助 QFile、QTextStream、QDir、QFileInfo 等类构建自定义日志系统。已有 2383 人学习适合希望提升软件可维护性与问题排查效率的开发者参考借鉴将日志管理能力落地到自己的项目中。1. 日志落盘这件小事为什么值得单独拆一个项目线上跑着的 Qt 客户端最怕的不是崩溃是崩溃之后翻遍目录找不到一条能对上时间的日志。我见过太多项目把 qDebug 直接往控制台一扔开发机上看着挺热闹装到客户机器上就变成黑匣子——出问题时只能靠猜。这个项目的切入点很实在用 Qt C 做一套按条件自动切分的日志落盘机制日志文件不是固定写死一个 app.log而是根据日期、大小、模块名等条件动态创建新文件写到一定边界就换下一个。它适合两类人一类是刚接手 Qt 桌面端、需要给现有工程补日志能力的新手另一类是想把日志模块做成可复用组件、后面还要接上报和归档的熟手。下面我按「它怎么组织 → 怎么跑起来 → 核心代码长什么样 → 怎么改」的顺序拆中间会穿插我自己踩过的坑。2. 架构与目录日志模块在 Qt 工程里怎么摆2.1 模块分层与职责边界这类日志组件在 Qt 工程里通常不塞进主窗口而是独立成一个log子目录编译成静态库或直接以源码形式被主工程 include。分层上我一般拆成三层最底下是文件写入层只负责打开 QFile、写 QTextStream、flush中间是策略层决定「什么时候该新建文件」也就是标题里说的「根据条件自动创建」最上面是门面层对外暴露LOG_INFO、LOG_ERROR这类宏业务代码只调宏不碰文件对象。这样拆的好处是策略可替换。今天按天切明天产品说单文件不能超过 10MB你只动策略层门面层的宏一行不改。常见做法是把策略抽象成一个纯虚基类具体策略继承它工厂根据配置返回实例。// logstrategy.h —— 策略层接口只回答要不要换文件 class LogStrategy { public: virtual ~LogStrategy() default; // 返回 true 表示当前文件已到边界需要滚动 virtual bool shouldRoll(qint64 currentSize, const QDateTime now) 0; // 新文件名的生成规则也交给策略 virtual QString nextFileName(const QString base, const QDateTime now) 0; };shouldRoll拿到当前文件字节数和当前时间两个入参按天切的策略只看日期是否跨天按大小切的策略只看currentSize是否超过阈值。nextFileName把命名规则也收进策略避免文件名拼接散落在各处。参数说明base是配置里的基础名如appnow由调用方传入而不是策略内部取是为了方便单元测试时注入固定时间。2.2 目录结构与构建接入一个能直接塞进现有工程的目录大概长这样.pro或CMakeLists.txt里把log/整个加进去即可project/ ├── log/ │ ├── logger.h # 门面单例 宏定义 │ ├── logger.cpp │ ├── logstrategy.h # 策略基类 │ ├── dailystrategy.h # 按天切 │ ├── sizestrategy.h # 按大小切 │ └── logconfig.h # 配置结构体 ├── main.cpp └── mainwindow.cpplogconfig.h里放一个纯数据结构字段包括日志根目录、基础文件名、最小级别、单文件上限、保留天数。用结构体而不是散落的全局变量是为了后面从 ini/json 读配置时能一次性灌进去。构建接入时注意一点如果编成静态库宏里用到的Logger::instance()必须保证符号可见我一般干脆不编库直接把log/源码加进主工程省得处理导出宏。2.3 线程安全的落盘路径Qt 客户端里日志调用可能来自 UI 线程、工作线程、网络回调写入层必须加锁。最省事的做法是Logger内部持一把QMutexwrite全程QMutexLocker。但锁粒度别太大——格式化字符串在锁外做只有真正写文件和判断滚动放在锁内。下面这段是门面层写入的核心骨架void Logger::write(Level lv, const QString tag, const QString msg) { if (lv m_config.minLevel) return; // 级别过滤锁外做 const QString line formatLine(lv, tag, msg); // 拼时间戳、级别、线程id QMutexLocker locker(m_mutex); // 从这里开始临界区 if (!m_file.isOpen()) openNewFile(QDateTime::currentDateTime()); QTextStream ts(m_file); ts line \n; ts.flush(); // 关键崩溃前的最后一行靠它 m_currentSize line.toUtf8().size() 1; if (m_strategy-shouldRoll(m_currentSize, QDateTime::currentDateTime())) { rollFile(); // 关旧文件、开新文件 } }逻辑说明先做级别过滤能挡掉大量 debug 输出避免无谓加锁formatLine里带上毫秒时间戳和QThread::currentThreadId()排查多线程问题时这两个字段救命flush每行都调会有一点性能损耗但对客户端日志量完全可接受换来的是进程被强杀时最后几行不丢。参数上m_currentSize用 UTF-8 字节数累加而不是QString::size()因为中文日志按字符算会低估实际文件大小滚动阈值就对不上了。3. 跑起来从零到第一条日志落盘3.1 环境准备与最小可运行工程需要 Qt 5.12 以上Qt 6 也能编注意QTextStream的编码设置在 Qt 6 里默认就是 UTF-8。新建一个 Qt Widgets 或 Console 工程都行把log/目录拷进去.pro里加QT coreSOURCES和HEADERS补上对应文件。最小验证不用起界面main.cpp里初始化后打几条日志就够int main(int argc, char* argv[]) { QApplication app(argc, argv); LogConfig cfg; cfg.rootDir QCoreApplication::applicationDirPath() /logs; cfg.baseName app; cfg.minLevel Level::Info; cfg.maxSizeMB 5; // 单文件 5MB 触发滚动 Logger::instance().init(cfg); // 建目录、开首个文件 LOG_INFO(startup, 程序启动完成); LOG_ERROR(net, 连接超时重试第 1 次); return app.exec(); }init里要做三件事QDir().mkpath(cfg.rootDir)确保目录存在客户机器上目录不存在导致 QFile 打开失败是最常见的翻车点按策略算出首个文件名并打开把m_currentSize初始化为已有文件大小支持追加写而不是每次启动覆盖。跑起来后去applicationDirPath()/logs下应该能看到类似app_20240521.log的文件内容带时间戳和级别。3.2 自动创建文件的触发条件怎么配标题里的「根据条件自动创建」落地成配置就是几个字段的组合。我一般支持三种触发跨天、超大小、手动强制。跨天判断不能简单比较date().day()要比较完整日期否则跨月会漏超大小用字节数手动强制用于「用户点导出日志」这种场景直接调rollFile()。配置项类型作用常见取值rootDirQString日志根目录可执行文件同级 logsbaseNameQString文件名前缀app / clientminLevelLevel最低记录级别Info发布版/ Debug调试版maxSizeMBint单文件上限520keepDaysint保留天数超期清理730rollByDaybool是否跨天滚动truekeepDays的清理逻辑放在init和每次rollFile之后跑一次扫目录里匹配baseName_*.log的文件按文件名里的日期和当前日期比超期的QFile::remove。注意清理别放在写入临界区里做扫目录是 IO 操作会拖慢日志写入我一般丢到一个QTimer::singleShot(0, ...)里异步跑。3.3 验证滚动是否真的生效光看代码不算数得让它真滚一次。最直接的办法是把maxSizeMB临时改成 0 或者写个循环灌日志for (int i 0; i 200000; i) { LOG_INFO(stress, QString(压测日志行 %1).arg(i)); }跑完看目录应该出现app_20240521_001.log、app_20240521_002.log这种带序号的多个文件且每个文件大小接近但不超过阈值。如果只有一个文件一直变大八成是shouldRoll里m_currentSize没累加或者rollFile里新文件名算出来和旧的一样导致覆盖。验证跨天滚动更简单把系统时间往后调一天再打一条日志看是否生成新日期文件——调完记得调回来别问我怎么知道的。4. 核心代码拆解策略、命名与滚动4.1 按天与按大小两种策略实现策略层的两个实现都不长但边界条件要抠。按天策略的核心是比较「当前文件创建日期」和「现在日期」class DailyStrategy : public LogStrategy { public: explicit DailyStrategy(const QDate fileDate) : m_fileDate(fileDate) {} bool shouldRoll(qint64, const QDateTime now) override { return now.date() ! m_fileDate; // 只比日期不比时分秒 } QString nextFileName(const QString base, const QDateTime now) override { m_fileDate now.date(); // 滚动后更新基准日期 return QString(%1_%2.log).arg(base, now.toString(yyyyMMdd)); } private: QDate m_fileDate; };m_fileDate记录当前文件属于哪一天shouldRoll只做日期比较nextFileName里顺手把基准日期更新掉——这一步容易漏漏了就会每次写都判定要滚动疯狂建文件。按大小策略更简单shouldRoll返回currentSize m_limitnextFileName用「日期 三位序号」拼序号从当前目录里已有文件推算最大值加一避免重启后序号从 001 重来导致覆盖。4.2 文件名的生成与冲突处理文件名冲突是血泪经验重灾区。假设同一天内滚动多次序号必须单调递增如果程序重启得先扫目录拿到当天最大序号再续。下面这段是rollFile里的命名逻辑QString Logger::buildRollName(const QDateTime now) { const QString prefix QString(%1_%2) .arg(m_config.baseName, now.toString(yyyyMMdd)); QDir dir(m_config.rootDir); // 匹配 base_20240521_*.log取最大序号 const QStringList files dir.entryList( {prefix _*.log}, QDir::Files, QDir::Name); int seq 0; if (!files.isEmpty()) { const QString last files.last(); // Name 排序后最后一个最大 const QRegularExpression re(_(\\d{3})\\.log$); auto m re.match(last); if (m.hasMatch()) seq m.captured(1).toInt(); } return QString(%1_%2.log).arg(prefix) .arg(seq 1, 3, 10, QChar(0)); // 补零到三位 }逻辑说明entryList用QDir::Name排序三位补零保证字符串序和数字序一致取最后一个就是最大序号正则只认三位数字防止把别的文件误匹配进来。参数上seq 1后补零到三位意味着单日最多 999 个文件按 5MB 一个算够 5GB客户端场景绰绰有余真要更多就把\\d{3}和宽度一起改。4.3 门面宏与级别过滤业务代码不该关心Logger单例怎么拿宏包一层最舒服#define LOG_INFO(tag, msg) Logger::instance().write(Level::Info, tag, msg) #define LOG_WARN(tag, msg) Logger::instance().write(Level::Warn, tag, msg) #define LOG_ERROR(tag, msg) Logger::instance().write(Level::Error, tag, msg)tag用来标模块net、ui、db后面排查时grep一个 tag 就能捞出整条链路。级别过滤放在write入口发布版把minLevel设成 InfoDebug 日志在编译期其实还在只是运行时被挡掉——如果在意体积可以用#ifdef QT_DEBUG把LOG_DEBUG宏定义成空。这里有个细节宏参数里的msg如果是拼接表达式注意别在宏里对它求值两次上面写法只传一次没问题。5. 避坑与排查日志模块上线前必须过的几道坎5.1 现象日志文件是空的或只有第一行原因通常是QTextStream析构时机不对。如果每次write都在栈上新建QTextStream且没flush数据留在缓冲区进程正常退出时可能丢异常退出必丢。解决像 2.3 那样每行flush()或者把QTextStream作为成员持有、在rollFile和析构时显式flush。我倾向每行 flush客户端日志量撑得住。5.2 现象中文日志乱码Qt 5 里QTextStream默认编码跟系统 locale 走Windows 上可能是 GBKLinux 上是 UTF-8同一份代码两边表现不一致。解决打开文件后显式ts.setEncoding(QStringConverter::Utf8)Qt 6或ts.setCodec(UTF-8)Qt 5并且m_currentSize也按 UTF-8 字节算两边才对得上。5.3 现象多线程下日志串行、偶发崩溃原因一是没加锁二是锁内做了重活比如在临界区里扫目录清理旧文件。解决写入临界区只包「打开文件 写 累加大小 判断滚动」清理和格式化都挪到锁外Logger用Q_GLOBAL_STATIC或函数内静态局部变量保证单例初始化线程安全别用裸指针加if (!p) p new ...那种写法。5.4 现象滚动后旧文件被覆盖就是 4.2 说的序号问题——重启后序号从 001 重来正好撞上当天已有文件。解决buildRollName必须先扫目录取最大序号另外rollFile里打开新文件用QIODevice::WriteOnly | QIODevice::Append即使撞名也是追加而不是截断多一层保险。5.5 现象客户机器上根本没生成 logs 目录QFile::open不会自动建父目录目录不存在直接返回 false而很多人没检查返回值日志就静默丢了。解决init里mkpath并检查返回值openNewFile里open失败时至少往qWarning打一条别让失败无声无息。6. 二次开发把日志模块改成你要的样子改造方向我按投入产出排个序。最值得先做的是配置外置把LogConfig从代码里挪到QSettings或一个log.ini现场排查时不用重新编译就能把minLevel调成 Debug。读取逻辑放在init开头字段缺失就用默认值兜底别让一个拼错的 key 导致整个日志模块不工作。第二个方向是加一个内存环形缓冲。文件日志适合事后查但崩溃瞬间的上下文往往在最后几十行里而文件可能还没 flush 完。做法是维护一个固定长度的QQueueQStringwrite时同时塞一份崩溃处理器qInstallMessageHandler或平台崩溃回调里把队列整个 dump 到单独的crash_*.log。这个改动不大但排障体验提升明显。第三个方向是接入 Qt 自己的消息体系。用qInstallMessageHandler把qDebug/qWarning/qCritical重定向到你的Logger这样第三方库里的 qWarning 也能进文件不用挨个改调用点。注意 handler 里不能再调 qDebug否则递归handler 内部直接走Logger::write的底层函数。验证改造是否成功我习惯写一个小的自测函数在main里用QTimer定时打日志、定时改配置、定时模拟崩溃跑一晚上看第二天目录里的文件是否符合预期——滚动序号连续、没有空文件、crash 文件里有最后上下文。这套自测我一般留在工程里加个#ifdef LOG_SELFTEST开关后面每次动日志模块都跑一遍。说个我自己的教训早些年给一个客户端加日志图省事没做目录检查和 flush结果客户现场连续两次崩溃都没留下任何文件最后靠复现才定位。从那以后我每次接日志模块第一件事就是把「目录建没建、每行 flush 没 flush、多线程加锁没加锁」这三条强制走一遍再谈滚动和清理。这套按条件自动建文件的思路不复杂难的是边界都想到。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?