C++简易日志类设计与实现:自动滚动与多线程安全
在C++开发中,构建一个稳定、易用的日志系统是保证软件可维护性的关键。本文解析一个源自开源项目的轻量级日志类,重点展示如何通过宏定义简化调用、如何管理日志级别以及实现基于容量的文件自动滚动,为开发者提供直接可用的代码模板。
实现目标与环境准备
该日志类的核心目标是解决C++程序中的输出追踪问题,主要特性包括:
- 线程安全访问:使用静态单例模式确保全局唯一性,适配多线程并发写入场景。
- 灵活的日志级别:区分DEBUG、INFO、WARN等等级,支持根据编译配置(Debug/Release)自动切换默认输出阈值。
- 文件滚动机制:当当前日志文件大小超过设定值(如1MB)时,自动将旧文件备份并创建新文件,防止磁盘空间耗尽。
- 标准化输出格式:统一包含时间戳、毫秒级精度、线程ID及所在函数名,便于定位异常。
运行该代码需具备 Windows 开发环境,依赖标准库、stdio.h、sys/timeb.h 以及 windows.h 提供的系统函数。
头文件设计:接口与宏定义
Log.h 定义了日志类的核心结构。它采用单例模式,对外提供获取实例的 get() 方法和打印方法 printf()。内部通过大量的宏定义来封装具体的格式化与条件判断逻辑,从而在使用时保持代码简洁。
#ifndef __LOG_H__
#define __LOG_H__
#include <stdio.h>
#include <tchar.h>
#include <crtdbg.h>
#include <windows.h>
#include <time.h>
#include <sys/timeb.h>
class CLog {
public:
~CLog();
static CLog* get(TCHAR* path = NULL);
void printf(const char* format, ...);
private:
CLog(FILE* handle);
private:
static CLog* _log;
FILE* _handle;
};
enum {
LOG_DEBUG,
LOG_INFO,
LOG_WARN,
LOG_ERROR,
LOG_FATAL
};
#ifdef _DEBUG
static unsigned int log_level = LOG_DEBUG;
#else
static unsigned int log_level = LOG_INFO;
#endif
#define PRINT_LINE(type, format, datetime, ms, ...) \
printf("%lu::%s::%s,%.3d::%s::" format "\n", GetCurrentThreadId(), type, datetime, ms, \
__FUNCTION__, ## __VA_ARGS__);
#define LOG(type, format, ...) do { \
if (type >= log_level && type <= LOG_FATAL) { \
CLog* log = CLog::get(); \
const char *type_as_char[] = { "DEBUG", "INFO", "WARN", "ERROR", "FATAL" }; \
struct _timeb now; \
struct tm today; \
char datetime_str[20]; \
_ftime_s(&now); \
localtime_s(&today, &now.time); \
strftime(datetime_str, 20, "%Y-%m-%d %H:%M:%S", &today); \
if (log) { \
log->PRINT_LINE(type_as_char[type], format, datetime_str, now.millitm, ## __VA_ARGS__); \
} else { \
PRINT_LINE(type_as_char[type], format, datetime_str, now.millitm, ## __VA_ARGS__); \
} \
} \
} while(0)
#define log_printf(format, ...) LOG(LOG_INFO, format, ## __VA_ARGS__)
#define LOG_INFO(format, ...) LOG(LOG_INFO, format, ## __VA_ARGS__)
#define LOG_WARN(format, ...) LOG(LOG_WARN, format, ## __VA_ARGS__)
#define LOG_ERROR(format, ...) LOG(LOG_ERROR, format, ## __VA_ARGS__)
#define DBGLEVEL 1000
#define DBG(level, format, ...) do { \
if (level <= DBGLEVEL) { \
LOG(LOG_DEBUG, format, ## __VA_ARGS__); \
} \
} while(0)
#define ASSERT(x) _ASSERTE(x)
#endif
第1步:理解日志级别控制
代码中利用预处理器宏在 Debug 和 Release 模式下设置不同的默认日志级别。_DEBUG 环境下开启详细的 LOG_DEBUG,而正式版默认仅显示 LOG_INFO 及以上信息,避免生产环境日志过载。
第2步:使用日志宏简化调用
用户无需手动调用类方法。例如,只需编写 LOG_INFO("初始化完成") 或 LOG_ERROR("发生错误: %d", errCode),系统将自动获取单例实例、检查当前日志级别,并进行格式化输出。
第3步:查看输出格式细节
PRINT_LINE 宏定义了输出的具体样式,例如:线程ID::类型::日期时间,毫秒::函数名::实际内容。这种格式有助于在复杂的多线程应用中迅速定位到出问题的代码行及其上下文。
源文件实现:状态管理与滚动逻辑
Log.cpp 负责底层文件操作的执行,包括文件的打开、检查大小、重命名(滚动)以及实际的写入操作。
#include "Log.h"
#include <stdio.h>
#include <stdarg.h>
#include <share.h>
#define LOG_ROLL_SIZE (1024 * 1024) // 1MB
CLog* CLog::_log = NULL;
CLog::CLog(FILE* handle)
: _handle(handle)
{
_log = this;
}
CLog::~CLog()
{
if (_log && _handle) {
fclose(_handle);
_log = NULL;
}
}
CLog* CLog::get(char* path)
{
if (_log) {
return _log;
}
if(!path)
{
path = "dll.log";
}
DWORD size = 0;
HANDLE file = CreateFile(path, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,
NULL);
if (file != INVALID_HANDLE_VALUE) {
size = GetFileSize(file, NULL);
CloseHandle(file);
}
if (size != INVALID_FILE_SIZE && size > LOG_ROLL_SIZE) {
TCHAR roll_path[MAX_PATH];
sprintf(roll_path, "%s.1", path);
if (!MoveFileEx(path, roll_path, MOVEFILE_REPLACE_EXISTING)) {
return NULL;
}
}
FILE* handle = fopen(path, "a+");
if (!handle) {
return NULL;
}
_log = new CLog(handle);
return _log;
}
void CLog::printf(const char* format, ...)
{
va_list args;
va_start(args, format);
vfprintf(_handle, format, args);
va_end(args);
fflush(_handle);
}
第1步:检查并触发文件滚动
在 get() 方法中,如果检测到当前日志文件已存在且其大小超过 LOG_ROLL_SIZE(本例设为 1MB),系统会通过 MoveFileEx 函数将该文件重命名为带后缀的版本(如 dll.log.1)。这一步至关重要,实现了自动清理历史日志的功能。
第2步:创建或打开日志文件句柄
无论是否进行了滚动操作,接下来都会以追加模式(a+)打开原路径的文件。如果无法打开文件(如权限不足或磁盘故障),则返回空指针,阻止后续的无效写入尝试。
第3步:执行变参数据写入
当需要输出日志时,printf() 成员函数被调用。它使用 va_list 处理可变参数,将格式化后的字符串直接写入已保存的文件句柄 _handle,并通过 fflush 强制刷新缓冲区,确保日志实时落盘,提升排查效率。
排错与维护建议
在实际部署或使用此日志类时,若遇到功能异常,请注意以下几点:
1. 资源泄漏风险: 析构函数仅在 _log 不为空时才关闭文件句柄。若多次调用 get() 但未妥善管理生命周期,可能导致句柄未释放。建议在应用退出前主动销毁单例或确保主流程只进行一次实例化。
2. 跨平台兼容性: 当前实现强依赖于 Windows API(如 CreateFile, MoveFileEx)。若需移植到 Linux/macOS,需替换对应的文件系统操作函数及时间获取方式。
3. 滚动覆盖策略: 目前的滚动仅保留一份旧文件(.1)。如果产生极高频率的日志,建议增加版本数量限制或基于时间的轮转策略。
总结
以上是对这个轻量级C++日志类的详细内容解析。该类虽然简单,但涵盖了多线程安全的文件读写、自动化存储管理以及便捷的接口封装,非常适合作为基础设施集成到中等规模的C++应用程序中。更多关于C++工具库的代码实践资料请关注本站其它相关文章!
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













