通过上一篇 《写一个简单的 C++ 日志库 - cllogger(2)- 日期时间》我们已经掌握了如何通过 std::chrono 提供的日期时间工具转换时间参数为指定格式的字符串。
现在我们可以把各个参数信息拼装为 Entry 实例,交给 OutputMessage()
void cllogger::LogInternal(Level level, std::chrono::system_clock::time_point timestamp, const WCHAR* source, const WCHAR* msg)
{
auto milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(timestamp.time_since_epoch());
auto seconds = std::chrono::duration_cast<std::chrono::seconds>(milliseconds).count();
auto time = localtime(&seconds);
WCHAR timestampSz[128];
size_t len = _snwprintf_s(
timestampSz,
_TRUNCATE,
L"%02i:%02i:%02i.%03llu",
time->tm_hour,
time->tm_min,
time->tm_sec,
milliseconds.count() % 1000);
Entry entry = { level, std::wstring(timestampSz, len), source, msg };
OutputMessage(entry);
}
OutputMessage() 将完成最终的日志字符串拼接并输出
void cllogger::OutputMessage(Entry& entry)
{
#ifndef _DEBUG
if (!m_LogToFile) return;
#endif
const WCHAR* levelSz =
(entry.level == Level::Error) ? L"ERROR" :
(entry.level == Level::Warning) ? L"WARNINIG" :
(entry.level == Level::Fatal) ? L"Fatal" :
L"DEBUG"
;
// 拼装日志信息字符串
std::wstring message = levelSz;
message += L" (";
message.append(entry.timestamp);
message += L") ";
message += entry.tag;
message += L": ";
message += entry.messsage;
message += L'\n';
// 输出日志信息字符串
}
日志消息的输出,可根据配置分别输出到调试窗口(或dbgview)、文件等。
先来实现输出到调试窗口,这里特指 windows 平台上 Visual studio 开发环境中,以 Debub 模式启动程序时底部显示的【输出】窗口。
在【输出】窗口中右键可以看到异常、单步筛选、模块加载、卸载、进程退出、线程退出、程序输出等信息都会输出到这里。
【程序输出】是指我们通过代码输出的信息,这里我们就要讲讲输出信息到【输出】窗口的两种方法:
1、OutputDebugString API
OutputDebugString(L"output message.");
如果我们希望只在debug版的时候才输出,则需要
#ifdef _DEBUG
::OutputDebugString(L"DEBUG");
#endif // _DEBUG
2、 _RPTn 宏系列
_RPTn 中的n
后缀指定args
中的(即格式化数据的)参数个数,它可以是 0、1、2、3、4 或 5。_RPTWn 是对应的宽字符版本。
如,没有args时:
_RPTW0(_CRT_WARN, message.c_str());
有1个参数时
int foo = 3;
_RPTW1(_CRT_WARN, L"foo is %d", foo);
后续以此类推。
考虑编译器及配置的不同,下面的调用方式更为周密安全:
#ifdef _UNICODE
#ifdef _RPTW0
_RPTW0(_CRT_WARN, foo);
#endif
#else
_RPT0(_CRT_WARN, foo);
#endif
这二者的主要区别是, OutputDebugString 是系统API,任何时候都会执行消息输出;但 _RPTn 则是 CRT 的一部分,它仅仅在调试模式下,输出到 Visual studio 的【输出】窗口中,而 Relase 版则会被替换为空,对性能基本没有影响。
CRT(C Runtime Library)
什么是CRT
Microsoft C 运行时库 (CRT) 。为了提高C语言的开发效率,C标准定义了一系列常用的函数,称为C库函数。C标准仅仅定义了函数原型,并没有提供实现。因此这个任务留给了各个支持C语言标准的编译器。每个编译器通常实现了标准C的超集,称为C运行时库(C Run Time Library),简称CRT。对于VC++编译器来说,它提供的CRT库支持C标准定义的标准C函数,同时也有一些专门针对Windows系统特别设计的函数
CRT包含哪些内容
内容 | 说明 |
启动、退出 | 入口函数、及所依赖的函数 |
标准函数 | C语言标准、标准库所拥有的函数实现 |
I/O | I/O的封装 |
堆 | 堆的封装和实现 |
语言实现 | 特殊功能的实现 |
调试 | 实现调试功能的代码 |
由此看来,CRT是 Windows 上编程的基石,提供了方方面面的底层支持。
使用 CRT 相关的功能很简单,只需要引入 <crtdbg.h> 即可。
我们接下来再列举一个 CRT 应用的例子。
CRT 调试技术
要使用 CRT 调试库之一,必须链接 /DEBUG,并使用 /MDd、/MTd 或 /LDd 进行编译。即,必须处于调试模式下。
C++ 程序员经常会遇到的困难之一便是内存泄露。内存泄露的原因是使用new或malloc等函数分配内存,但没有正确的配对使用delete和free释放内存,就会造成程序所占用的内存越来越大的现象。
检查内存泄漏的原理是,在分配的数据一头一尾分别插入对应的标志数据,微软使用的是0xfd 0xfd 0xfd 0xfd,检查是否溢出直接检查这两个标志数据是否被修改即可判断。
例如,我们分配10个字节的内存
char* a = new char[10];
a 的地址为 0x01039099,查看内存
可以看到数据两端都填充了0xfd,初始化分配的数据填充为0xcd。
主动调用 _CrtDumpMemoryLeaks 检查泄露:
#include <crtdbg.h>
int main()
{
char* a = new char[10];
_CrtDumpMemoryLeaks();
}
输出
捕获到了内存泄漏,且指明了内存地址和大小。
如果想输出泄露内存代码所在的文件名和行号,那就完美了,我们可以通过宏来做到
#define DEBUG_NEW new(_CLIENT_BLOCK, __FILE__, __LINE__)
#define new DEBUG_NEW
效果:
已经非常清晰明了了。我们再进一步,能否让内存泄露信息以弹出窗口的方式显示?完全可以,只需要通过 _CrtSetReportMode 设置报告现实的方式即可:
#include <crtdbg.h>
int main()
{
_CrtSetReportMode(_CRT_ERROR, _CRTDBG_MODE_WNDW | _CRTDBG_MODE_DEBUG);
_CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_WNDW | _CRTDBG_MODE_DEBUG);
char* a = new char[10];
_CrtDumpMemoryLeaks();
}
效果:
是不是很简单就做到了内存泄露的检查,其实,通过CRT我们还可以做到程序退出时自动检查,甚至为了降低检查对程序性能的影响,我们还可以设置检查内存泄露的频率。
总结
CRT 作为 Windows 编程中不可或缺的一部分,提供了基本的函数和宏,善用 CRT 会简化开发,提供可靠性,提供层序性能,确保了程序的高效健壮,需要我们好好的把握。
参考
- C runtime library reference | Microsoft Learn
- _RPT, _RPTF, _RPTW, _RPTFW Macros | Microsoft Learn