VC6 MFC轻量级日志类:无依赖、线程安全、兼容Windows 2000

发布时间:2026/9/16 2:12:29
VC6 MFC轻量级日志类:无依赖、线程安全、兼容Windows 2000
简介本资源是一个面向VC6 MFC开发者的轻量级C日志记录工具包专为解决传统Windows桌面应用中调试信息难追踪、运行状态难复现的问题而设计。适用于初学者理解日志机制也便于中高级开发者快速集成基础日志功能到遗留MFC项目中。压缩包为RAR格式共2个文件1个头文件Log.h定义CLog类接口1个源文件log.cpp实现时间戳格式化、文件追加写入及多等级日志输出整体仅2KB即下即用无依赖项。已有218人学习下载说明其在老版本VC开发场景中具备实际参考价值。读者可直接将该日志类嵌入现有MFC工程快速启用DEBUG/INFO/ERROR分级打印支持自动创建vc_log.txt日志文件、带精确时间戳与消息前缀的格式化输出并掌握文件指针管理、CString字符串拼接及异常抛出等典型VC6编程实践。1. VC6 MFC 下轻量级日志类不依赖第三方、无运行时库冲突、直接嵌入老项目就能用的 log 实现在维护一个运行了十五年以上的 VC6 MFC 工业控制软件时我遇到过最棘手的问题不是算法逻辑而是「出问题时根本不知道哪一行代码执行到了、变量值是多少、线程卡在哪」。当时项目已停更多年不能引入任何新 DLL比如 log4cplus 或 Boost.Log连std::string都不敢用——因为 VC6 的 STL 实现不完整且与 MFC 的CString混用极易崩溃。这时候一个仅含Log.h和log.cpp两个文件、编译即用、输出带时间戳等级文件行号、日志自动追加到文本文件的 CLog 类就成了救命稻草。它不是现代 C 日志框架而是一套专为 VC6 MFC 环境打磨的「手术刀式日志工具」没有宏定义污染全局命名空间不强制要求异常处理不依赖 ATL 或 WTL甚至能在CWinApp::InitInstance()之前就安全初始化。适合所有仍在维护 VC6/VC7.1 项目的工程师——尤其是那些被客户要求「必须在 Windows 2000 上跑通」、「不允许安装任何额外运行库」的硬性场景。2. CLog 类设计原理与 VC6 兼容性取舍为什么不用 std::ofstream、不封装 CTime::GetCurrentTime()2.1 为何坚持使用 FILE* fprintf 而非 CStdioFile 或 std::ofstreamVC6 的CStdioFile在多线程写入时存在隐式临界区竞争其内部缓冲未加锁而std::ofstream在 VC6 中默认不支持宽字符流std::wofstream根本不可用且构造/析构过程会触发_setmbcp()等区域设置调用在某些嵌入式 WinCE 子系统中引发断言失败。CLog直接采用 CRT 的fopen/fprintf/fflush组合原因有三fopen(_T(vc_log.txt), at)中的at模式确保每次写入自动定位到文件末尾a同时允许读写t表示文本模式兼容 DOS/Windows 换行符\r\nfprintf是原子写入单次调用内不会被线程打断配合fflush强制刷盘避免程序异常退出时日志丢失所有字符串操作基于 MFCCString天然支持 Unicode通过_UNICODE宏切换和 ANSI 双编译模式无需手动WideCharToMultiByte转码。提示若需在纯 Win32 SDK 项目无 MFC中复用该类只需将CString替换为TCHAR[]数组 _tcscpy_s并移除对AfxThrowFileException的调用改用GetLastError()判断错误。2.2 时间戳格式化为何绕过 CTime::Format而用 GetLocalTime wsprintfCTime::Format在 VC6 中存在两个致命缺陷一是内部调用GetDateFormat/GetTimeFormat时若系统区域设置为非英语如中文简体会导致格式化字符串包含全角冒号或空格破坏日志解析二是其构造函数CTime(time_t)在跨时区或夏令时切换期间可能返回错误时间。CLog改用 Win32 APIGetLocalTime获取SYSTEMTIME结构体再通过wsprintf手动拼接void CLog::FormatTime(CString strTime) { SYSTEMTIME st; GetLocalTime(st); TCHAR szBuf[64]; wsprintf(szBuf, _T(%04d-%02d-%02d %02d:%02d:%02d), st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond); strTime szBuf; }此实现完全规避区域设置影响且wsprintf在 VC6 中稳定支持%02d等格式符_stprintf在某些 SP6 补丁版本中存在栈溢出风险。注意szBuf必须声明为TCHAR类型而非char否则 Unicode 编译下wsprintf会写入宽字符导致后续fprintf输出乱码。2.3 日志等级参数为何用 LPCTSTR 而非枚举类型WriteLog(LPCTSTR pszLogLevel, LPCTSTR pszMessage)的第一个参数设计为字符串而非enum LOG_LEVEL { DEBUG, INFO, WARNING, ERROR }是为兼容 VC6 的预处理器限制。VC6 不支持enum class普通enum在头文件中定义后若多个.cpp文件包含Log.h链接时易出现LNK2005: xxx already defined错误。而字符串字面量_T(DEBUG)在编译期被合并为同一地址无重复定义风险。更重要的是这种设计允许动态注入等级名——例如在调试 Release 版本时通过读取注册表键值HKEY_LOCAL_MACHINE\SOFTWARE\MyApp\LogLevel返回VERBOSE直接传入即可无需修改枚举定义。3. 完整可编译代码实现与关键参数配置说明3.1 Log.h 头文件声明类接口与线程安全控制宏// Log.h #pragma once #include afx.h // MFC core components #include stdio.h // CRT file I/O #include windows.h // GetLocalTime class CLog { public: CLog(); ~CLog(); // 主日志写入接口pszLogLevel 为等级字符串如 _T(ERROR)pszMessage 为日志内容 void WriteLog(LPCTSTR pszLogLevel, LPCTSTR pszMessage); // 带文件名和行号的增强版写入推荐在实际业务代码中使用 void WriteLogEx(LPCTSTR pszLogLevel, LPCTSTR pszMessage, LPCTSTR pszFile, int nLine); // 设置日志文件路径默认为当前目录 vc_log.txt void SetLogFilePath(LPCTSTR pszPath); // 清空现有日志文件慎用生产环境建议关闭 void ClearLogFile(); protected: void FormatTime(CString strTime); void OpenLogFile(); void CloseLogFile(); private: CString m_strLogPath; // 日志文件完整路径 FILE* m_pFile; // 文件句柄线程不安全需外部同步 CRITICAL_SECTION m_cs; // VC6 不支持 std::mutex用 Win32 CRITICAL_SECTION 实现线程保护 };注意CRITICAL_SECTION是 VC6 下唯一可靠的用户态同步原语。InitializeCriticalSection和DeleteCriticalSection必须在构造/析构中成对调用否则会导致内存泄漏或崩溃。此处未使用CCriticalSectionMFC 封装类因其在 VC6 中部分成员函数未导出。3.2 log.cpp 实现文件含线程保护、路径解析与错误降级策略// log.cpp #include Log.h #include tchar.h #include shlwapi.h // PathFindFileName #pragma comment(lib, shlwapi.lib) CLog::CLog() : m_pFile(NULL), m_strLogPath(_T(vc_log.txt)) { InitializeCriticalSection(m_cs); } CLog::~CLog() { CloseLogFile(); DeleteCriticalSection(m_cs); } void CLog::SetLogFilePath(LPCTSTR pszPath) { if (pszPath *pszPath) { m_strLogPath pszPath; } } void CLog::ClearLogFile() { CloseLogFile(); _tremove(m_strLogPath); OpenLogFile(); // 重新打开即创建空文件 } void CLog::OpenLogFile() { EnterCriticalSection(m_cs); if (m_pFile NULL) { // 尝试创建日志目录若路径含子目录 TCHAR szDir[MAX_PATH] {0}; _tcscpy_s(szDir, m_strLogPath); PathRemoveFileSpec(szDir); if (*szDir !PathIsDirectory(szDir)) { CreateDirectory(szDir, NULL); // 忽略返回值即使失败也继续 } m_pFile _tfopen(m_strLogPath, _T(at)); if (m_pFile NULL) { // 降级策略若指定路径不可写尝试写入临时目录 TCHAR szTemp[MAX_PATH] {0}; GetTempPath(MAX_PATH, szTemp); _tcscat_s(szTemp, _T(vc_log_fallback.txt)); m_pFile _tfopen(szTemp, _T(at)); if (m_pFile) { // 记录降级事件到控制台仅调试时可见 _tprintf(_T([FALLBACK] Log redirected to %s\n), szTemp); } } } LeaveCriticalSection(m_cs); } void CLog::CloseLogFile() { EnterCriticalSection(m_cs); if (m_pFile) { fclose(m_pFile); m_pFile NULL; } LeaveCriticalSection(m_cs); } void CLog::FormatTime(CString strTime) { SYSTEMTIME st; GetLocalTime(st); TCHAR szBuf[64]; wsprintf(szBuf, _T(%04d-%02d-%02d %02d:%02d:%02d), st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond); strTime szBuf; } void CLog::WriteLog(LPCTSTR pszLogLevel, LPCTSTR pszMessage) { if (!pszLogLevel || !pszMessage) return; EnterCriticalSection(m_cs); OpenLogFile(); if (m_pFile NULL) { LeaveCriticalSection(m_cs); return; } CString strTime; FormatTime(strTime); CString strOutput; strOutput.Format(_T([%s] [%s]: %s), strTime, pszLogLevel, pszMessage); // 使用 _ftprintf_sVC6 SP6 支持替代 fprintf防止格式化字符串攻击 _ftprintf_s(m_pFile, _T(%s\n), strOutput); fflush(m_pFile); LeaveCriticalSection(m_cs); } void CLog::WriteLogEx(LPCTSTR pszLogLevel, LPCTSTR pszMessage, LPCTSTR pszFile, int nLine) { if (!pszLogLevel || !pszMessage || !pszFile) return; EnterCriticalSection(m_cs); OpenLogFile(); if (m_pFile NULL) { LeaveCriticalSection(m_cs); return; } CString strTime; FormatTime(strTime); // 提取文件名不含路径提升日志可读性 LPCTSTR pszFileName PathFindFileName(pszFile); CString strOutput; strOutput.Format(_T([%s] [%s] [%s(%d)]: %s), strTime, pszLogLevel, pszFileName, nLine, pszMessage); _ftprintf_s(m_pFile, _T(%s\n), strOutput); fflush(m_pFile); LeaveCriticalSection(m_cs); }关键参数说明与可调项参数/行为默认值修改方式影响说明m_strLogPathvc_log.txt调用SetLogFilePath(_T(D:\\logs\\app.log))支持绝对路径、相对路径若含不存在的目录类自动创建日志打开模式at修改OpenLogFile()中fopen第二个参数a支持读写t强制文本模式避免二进制写入换行符错误线程保护粒度整个WriteLog调用无法细粒度到单行日志因fprintffflush需原子性高频写入场景下可能成为性能瓶颈但保证日志行不交叉降级路径GetTempPath()vc_log_fallback.txt修改ClearLogFile()中的GetTempPath调用当主日志路径因权限/磁盘满失败时自动转向系统临时目录3.3 在 MFC 应用中集成从 CWinApp 到对话框控件的全链路调用在CWinApp派生类如CMyApp中声明全局日志对象并在InitInstance()开头初始化// MyApp.h class CMyApp : public CWinApp { public: virtual BOOL InitInstance(); static CLog g_Log; // 声明为 static确保单例 }; // MyApp.cpp CLog CMyApp::g_Log; // 定义 BOOL CMyApp::InitInstance() { // 在任何 MFC 对象创建前启用日志 CMyApp::g_Log.SetLogFilePath(_T(C:\\MyApp\\logs\\runtime.log)); // 记录启动事件 CMyApp::g_Log.WriteLog(_T(INFO), _T(Application starting...)); // 后续正常 MFC 初始化... return CWinApp::InitInstance(); }在对话框类中使用WriteLogEx自动注入文件名和行号// MyDialog.cpp void CMyDialog::OnBnClickedOk() { // 使用宏封装避免手动传参错误 #define LOG_DEBUG(msg) CMyApp::g_Log.WriteLogEx(_T(DEBUG), _T(msg), _T(__FILE__), __LINE__) #define LOG_ERROR(msg) CMyApp::g_Log.WriteLogEx(_T(ERROR), _T(msg), _T(__FILE__), __LINE__) LOG_DEBUG(_T(User clicked OK button)); if (!DoSomethingCritical()) { LOG_ERROR(_T(DoSomethingCritical failed!)); AfxMessageBox(_T(Operation failed.)); } }提示__FILE__在 VC6 中返回完整路径如D:\project\src\MyDialog.cppPathFindFileName自动截取为MyDialog.cpp大幅缩短日志长度便于快速定位。4. 生产环境部署技巧与典型故障排查表4.1 日志文件权限与磁盘满应对三步隔离法VC6 MFC 程序常以低权限用户如Guest或受限服务账户运行导致日志写入失败。采用以下三步隔离策略路径解耦不在程序安装目录写日志通常受 UAC 保护改用%ALLUSERSPROFILE%\MyApp\Logs所有用户可写或%LOCALAPPDATA%\MyApp\Logs当前用户专用大小监控在WriteLog写入前检查文件尺寸超 10MB 自动重命名归档void CLog::CheckLogSizeAndRotate() { if (m_pFile NULL) return; fseek(m_pFile, 0, SEEK_END); long nSize ftell(m_pFile); if (nSize 10 * 1024 * 1024) // 10MB { CString strArchive m_strLogPath; strArchive _T(.old); _trename(m_strLogPath, strArchive); fclose(m_pFile); m_pFile NULL; } }静默降级当fopen失败且降级路径也失败时改用OutputDebugString输出到 Visual Studio 输出窗口仅调试版有效确保开发阶段不丢日志。4.2 常见故障与对应排查命令/日志特征故障现象日志文件表现排查命令Windows 命令行根本原因与修复日志文件为空但程序正常运行文件存在但 0 字节dir /q vc_log.txt查看所有者icacls vc_log.txt /grant Users:F授予写入权进程以高权限启动日志文件被创建为管理员所有后续低权限实例无法写入日志内容乱码如? ? ? ?文件用记事本打开显示方块chcp查看当前代码页type vc_log.txt | findstr /c:[测试是否能匹配[VC6 默认 ANSI 编码若系统区域设置为 UTF-8需在项目属性 → 配置属性 → 常规 → 字符集 → 选择「使用多字节字符集」同一时刻多行日志内容混杂如[INFO] [DEBUG]出现在同一行日志行首缺失[或时间戳错位debugview捕获OutputDebugString输出确认是否线程未加锁EnterCriticalSection/LeaveCriticalSection未配对或CloseLogFile被多线程并发调用WriteLogEx输出的文件名显示为完整路径而非文件名日志中出现D:\project\src\MyDialog.cpp(123)dumpbin /headers log.obj | findstr CRT确认链接的 CRT 版本shlwapi.lib未正确链接PathFindFileName解析失败回退到原始__FILE__4.3 性能压测下的日志优化禁用时间戳与异步缓冲开关在高频数据采集场景如每秒 1000 条日志GetLocalTimewsprintf成为瓶颈。此时可关闭时间戳改用单调递增计数器// 在 Log.h 中添加 static volatile LONG g_nLogCounter 0; // 在 WriteLog 中替换 FormatTime 调用 // CString strTime; FormatTime(strTime); → CString strTime; strTime.Format(_T(%ld), InterlockedIncrement(g_nLogCounter));若需更高性能可启用内存缓冲需自行管理缓冲区溢出// 添加缓冲区成员 TCHAR m_szBuffer[4096]; int m_nBufferPos; // WriteLog 中改为 if (m_nBufferPos strOutput.GetLength() sizeof(m_szBuffer)/sizeof(TCHAR)) { _tcscat_s(m_szBuffer m_nBufferPos, sizeof(m_szBuffer)/sizeof(TCHAR) - m_nBufferPos, strOutput); _tcscat_s(m_szBuffer m_nBufferPos, sizeof(m_szBuffer)/sizeof(TCHAR) - m_nBufferPos, _T(\n)); m_nBufferPos strOutput.GetLength() 1; } else { // 缓冲区满刷入文件 _ftprintf_s(m_pFile, _T(%s), m_szBuffer); fflush(m_pFile); m_nBufferPos 0; _tcscpy_s(m_szBuffer, _T()); }注意此缓冲方案牺牲了实时性日志可能延迟数秒才落盘仅适用于可接受短暂延迟的监控场景。本文还有配套的精品资源点击获取