写一个简单的 C++ 日志库 - cllogger(3)- CRT

通过上一篇 《写一个简单的 C++ 日志库 - cllogger(2)- 日期时间》我们已经掌握了如何通过 std::chrono 提供的日期时间工具转换时间参数为指定格式的字符串。

现在我们可以把各个参数信息拼装为 Entry 实例,交给 OutputMessage()

cpp 复制代码
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() 将完成最终的日志字符串拼接并输出

cpp 复制代码
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

cpp 复制代码
    OutputDebugString(L"output message.");

如果我们希望只在debug版的时候才输出,则需要

cpp 复制代码
#ifdef _DEBUG
    ::OutputDebugString(L"DEBUG");
#endif // _DEBUG

2、 _RPTn 宏系列

_RPTn 中的n后缀指定*args*中的(即格式化数据的)参数个数,它可以是 0、1、2、3、4 或 5。_RPTWn 是对应的宽字符版本。

如,没有args时:

cpp 复制代码
_RPTW0(_CRT_WARN, message.c_str());

有1个参数时

cpp 复制代码
int foo = 3;
_RPTW1(_CRT_WARN, L"foo is %d", foo);

后续以此类推。

考虑编译器及配置的不同,下面的调用方式更为周密安全:

cpp 复制代码
#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 和 API 及操作系统之间的关系

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个字节的内存

cpp 复制代码
char* a = new char[10];

a 的地址为 0x01039099,查看内存

可以看到数据两端都填充了0xfd,初始化分配的数据填充为0xcd。

主动调用 _CrtDumpMemoryLeaks 检查泄露:

cpp 复制代码
#include <crtdbg.h>

int main()
{
	char* a = new char[10];
	_CrtDumpMemoryLeaks();
}

输出

捕获到了内存泄漏,且指明了内存地址和大小。

如果想输出泄露内存代码所在的文件名和行号,那就完美了,我们可以通过宏来做到

cpp 复制代码
#define DEBUG_NEW new(_CLIENT_BLOCK, __FILE__, __LINE__) 
#define new DEBUG_NEW

效果:

已经非常清晰明了了。我们再进一步,能否让内存泄露信息以弹出窗口的方式显示?完全可以,只需要通过 _CrtSetReportMode 设置报告现实的方式即可:

cpp 复制代码
#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 会简化开发,提供可靠性,提供层序性能,确保了程序的高效健壮,需要我们好好的把握。


参考

相关推荐
萝卜兽编程7 分钟前
优先级队列
c++·算法
Bruce小鬼8 分钟前
QT文件基本操作
开发语言·qt
2202_7544215414 分钟前
生成MPSOC以及ZYNQ的启动文件BOOT.BIN的小软件
java·linux·开发语言
我只会发热21 分钟前
Java SE 与 Java EE:基础与进阶的探索之旅
java·开发语言·java-ee
懷淰メ31 分钟前
PyQt飞机大战游戏(附下载地址)
开发语言·python·qt·游戏·pyqt·游戏开发·pyqt5
hummhumm1 小时前
第 22 章 - Go语言 测试与基准测试
java·大数据·开发语言·前端·python·golang·log4j
宁静@星空1 小时前
006-自定义枚举注解
java·开发语言
hummhumm1 小时前
第 28 章 - Go语言 Web 开发入门
java·开发语言·前端·python·sql·golang·前端框架
武子康1 小时前
Java-07 深入浅出 MyBatis - 一对多模型 SqlMapConfig 与 Mapper 详细讲解测试
java·开发语言·数据库·sql·mybatis·springboot
珹洺1 小时前
C语言数据结构——详细讲解 双链表
c语言·开发语言·网络·数据结构·c++·算法·leetcode