☰
MFC项目集成log4cplus日志库实战:从编译到多线程日志管理
2026/10/21 7:58:36 网站建设 项目流程

1. 为什么要在MFC项目里引入log4cplus

做过Windows桌面开发的人大概都有这种体会:项目初期用OutputDebugString或者TRACE宏打印调试信息,跑得挺顺;等到项目膨胀到几十个cpp文件、几万行代码,某个偶现的崩溃或者数据异常找上门来,你才发现那些散落在各处的printf式日志根本不够用。没有日志级别、没有文件输出、没有时间戳、没有线程标识,排查问题基本靠猜。

MFC作为一套经典的Windows应用框架,本身并没有提供一套成熟的日志系统。TRACE宏只在Debug模式下有效,Release版本直接失效;AfxMessageBox会阻塞界面,生产环境根本不能用;自己写一个fopen+fprintf的简易日志类,又会遇到多线程写文件冲突、日志文件无限膨胀、格式不统一等一堆麻烦事。

log4cplus就是来解决这些问题的。它是Log4j的C++移植版本,提供了一套完整的日志框架:日志级别(TRACE/DEBUG/INFO/WARN/ERROR/FATAL)、多种输出目标(控制台、文件、滚动文件、系统日志等)、灵活的格式化布局、配置文件驱动、线程安全。把它集成到MFC项目里,基本上就是"一次配置,长期受益"。

这篇文章面向的是有一定MFC基础、但没怎么接触过第三方日志库的开发者。我会从零开始,讲清楚怎么把log4cplus编译进MFC工程、怎么初始化、怎么在对话框和文档类里打日志、怎么配置滚动文件避免日志撑爆硬盘,以及我在实际集成过程中踩过的几个坑。所有代码都是可以直接复制到项目里跑的,不需要你再去翻官方文档。

提示:log4cplus是开源库,遵循Apache License 2.0,商用项目也可以放心使用。本文基于log4cplus 2.x版本,1.x版本在API上有一些差异,后面会专门说明。

2. 把log4cplus编译成MFC能用的静态库

2.1 源码获取与目录结构梳理

log4cplus的源码可以从其官方仓库获取,下载下来解压后,你会看到这样的目录结构:

log4cplus-2.x.x/ ├── include/ │ └── log4cplus/ │ ├── logger.h │ ├── appender.h │ ├── layout.h │ └── ... ├── src/ │ ├── logger.cxx │ ├── appender.cxx │ └── ... ├── msvc/ │ └── log4cplus.sln └── ...

关键目录是include和src。include里是头文件,src里是源文件。msvc目录下有现成的Visual Studio解决方案,但直接用那个解决方案编译出来的库,默认是动态库(DLL)或者不匹配MFC的运行时库配置,直接拿来用很容易出现链接错误。

我的建议是:不要用现成的sln,而是把源码直接嵌入到你的MFC工程里,或者单独建一个静态库工程。这样做的好处是编译选项完全可控,不会出现"库用的是/MT,你的工程用的是/MD"这种运行时库不匹配的经典问题。

2.2 静态库工程的编译选项配置

如果你选择单独建一个静态库工程,步骤如下:

  1. 在Visual Studio中新建"Win32项目",选择"静态库",命名为log4cplus_static。
  2. 把include目录添加到工程的"附加包含目录"。
  3. 把src目录下所有的.cxx文件添加到工程的源文件中。
  4. 关键一步:配置运行时库。打开工程属性 → C/C++ → 代码生成 → 运行时库,选择多线程调试(/MTd)(Debug)或多线程(/MT)(Release)。

为什么必须选/MT而不是/MD?因为MFC项目默认使用/MT(静态链接MFC)或/MD(动态链接MFC)。如果你的MFC工程用的是/MT,而log4cplus库用的是/MD,链接时会出现_ITERATOR_DEBUG_LEVEL不匹配或者RuntimeLibrary冲突的错误。最稳妥的做法是让log4cplus的运行时库配置和你的MFC工程完全一致。

如果你不想单独建工程,也可以直接把log4cplus的源文件拖进你的MFC工程里一起编译。这样做的好处是省去了库链接的步骤,缺点是工程文件会变得比较大,编译时间会变长。对于中小型项目,我倾向于直接嵌入源码,简单粗暴,不容易出错。

2.3 预处理器定义与字符集处理

log4cplus在Windows下编译时,有几个预处理器定义需要注意:

  • LOG4CPLUS_STATIC:告诉log4cplus你用的是静态库,不是DLL。这个定义必须加,否则会出现__declspec(dllimport)相关的链接错误。
  • UNICODE和_UNICODE:如果你的MFC工程是Unicode字符集(现在基本都是),这两个定义要加上。log4cplus内部会根据这两个宏决定使用wchar_t还是char版本的API。
  • WIN32:Windows平台必须定义。

在工程属性 → C/C++ → 预处理器 → 预处理器定义中,加上:

LOG4CPLUS_STATIC UNICODE _UNICODE WIN32

字符集方面,MFC工程通常用Unicode。log4cplus在Unicode模式下,日志消息类型是log4cplus::tstring,也就是std::wstring。如果你从MFC的CString传日志消息,需要做一次转换。这个后面在代码里会具体讲。

2.4 编译过程中常见的链接错误与解决

第一次编译log4cplus静态库,大概率会遇到几个链接错误。我把最常见的几个列出来:

错误信息原因解决方法
无法解析的外部符号 __imp__WSAStartup缺少ws2_32.lib在链接器 → 输入 → 附加依赖项中加入ws2_32.lib
无法解析的外部符号 _timezone时区相关函数在静态库中不可用定义LOG4CPLUS_USE_SYSTEM_TIME或改用_timezone替代方案
LNK2005: _malloc 已经在 libcmt.lib 中定义运行时库冲突确保所有工程使用相同的运行时库配置
无法解析的外部符号 _snprintf旧版VS的snprintf问题在预处理器中定义_CRT_SECURE_NO_WARNINGS

其中ws2_32.lib是最容易漏掉的。log4cplus的SocketAppender需要网络支持,即使你不用SocketAppender,链接器也会尝试解析相关符号。加上这个库就没事了。

注意:如果你用的是Visual Studio 2015及以上版本,log4cplus 2.x的源码基本可以直接编译通过。如果是VS2010或更早版本,可能需要手动修改一些C++11特性的代码,比如nullptr、auto等。建议尽量用较新的VS版本。

3. 在MFC对话框程序里初始化log4cplus

3.1 初始化时机的选择:InitInstance还是对话框构造函数

log4cplus的初始化应该放在哪里?这是一个看似简单但实际很关键的问题。

放在CWinApp::InitInstance()里是最合理的。原因有三:第一,InitInstance在应用程序启动时只执行一次,适合做全局初始化;第二,此时MFC框架已经初始化完毕,可以安全使用MFC的字符串和文件API;第三,如果初始化失败,可以在InitInstance里直接返回FALSE,让程序优雅退出,而不是等到对话框弹出来才报错。

有些开发者习惯把日志初始化放在主对话框的OnInitDialog()里。这样做的问题是:如果对话框创建之前就发生了错误(比如配置文件读取失败),你根本没有日志可以看。所以,越早初始化越好。

一个典型的InitInstance初始化代码如下:

BOOL CMyApp::InitInstance() { // 初始化log4cplus log4cplus::Initializer initializer; // 创建控制台Appender(可选,方便调试) log4cplus::SharedAppenderPtr consoleAppender( new log4cplus::ConsoleAppender(false, true)); // 创建滚动文件Appender log4cplus::tstring logFile = _T("logs\\app.log"); log4cplus::SharedAppenderPtr fileAppender( new log4cplus::RollingFileAppender( logFile, // 文件名 10 * 1024 * 1024, // 最大文件大小:10MB 5, // 最大备份文件数 false, // 不立即刷新 true // 线程安全 )); // 设置布局格式 log4cplus::tstring pattern = LOG4CPLUS_TEXT("%D{%Y-%m-%d %H:%M:%S.%q} [%t] %-5p %c - %m%n"); fileAppender->setLayout( std::unique_ptr<log4cplus::Layout>( new log4cplus::PatternLayout(pattern))); // 获取根Logger并添加Appender log4cplus::Logger rootLogger = log4cplus::Logger::getRoot(); rootLogger.addAppender(fileAppender); rootLogger.setLogLevel(log4cplus::INFO_LOG_LEVEL); // 如果需要在Debug模式下输出到控制台 #ifdef _DEBUG rootLogger.addAppender(consoleAppender); #endif LOG4CPLUS_INFO(rootLogger, LOG4CPLUS_TEXT("应用程序启动")); CMyDlg dlg; m_pMainWnd = &dlg; INT_PTR nResponse = dlg.DoModal(); LOG4CPLUS_INFO(rootLogger, LOG4CPLUS_TEXT("应用程序退出")); return FALSE; }

这段代码里几个关键点值得展开说。

log4cplus::Initializer是一个RAII对象,它的构造函数会初始化log4cplus的内部资源,析构函数会清理。把它放在InitInstance的栈上,可以保证在函数退出时自动清理。注意不要把它放在全局作用域,因为全局对象的构造顺序不确定,可能导致log4cplus内部状态未初始化就被使用。

RollingFileAppender的构造函数参数依次是:文件名、最大文件大小(字节)、最大备份数、是否立即刷新、是否线程安全。这里设置的是10MB一个文件,最多保留5个备份,也就是最多占用60MB磁盘空间。对于大多数桌面应用,这个配置足够了。

3.2 日志格式的PatternLayout配置详解

PatternLayout决定了每条日志长什么样。上面代码里用的格式是:

%D{%Y-%m-%d %H:%M:%S.%q} [%t] %-5p %c - %m%n

拆解一下每个占位符的含义:

占位符含义示例输出
%D{%Y-%m-%d %H:%M:%S.%q}日期时间,精确到毫秒2025-01-15 14:30:22.123
%t线程ID0x1a2b
%-5p日志级别,左对齐,宽度5INFO
%cLogger名称root
%m日志消息应用程序启动
%n换行符\r\n

这个格式是我个人比较推荐的:时间戳精确到毫秒,方便排查时序问题;线程ID对于多线程程序至关重要;日志级别左对齐看起来整齐;Logger名称可以区分不同模块。

如果你想要更简洁的格式,可以用:

%D{%H:%M:%S} %-5p %m%n

输出效果是14:30:22 INFO 应用程序启动,适合日志量很大的场景。

如果你想要更详细的格式,可以加上文件名和行号:

%D{%Y-%m-%d %H:%M:%S} [%t] %-5p %c %F:%L - %m%n

其中%F是源文件名,%L是行号。但要注意,%F和%L需要编译器支持__FILE__和__LINE__宏的传递,log4cplus的宏LOG4CPLUS_INFO等已经内置了这些信息,所以可以直接用。不过这两个占位符会稍微增加一点运行时开销,因为需要解析文件路径字符串。

3.3 用配置文件驱动日志初始化

硬编码日志配置的缺点是:每次调整日志级别或者输出路径,都要重新编译程序。更好的做法是用一个.properties配置文件来驱动。

log4cplus支持类似Log4j的properties配置文件。创建一个log4cplus.properties文件:

# 根Logger配置 log4cplus.rootLogger=INFO, FILE, CONSOLE # 文件Appender log4cplus.appender.FILE=log4cplus::RollingFileAppender log4cplus.appender.FILE.File=logs/app.log log4cplus.appender.FILE.MaxFileSize=10MB log4cplus.appender.FILE.MaxBackupIndex=5 log4cplus.appender.FILE.layout=log4cplus::PatternLayout log4cplus.appender.FILE.layout.ConversionPattern=%D{%Y-%m-%d %H:%M:%S.%q} [%t] %-5p %c - %m%n # 控制台Appender log4cplus.appender.CONSOLE=log4cplus::ConsoleAppender log4cplus.appender.CONSOLE.layout=log4cplus::PatternLayout log4cplus.appender.CONSOLE.layout.ConversionPattern=%D{%H:%M:%S} %-5p %m%n

然后在代码里这样初始化:

log4cplus::Initializer initializer; log4cplus::PropertyConfigurator::doConfigure( LOG4CPLUS_TEXT("log4cplus.properties"));

用配置文件的好处是:运维人员或者测试人员可以直接修改日志级别,不需要开发人员重新编译。比如线上环境出了问题,把rootLogger的级别从INFO改成DEBUG,重启程序就能看到详细日志。

注意:配置文件的路径要写对。如果程序的工作目录和配置文件不在同一个目录,需要用绝对路径,或者用GetModuleFileName获取exe路径再拼接。我一般会把配置文件放在exe同级目录,然后用相对路径./log4cplus.properties。

4. 在MFC各个模块中打日志的实战写法

4.1 在对话框类中获取Logger并输出日志

在MFC对话框类里打日志,第一步是获取一个Logger实例。log4cplus的Logger是按名称管理的,同一个名称多次调用getInstance返回的是同一个对象。

// MyDlg.h #include <log4cplus/logger.h> #include <log4cplus/loggingmacros.h> class CMyDlg : public CDialogEx { // ... private: log4cplus::Logger m_logger; };
// MyDlg.cpp CMyDlg::CMyDlg(CWnd* pParent /*=nullptr*/) : CDialogEx(IDD_MY_DIALOG, pParent) , m_logger(log4cplus::Logger::getInstance( LOG4CPLUS_TEXT("MyDlg"))) { } BOOL CMyDlg::OnInitDialog() { CDialogEx::OnInitDialog(); LOG4CPLUS_INFO(m_logger, LOG4CPLUS_TEXT("对话框初始化开始")); // ... 初始化控件 ... LOG4CPLUS_INFO_FMT(m_logger, LOG4CPLUS_TEXT("对话框初始化完成,控件数量:%d"), GetDlgItemCount()); return TRUE; }

这里用了两个宏:LOG4CPLUS_INFO和LOG4CPLUS_INFO_FMT。前者直接输出一个字符串,后者支持printf风格的格式化。LOG4CPLUS_INFO_FMT在需要输出变量值时非常方便。

注意Logger的名称"MyDlg"。这个名称会出现在日志的%c占位符位置。给每个类起一个独立的Logger名称,好处是可以在配置文件里单独控制某个类的日志级别。比如你只想看数据库模块的DEBUG日志,可以在配置文件里加:

log4cplus.logger.Database=DEBUG

4.2 CString与log4cplus字符串的相互转换

MFC里大量使用CString,而log4cplus在Unicode模式下使用std::wstring。两者之间的转换是绕不开的。

从CString到log4cplus的tstring:

CString strValue = _T("some value"); log4cplus::tstring tstrValue = strValue.GetString(); LOG4CPLUS_INFO(m_logger, tstrValue);

在Unicode模式下,CString::GetString()返回LPCTSTR,也就是const wchar_t*,可以直接构造std::wstring。所以转换是隐式的,不需要额外操作。

从log4cplus的tstring到CString:

log4cplus::tstring tstrMsg = LOG4CPLUS_TEXT("log message"); CString strMsg = tstrMsg.c_str();

同样,在Unicode模式下也是直接兼容的。

但如果你用的是多字节字符集(MBCS),情况就复杂一些。CString是char版本的,而log4cplus的tstring也是std::string,两者可以直接互转。不过现在新项目基本都用Unicode了,MBCS的情况越来越少。

一个常见的坑是:在格式化日志时,把CString直接传给%s占位符。在Unicode模式下,%s期望的是wchar_t*,而CString的GetString()返回的也是wchar_t*,所以没问题。但如果你不小心用了%S(大写S),它会期望char*,就会输出乱码。记住:Unicode模式下用%s,不要用%S。

4.3 在文档类和视图类中共享Logger

MFC的文档/视图架构里,文档类负责数据,视图类负责显示。两个类都需要打日志,但它们的Logger名称应该不同,方便区分日志来源。

// MyDoc.h class CMyDoc : public CDocument { private: log4cplus::Logger m_logger; }; // MyDoc.cpp CMyDoc::CMyDoc() : m_logger(log4cplus::Logger::getInstance( LOG4CPLUS_TEXT("MyDoc"))) { } void CMyDoc::Serialize(CArchive& ar) { if (ar.IsStoring()) { LOG4CPLUS_INFO(m_logger, LOG4CPLUS_TEXT("开始保存文档")); // ... 保存逻辑 ... LOG4CPLUS_INFO_FMT(m_logger, LOG4CPLUS_TEXT("文档保存完成,数据大小:%lld 字节"), ar.GetFile()->GetLength()); } else { LOG4CPLUS_INFO(m_logger, LOG4CPLUS_TEXT("开始加载文档")); // ... 加载逻辑 ... } }

视图类同理:

// MyView.cpp CMyView::CMyView() : m_logger(log4cplus::Logger::getInstance( LOG4CPLUS_TEXT("MyView"))) { } void CMyView::OnDraw(CDC* pDC) { LOG4CPLUS_DEBUG(m_viewLogger, LOG4CPLUS_TEXT("OnDraw 被调用")); // ... 绘制逻辑 ... }

这样配置之后,日志文件里就能看到类似这样的输出:

2025-01-15 14:30:22.123 [0x1a2b] INFO MyDoc - 开始保存文档 2025-01-15 14:30:22.456 [0x1a2b] INFO MyDoc - 文档保存完成,数据大小:10240 字节 2025-01-15 14:30:23.789 [0x1a2b] DEBUG MyView - OnDraw 被调用

通过Logger名称,你可以一眼看出日志是哪个模块产生的。如果某个模块日志太多,可以在配置文件里单独把它的级别调高。

4.4 异常捕获与错误日志的配合使用

日志最重要的用途之一就是记录异常。在MFC里,异常可能来自多个地方:C++标准异常、MFC异常、Windows结构化异常(SEH)。一个完整的异常处理应该把异常信息记录到日志里。

void CMyDlg::OnButtonProcess() { try { LOG4CPLUS_INFO(m_logger, LOG4CPLUS_TEXT("开始处理数据")); // ... 可能抛出异常的代码 ... ProcessData(); LOG4CPLUS_INFO(m_logger, LOG4CPLUS_TEXT("数据处理完成")); } catch (const log4cplus::Exception& e) { LOG4CPLUS_ERROR_FMT(m_logger, LOG4CPLUS_TEXT("log4cplus异常:%s"), e.what()); } catch (const std::exception& e) { LOG4CPLUS_ERROR_FMT(m_logger, LOG4CPLUS_TEXT("标准异常:%s"), e.what()); } catch (CMemoryException* e) { LOG4CPLUS_FATAL(m_logger, LOG4CPLUS_TEXT("内存不足")); e->Delete(); } catch (CFileException* e) { TCHAR szError[256]; e->GetErrorMessage(szError, 256); LOG4CPLUS_ERROR_FMT(m_logger, LOG4CPLUS_TEXT("文件异常:%s"), szError); e->Delete(); } catch (...) { LOG4CPLUS_FATAL(m_logger, LOG4CPLUS_TEXT("未知异常")); } }

这里有几个细节值得注意。MFC的异常类(CMemoryException、CFileException等)是堆分配的,捕获后必须调用Delete()释放,否则会内存泄漏。CFileException::GetErrorMessage可以获取可读的错误描述,比直接输出错误码有用得多。

另外,LOG4CPLUS_ERROR_FMT里的%s在Unicode模式下期望wchar_t*。e.what()返回的是const char*(对于标准异常),直接传给%s会出问题。正确的做法是用log4cplus::tstring转换:

catch (const std::exception& e) { std::string what = e.what(); log4cplus::tstring twhat(what.begin(), what.end()); LOG4CPLUS_ERROR_FMT(m_logger, LOG4CPLUS_TEXT("标准异常:%s"), twhat.c_str()); }

或者更简单的方式,直接用LOG4CPLUS_ERROR宏拼接:

LOG4CPLUS_ERROR(m_logger, LOG4CPLUS_TEXT("标准异常:") + log4cplus::tstring(e.what(), e.what() + strlen(e.what())));

5. 日志文件滚动策略与磁盘空间控制

5.1 RollingFileAppender的参数计算与调优

RollingFileAppender是生产环境最常用的Appender。它的核心参数有三个:MaxFileSize、MaxBackupIndex、Filename。

MaxFileSize决定了单个日志文件的最大大小。当文件达到这个大小,log4cplus会自动把它重命名为app.log.1,然后新建一个app.log继续写。如果app.log.1已经存在,它会被重命名为app.log.2,以此类推。

MaxBackupIndex决定了保留多少个备份文件。如果设置为5,那么最多保留app.log.1到app.log.5,加上当前的app.log,总共6个文件。当第6个备份需要创建时,最旧的app.log.5会被删除。

磁盘占用上限的计算公式是:

最大磁盘占用 = MaxFileSize × (MaxBackupIndex + 1)

比如MaxFileSize=10MB,MaxBackupIndex=5,最大占用就是60MB。这个数字要根据你的应用场景来定。如果是桌面工具类软件,60MB可能偏大;如果是服务器端程序,60MB可能偏小。

我的经验值是:对于桌面应用,单个文件5-10MB,保留3-5个备份,总占用控制在20-50MB之间。对于长时间运行的服务,单个文件50-100MB,保留10-20个备份,总占用控制在1-2GB之间。

5.2 按日期滚动的替代方案

RollingFileAppender是按大小滚动的。有些场景下,按日期滚动更符合需求——比如每天生成一个日志文件,方便归档和检索。

log4cplus本身没有提供按日期滚动的Appender,但可以通过DailyRollingFileAppender来实现(部分版本支持),或者自己写一个简单的Appender。

如果log4cplus版本不支持DailyRollingFileAppender,一个变通方案是在程序启动时,根据当前日期生成文件名:

SYSTEMTIME st; GetLocalTime(&st); CString strLogFile; strLogFile.Format(_T("logs\\app_%04d%02d%02d.log"), st.wYear, st.wMonth, st.wDay); log4cplus::SharedAppenderPtr fileAppender( new log4cplus::RollingFileAppender( strLogFile.GetString(), 50 * 1024 * 1024, 10, false, true));

这样每次启动程序都会生成一个以日期命名的日志文件。如果程序是长期运行的,可以结合一个定时器,每天零点重新初始化Appender。不过这种做法比较粗糙,更优雅的方式是继承Appender类自己实现。

5.3 日志级别动态调整的实用技巧

生产环境中,日志级别通常设置为INFO或WARN,避免日志量过大。但当出现问题时,需要临时把级别调到DEBUG来获取详细信息。

如果使用配置文件驱动,修改配置文件后需要重启程序才能生效。log4cplus提供了PropertyConfigurator::doConfigure的重新调用机制,可以在不重启程序的情况下重新加载配置:

void CMyDlg::OnButtonReloadLogConfig() { try { log4cplus::PropertyConfigurator::doConfigure( LOG4CPLUS_TEXT("log4cplus.properties")); LOG4CPLUS_INFO(m_logger, LOG4CPLUS_TEXT("日志配置已重新加载")); } catch (const std::exception& e) { LOG4CPLUS_ERROR_FMT(m_logger, LOG4CPLUS_TEXT("重新加载日志配置失败:%s"), e.what()); } }

你可以把这个功能绑定到一个隐藏的快捷键或者调试菜单上,方便现场排查问题时快速切换日志级别。

另一个技巧是:在代码里根据条件动态调整某个Logger的级别:

log4cplus::Logger dbLogger = log4cplus::Logger::getInstance(LOG4CPLUS_TEXT("Database")); if (bDebugMode) { dbLogger.setLogLevel(log4cplus::DEBUG_LOG_LEVEL); } else { dbLogger.setLogLevel(log4cplus::WARN_LOG_LEVEL); }

这样可以在程序运行时根据用户设置或者命令行参数来决定日志详细程度。

6. 集成过程中容易踩的几个坑

6.1 静态库链接顺序导致的符号缺失

Visual Studio的链接器对静态库的链接顺序有要求。如果log4cplus静态库依赖了其他库(比如ws2_32.lib),而这些库在链接器命令行中出现在log4cplus之后,就会出现符号缺失。

解决方法是在"链接器 → 输入 → 附加依赖项"中,把log4cplus的依赖库放在log4cplus库的前面:

ws2_32.lib;log4cplus_static.lib;%(AdditionalDependencies)

或者更简单的方式:在代码中显式添加pragma comment:

#pragma comment(lib, "ws2_32.lib") #pragma comment(lib, "log4cplus_static.lib")

#pragma comment的好处是链接顺序由编译器自动处理,不容易出错。

6.2 Unicode与MBCS混用导致的乱码

前面提到过,Unicode模式下%s期望wchar_t*,MBCS模式下%s期望char*。如果你的工程是Unicode,但某个第三方库返回的是char*字符串,直接传给%s就会输出乱码或者只输出第一个字符。

一个典型的场景是从Windows API获取错误信息:

DWORD dwError = GetLastError(); LPTSTR lpMsgBuf = nullptr; FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, nullptr, dwError, 0, (LPTSTR)&lpMsgBuf, 0, nullptr); // 错误写法:在Unicode模式下,lpMsgBuf是wchar_t*,但%S期望char* // LOG4CPLUS_ERROR_FMT(m_logger, _T("错误:%S"), lpMsgBuf); // 正确写法:Unicode模式下用%s LOG4CPLUS_ERROR_FMT(m_logger, LOG4CPLUS_TEXT("错误:%s"), lpMsgBuf); LocalFree(lpMsgBuf);

记住一个简单的规则:在Unicode工程里,log4cplus的格式化宏中,字符串一律用%s,不要用%S。

6.3 多线程环境下日志丢失或错乱

log4cplus本身是线程安全的,RollingFileAppender的构造函数最后一个参数设置为true就开启了线程安全模式。但如果你在多个线程中同时写日志,仍然可能遇到日志顺序错乱的问题。

这不是log4cplus的bug,而是多线程程序的固有特性:线程A先调用了日志宏,但线程B的日志可能先被写入文件。如果你需要严格的时序,可以在日志消息中加上线程ID和精确到微秒的时间戳,事后分析时按时间戳排序。

另一个多线程的坑是:在InitInstance中初始化log4cplus,然后在工作线程中使用Logger。如果工作线程在log4cplus初始化之前就启动了,会访问未初始化的内部状态,导致崩溃。确保所有工作线程都在InitInstance之后创建。

6.4 程序退出时的日志刷新与资源释放

log4cplus的RollingFileAppender默认使用缓冲写入,也就是说日志消息先写到内存缓冲区,缓冲区满了才真正写入磁盘。这样做性能好,但程序崩溃时可能丢失最后几条日志。

如果你希望每条日志都立即写入磁盘,可以把RollingFileAppender构造函数的第四个参数(immediateFlush)设置为true:

new log4cplus::RollingFileAppender( logFile, 10 * 1024 * 1024, 5, true, // immediateFlush = true true // threadSafe = true );

代价是性能会下降,因为每次写日志都要调用fflush。对于日志量不大的桌面应用,这个代价可以接受。对于高频日志场景,建议保持false,但在程序退出时手动调用flush:

log4cplus::Logger rootLogger = log4cplus::Logger::getRoot(); rootLogger.shutdown();

shutdown()会刷新所有Appender的缓冲区并关闭文件。把它放在InitInstance的返回之前,或者放在CWinApp::ExitInstance()中。

注意:log4cplus::Initializer的析构函数也会自动调用shutdown,但前提是它是在栈上正确构造和析构的。如果你把Initializer放在了全局作用域,析构顺序可能不确定,导致日志丢失。

7. 从1.x迁移到2.x需要注意的API变化

如果你的项目之前用的是log4cplus 1.x,升级到2.x时需要注意几个不兼容的变化。

第一个变化是SharedAppenderPtr的构造方式。1.x中可以直接用裸指针,2.x中推荐用std::unique_ptr或者std::shared_ptr:

// 1.x 写法 log4cplus::SharedAppenderPtr appender(new log4cplus::ConsoleAppender()); // 2.x 写法(推荐) auto appender = std::make_shared<log4cplus::ConsoleAppender>();

第二个变化是Layout的设置方式。1.x中setLayout接受裸指针,2.x中接受std::unique_ptr:

// 1.x appender->setLayout(new log4cplus::PatternLayout(pattern)); // 2.x appender->setLayout(std::unique_ptr<log4cplus::Layout>( new log4cplus::PatternLayout(pattern)));

第三个变化是PropertyConfigurator的API。2.x中doConfigure变成了静态方法,不再需要先创建PropertyConfigurator对象。

第四个变化是字符串类型。2.x中log4cplus::tstring在Unicode模式下是std::wstring,而1.x中可能是std::string。这个变化会影响所有涉及字符串的代码。

迁移时建议先在测试环境中编译运行,确认所有日志功能正常后再上生产。如果项目比较大,可以先用条件编译同时支持两个版本,逐步迁移。

8. 一个完整的可运行示例工程结构

最后,我把一个完整的MFC+log4cplus示例工程的结构列出来,方便你对照检查自己的工程配置。

MyMfcApp/ ├── MyMfcApp.sln ├── MyMfcApp/ │ ├── MyMfcApp.vcxproj │ ├── MyMfcApp.cpp # InitInstance中初始化log4cplus │ ├── MyMfcApp.h │ ├── MyDlg.cpp # 对话框类,使用Logger │ ├── MyDlg.h │ ├── MyDoc.cpp # 文档类,使用Logger │ ├── MyDoc.h │ ├── MyView.cpp # 视图类,使用Logger │ ├── MyView.h │ ├── log4cplus.properties # 日志配置文件 │ └── logs/ # 日志输出目录(需要手动创建) ├── log4cplus_static/ # log4cplus静态库工程 │ ├── log4cplus_static.vcxproj │ └── src/ # log4cplus源码 └── include/ └── log4cplus/ # log4cplus头文件

工程配置的关键点回顾:

  • 所有工程使用相同的运行时库配置(/MT或/MD)
  • 预处理器定义中包含LOG4CPLUS_STATIC、UNICODE、_UNICODE
  • 附加依赖项中包含ws2_32.lib
  • 日志目录logs/需要在程序启动时检查是否存在,不存在则创建

创建日志目录的代码:

CString strLogDir = _T("logs"); if (!PathFileExists(strLogDir)) { CreateDirectory(strLogDir, nullptr); }

PathFileExists和CreateDirectory需要包含shlwapi.h并链接shlwapi.lib。

这套配置我在多个MFC项目中用过,从VS2010到VS2022都能正常工作。唯一需要根据VS版本调整的是log4cplus源码中一些C++标准特性的兼容性处理,但2.x版本对C++11的要求已经很低了,基本上开箱即用。

如果你在集成过程中遇到了本文没覆盖到的问题,我的建议是:先检查运行时库配置是否一致,再检查预处理器定义是否完整,最后检查链接顺序。这三个地方能解决90%的集成问题。剩下的10%,多半是字符集转换的细节,耐心调试一下就能搞定。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询