C++调试输出流的实现

在开发过程中,调试是不可或缺的一部分。对于复杂的程序,跟踪程序的执行流程和变量的值是非常有帮助的。本文将介绍如何在C++中实现一个类似于标准模板库(STL)风格的调试输出流,以及如何使用RAII模式进行函数调用和对象创建的跟踪。

在编写一个用于特定数学计算的小程序时,发现程序的执行结果与预期不符。为了追踪程序的执行流程和变量的值,需要在特定的执行点输出一些信息。在控制台应用程序中,通常使用std::cout来实现这一功能。但是,如果希望保持控制台的整洁,或者在没有控制台的Windows应用程序中,这种方法可能不太适用。

解决方案

在Windows平台上,可以使用OutputDebugString函数来实现这一功能。但是,这个函数只能输出一个字符串,不能支持C++风格的接口。因此,目标是编写一个类似于STL流的类,使用OutputDebugString函数来输出数据。

实现细节

为了实现这个目标,需要创建一个类,该类重载了operator<<运算符,以便在调用Win32 API之前将输入数据转换为字符串。通过这种方式,可以避免重新实现STL的大部分格式化工具。

在STL中,所有的流都是基于std::basic_ostream基类的重新解释,为其提供了一个方便的std::basic_streambuf类的覆盖。流负责数据格式化,并通过选定的locale提供工具,而streambuf则提供了流写入的空间。流还负责将输出定向到媒体(在本例中是调试器;更一般地,是标准输出或打开的文件)。

为了正确处理这个任务,需要提供一个basic_streambuf的覆盖,将提供缓冲区的内容刷新到调试输出中。

如果希望在纯Win32环境中使用这样的函数,必须准备好使用TCHAR。因此,std::ostreamstd::wostream不适用,因为它们是预定义的字符类型。

需要另一个类型定义。在GE_::dbg命名空间中,放置了一个ostream_t类型定义,它是basic_ostream的别名。

一个内部的report_h命名空间用于包含实现细节。在其中,类globals声明为成员的ostream_t流对象和streambuf对象,其中streambuf是从std::basic_streambuf派生的类。

为了提供跟踪缩进,还有一个depth成员。不管streambuf的实现如何,globals构造函数初始化流,传递streambuf对象的地址。此外,内联的global函数提供了对globals的懒静态实例的访问。

在内部命名空间之外,GE_::dbg::out()返回对静态隐藏流的引用,使其全局可用。

streambuf类从std::basic_streambuf派生,其构造函数调用继承的pubsetbuf,设置一个提供的TCHAR[]缓冲区,保留一个字符作为guardian,用作字符串终结符。

这个实现的关键组件是setbufoverflowsync虚拟函数的覆盖。

setbuf覆盖简单地调用基setp函数,告诉它使用从第一个到倒数第二个字符的提供的缓冲区(因此总是保留最后一个字符为null)。

sync函数在流需要刷新缓冲区时被调用。当遇到std::flushstd::endl操作符时会发生这种情况。它只是获取当前运行的pbase()pptr()(流输出的开始和最后一个输出字符),计算字符串大小,强制最后一个输出字符之后的字符为'\0',并调用OutputDebugString,从而打印缓冲区的内容。之后,它调用pbump(-sz)来倒带缓冲区,允许流覆盖它。

overflow函数在流发送到输出的字符多于缓冲区可以容纳时被调用。它只是调用sync,从而刷新缓冲区,将“待处理”的字符放置在刚刚清理的缓冲区中,并增加写入位置。

上述描述的代码是实现以下代码片段所需的全部内容:

int n = 5; double d = 6.75; dbg::out() << "n = " << n << "; d = " << d << std::endl;

这将跟踪一行,如:

n = 5; d = 6.75

dbg::trace类是一个RAII类(一个其析构函数“撤销”了构造函数所做的资源的类),它有助于跟踪函数调用的进入/退出。

它接受nameaddress参数,并在构造/析构时增加/减少globals.depth成员。它的构造函数写入传递的name[address],前面有一个>符号。类似地,析构函数写入<name[address],具有相同的缩进。

通用operator<<将参数输出到流,前面有一个缩进的.。这种“效果”也可以通过在正常的<<链中使用dbg::dpeth操作符来获得。

因此,这个函数:

void function(int n) { dbg::trace trc("function", function); trc << "hallo with n = " << n << std::endl; if (n) function(n-1); }

如果用n = 3调用,将跟踪为:

>function[xxxxxxxx] .hallo with n = 3 >function[xxxxxxxx] .hallo with n = 2 >function[xxxxxxxx] .hallo with n = 1 >function[xxxxxxxx] .hallo with n = 0

dbg::track类类似于dbg::trace,但它使用"C:""D:"字符串而不是"<"">"符号,并且在写入时不增加/减少深度(同时与它一致)。

它可以用于跟踪对象的创建/销毁,如下所示:

class myobject { private: dbg::track trk; public: myobject() : trk("myobject", &trk) {} };

以及以下代码:

int main() { dbg::trace trc("main", 0); trc << "creating two objects on stack" << std::endl; myobject m1, m2; trc << "creating a static object" << std::endl; static myobject m3; trc << "Bye" << std::endl; }

将跟踪为:

>main[00000000] .creating two object on stack C:myobject[xxxxxxxx] C:myobject[yyyyyyyy] .creating a static object C:myobject[zzzzzzzz] .Bye D:myobject[yyyyyyyy] D:myobject[xxxxxxxx]

(注意静态对象在main返回后被销毁)

其他事项

插入跟踪代码的一个问题是使其在发布版本中消失(以避免跟踪字符串字面量和可能的冗长函数调用)。

在之前的文章中,提出了使用“假流”的方法,其操作实现为“什么都不做”,让编译器优化过程移除假调用。在这里,使用了一种更传统的方法。DBG宏定义为如果定义了_DEBUG符号,则为其参数;如果没有定义,则为“无”。

可以像这个代码片段一样放置调用:

void myfunc() { DBG(dbg::trace trc("myfunc", myfunc);) ... DBG(trc << "some code" << a_variable << std::endl;) ... }

或者在跟踪对象中插入一些虚拟变量,如:

class myclass { DBG(dbg::trackedobj trk;) public: ... };

希望这个新版本比其前身更加灵活和可用。一些旧文章的“功能”(如stackval操作符)在这里没有出现,主要是因为如果使用RAII包装器,它们不是必需的。

无论如何,已经将旧文章的文本和嵌入代码放在这个子页面上,供任何想查看历史的人使用。

这里介绍的代码是在C++8.0(VSC++ express)中编译的。代码本身对编译器不太敏感,但是编译器提供的STL的不同版本可能会有所不同。

一个特殊情况是使用UNICODE:当TCHAR定义为char(无unicode)时,wchar_t*类型由std::ostream作为void*处理(打印地址的十六进制表示)。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485