能够自我调试的代码






4.78/5 (67投票s)
2003年2月9日
34分钟阅读

328980

5112
用于检测和报告严重错误的宏集,结合稳健的代码编写技术。
目录
- 1.6.3.93 版本前言
- 第三版前言
- 引言
- 能够自我调试的代码
- 调试宏的要求
- QAFDebug 调试宏
- 使用调试宏
- 错误日志文件和 VC++ 调试跟踪窗口
- 发布版与调试版构建
- 为您的项目自定义调试日志
- 单元测试支持
- 语法着色和自动文本补全
- 附加示例
- 附录 A:参考文献
- 附录 B:C++ 标准 ASSERT 的缺点
- 附录 C:历史
- 附录 D:缺失的功能
- 附录 E:Q_TRACEX 宏和跟踪日志
1.6.3.93 版本前言
新版本增加了对 VC++ 6.0/7.0/7.1/8 的支持(已在 VC++ 6.0 SP 6 和 VC++ .NET 2005 beta 上测试),以及对 Linux 的支持(已在 Feudora Core 2 发行版上测试)。不幸的是,我无法在 VC++ .NET 2003 和 Embedded VC++ 上进行测试,但它应该仍然兼容。无论如何,我也会保留前一版本源代码,仅对论坛中建议的最小修复进行处理。新版本没有改变关键调试日志的功能,因此我建议您仅在需要新功能时才升级到新版本:VC++ 2005 支持、Linux 支持或跟踪日志类,这些都得到了显著改进(请参见 附录 E:Q_TRACEX 宏和跟踪日志)。
关于 Embedded VC++ 支持的说明:仅提供关键错误日志(qafdebug.h 和 qafdebug.cpp)。跟踪日志是使用模板开发的,据我所知,Embedded VC++ 不支持模板。
关于 Linux 支持的说明:与 Windows 相比,它并不完整。我没有为跟踪日志类实现多线程同步。没有打印版本号,甚至没有应用程序名称。我非常希望有人能帮助改进 Linux 支持。关于跟踪日志的一个重要之处是,在 Linux 上它们是通过环境变量配置的。
第三版前言
在发表这篇文章后,调试宏得到了显著改进,所以我决定发布新版本代码。
它支持 VC++ 6.0/7.0/7.1(已在 VC++ 6.0 SP 5 和 VC++ .NET 2003 上测试)以及 Embedded VC++ 3.0(感谢 Vadim Entov 将源代码移植到 Windows CE)。
错误输出消息格式已更改,以提高可读性和详细程度。现在它提供了关于错误的详细信息(包括错误代码和消息)、被调试应用程序的版本,以及可选的报告错误的源 DLL 模块的文件名和版本(当多个 DLL 共享同一源文件时,这是一个非常方便的功能)。此外,错误日志现在以 UTF-8 格式写入,这简化了 UNICODE 和 ASCII 模块的混合使用。
C:\SOColl\SOMain.cpp(137) : Check error: got 10 (0xA) while expected 0 (0x0)
time: 2003-12-08 17:34:33:443
process: 0x00000280
thread: 0x00000560
application: C:\Program Files\MSOffice\WINWORD.EXE <10.0.5522.0>
module: C:\SOColl\Debug\SOColl.dll <1.6.0.8>
last error: 2, The system cannot find the file specified.
expression: ERR_NO_ERROR == DoProcess( szInputFile )
添加了几种新宏:Q_CHECK
、Q_ERROR_SUCCESS
、Q_MAPI_ERROR
、Q_RETURN
、Q_MFC_EXCEPTION
、Q_STD_EXCEPTION
和 Q_SET_MODULE
(感谢 Andrey Krasnopolsky 在库开发中的帮助)。此外,还有一个可选的 QSockErr.h 头文件,实现了 Q_SOCKET_ERROR
宏,简化了套接字错误报告。
我仅更新了文章中的几个部分:有关新宏的描述,请参阅 使用调试宏;有关单元测试的更好介绍,请查看 单元测试支持;有关更改和改进,请阅读 为您的项目自定义调试日志;另外,不要错过新的 附加示例 部分,其中包含一些可视化调试宏强大功能的示例。
在尝试将此错误日志设施用作跟踪日志的几次之后,我决定解释一下我认为的错误日志和跟踪日志之间的区别。请参见 附录 E:Q_TRACEX 宏和跟踪日志。我已将跟踪日志设施附加到本文档中。请注意,我并没有在这方面投入大量时间,虽然我一直使用它,但我仍然找不到时间完成跟踪日志级别功能(我很乐意得到任何帮助)。因此,跟踪日志源代码按原样提供,并附有简短说明。
我要感谢论坛中的所有人帮助我更好地理解和论证我的观点,特别是 Marc Clifton、Daniel Turini、Sam Levy、Stoyan Damov、Alex Reser 和 Stewart Tootill。我也要感谢社区的组织者和维护者提供的想法、时间和精力。
引言
标准的 C++ 调试宏并不丰富。您的工具集中通常只有几个这样的宏及其变体:ASSERT()
和 TRACE()
(有关标准调试宏的完整列表,请参阅 [MSDN: Assertions])。在其通用实现中,ASSERT()
会弹出一个对话框并/或终止程序。TRACE()
通常将字符串输出到调试输出。
“断言自己”的概念在调试中非常有用,但它受到标准调试宏的限制。它们存在严重的缺点(请参见 附录 B:C++ 标准 ASSERT 的缺点)。它们不适用于我工作的项目:DLL 和 ActiveX COM 对象,通常在服务器上运行。它们缺少对我来说很重要的某些功能(例如,在发布版本中具有关键错误日志的能力,向该日志报告实际错误消息,以及控制模块版本)。我不能允许它们终止服务器进程。我不想让 QA 人员面对“中止、重试、忽略”的对话框。最后,我不喜欢这些 ASSERT
s 在源代码中的显示方式(请参见 列表 2)。我想要一个替代方案。
更重要的是,传统上这些调试宏的作用仅限于输入参数测试,而它们可以检测到广泛的编程错误。我认为标准 ASSERT
宏的设计限制了它的功能。为了克服 ASSERT
的局限性,我开发了一套简单、灵活且强大的调试宏,使我能够在开发早期捕获大多数编程错误。此外,这些宏作为一种工具,可以激发编写“自调试代码”。本文介绍的技术和工具已被证明在实际项目中非常有用。
能够自我调试的代码
在深入探讨实现细节之前,我想引导您了解编写“自调试代码”的思想。这个思想很简单,也很古老,不是我发明的:进行防御性编程,暴露缺陷,并且始终检查错误返回值(请参见 [The Practice of Programming, page 142])。
这是一个例子。假设您有一个如下函数(此示例特定于 Win32、VC++、COM 和 ATL)
// Listing 1: A regular C++ source code (it will not crash by AV) HRESULT ConvertFile2File( BSTR bstrInFileName, BSTR * pbstrRet ) { if( NULL == bstrRet ) return E_INVALIDARG; // Create the object (I use a descendant of CComDispatchDriver) QComDispatchDriver piConvDisp; HRESULT hr = piConvDisp.CreateObject ( L"QTxtConvert.QTxtConv", CLSCTX_LOCAL_SERVER ); if( FAILED(hr) ) return hr; hr = piConvDisp.Invoke1( L"Convert", CComVariant(bstrInFileName), NULL ); if( FAILED(hr) ) return hr; CComVariant varTemp; // Get the result hr = piConvDisp.Get( L"Target", &varTemp ); if( FAILED(hr) ) return hr; hr = varTemp.ChangeType( VT_BSTR ); if( FAILED(hr) ) return hr; CComBSTR bstrResult = varTemp.bstrVal; // Get the optional Author property hr = piConvDisp.Get( L"Author", &varTemp ); if( Q_SUCCEEDED( hr ) && Q_ASSERT( VT_BSTR == V_VT(&varTemp) ) ) { CComBSTR szAuthor = V_BSTR( &varTemp ); // The Author property may be an empty string - it's okay! if( szAuthor.Length() > 0 ) { bstrResult += L"&>"; bstrResult += szAuthor; } } return bstrResult.CopyTo(pbstrRet); }
这个函数是防御性的,并且错误处理是恰当的。对于发布版代码来说,这已经相当不错了,但首先您必须调试这段代码。这里我有一个问题 - 虽然这个函数能够准确地返回错误代码,但我不知道是什么原因导致了错误。这个函数可能从 6 个不同的地方返回相同的 E_FAIL
。要知道它在哪里失败的唯一方法是在调试器中一步步跟踪,这并非总是可能的。用户或 QA 人员会发现某个功能不起作用,但却没有任何问题诊断。您会怎么做?
好吧,这时 ASSERT()
宏就来帮我们了。使用 ASSERT
宏,我们可以在错误发生时立即发现它。我们可以测试输入参数、非法条件和我们的设计假设。看看修改后的代码
// Listing 2: A good C++ source code (it will prompt on any error) HRESULT ConvertFile2File( BSTR bstrInFileName, BSTR * pbstrRet ) { // test the input parameters ASSERT( NULL != bstrRet ); ASSERT( SysStringLen(bstrInFileName) > 0 ); if( NULL == bstrRet ) return E_INVALIDARG; // Create the object (I use a descendant of CComDispatchDriver) QComDispatchDriver piConvDisp; HRESULT hr = piConvDisp.CreateObject ( L"QTxtConvert.QTxtConv", CLSCTX_LOCAL_SERVER ); // I assume that the object is always installed and available. // If it is not created, this is definitely a bug. ASSERT( SUCCEEDED(hr) ); if( FAILED(hr) ) return hr; hr = piConvDisp.Invoke1( L"Convert", CComVariant(bstrInFileName), NULL ); // This method should usually never fail. // If something wrong happens, it is most // possible a bug, I want to know about it. // Attention: I filter one expected error first! ASSERT( (E_INVALID_FORMAT == hr) || SUCCEEDED(hr) ); if( FAILED(hr) ) return hr; CComVariant varTemp; // Get the result hr = piConvDisp.Get( L"Target", &varTemp ); // If this property is not available, it is a bug. ASSERT( SUCCEEDED(hr) ); if( FAILED(hr) ) return hr; hr = varTemp.ChangeType( VT_BSTR ); // If the property has a wrong type, it is a bug. ASSERT( SUCCEEDED(hr) ); if( FAILED(hr) ) return hr; CComBSTR bstrResult = varTemp.bstrVal; // Get the optional Author property hr = piConvDisp.Get( L"Author", &varTemp ); // If this property is not available, it is a bug. // If the property has a wrong type, it is a bug. ASSERT( SUCCEEDED(hr) ); ASSERT( VT_BSTR == V_VT(&varTemp) ); if( SUCCEEDED( hr ) && ( VT_BSTR == V_VT(&varTemp) ) ) { CComBSTR szAuthor = V_BSTR( &varTemp ); // The Author property may be an empty string - it's okay! if( szAuthor.Length() > 0 ) { bstrResult += L"&>"; bstrResult += szAuthor; } } hr = bstrResult.CopyTo(pbstrRet); // If I cannot copy a string, it is a catastrophe! ASSERT( SUCCEEDED(hr) ); return hr; }
它长且丑陋吗?- 是的,我同意。它滥用了 ASSERT
吗?- 完全不是!(可能只是一点点 :))
ASSERT
的通常位置是在函数开头,用于测试输入参数。大多数程序员到此为止。我认为他们忘记了 ASSERT
也是用于测试设计假设和非法条件的工具。我假设对象必须已注册并可用。我假设它在 Convert()
中永远不会失败。我假设有两个 BSTR
属性。最后,我假设我的对象不会耗尽计算机的所有内存。所有这些都是我的设计假设。我通过添加 ASSERT
s 来测试我的假设。这一点丝毫不会消除常规错误处理。
好了,我在这里混合了运行时错误条件(内存溢出)和运行时编程错误或非法条件(输入参数中的NULL
指针)。我认为它们非常接近,因为它们不是我的算法所期望的。这些是意外错误,应该由 ASSERT
s 处理。我永远不会使用断言来测试预期错误(例如写入只读文件)。您可以在代码中看到,我过滤了一个在该函数中可能发生的预期错误:Convert
方法可能返回 E_UNKNOWN_FORMAT
错误。没有其他预期错误(如文件读取错误),因为输入文件是由调用进程生成的,并且必须始终可供我使用。如果无法读取,这是一个意外错误。
我绝对想要所有这些调试功能,但我不喜欢代码的外观。即使我知道代码在某个特定位置失败了,我也想知道原因 - 至少要获得 HRESULT
代码。此外,我可能要重申我在 附录 B:C++ 标准 ASSERT 的缺点 中所写的一切。我认为现在是时候推出正确的工具了。
调试宏的要求
在论坛上讨论本文时,我引用了 Brian W. Kernighan 和 Rob Pike 的《The Practice Of Programming》。他们说,大多数程序员(公司)都会编写自己的 ASSERT
宏版本来克服标准宏的局限性。他们只是建议使用其他名称,以免破坏其他人的代码。
当我第一次开始考虑一套调试宏时,我定义了以下要求
- 只有几个易于使用的函数或宏,具有清晰的语义(使其用户友好……抱歉 - 开发友好!)。
- 错误报告发送到文件或调试控制台(无弹出对话框,请参阅 附录 B 的缺点 2)。
- 启用/禁用调试宏不应影响正常程序执行(添加调试宏不应导致表达式被评估两次,请参阅 附录 B 的缺点 5)。
- 调试宏可以透明地集成到源代码中,并在发布版本中移除它们不应改变算法(每个
ASSERT
宏通常占据单独一行,并导致程序重复测试条件两次)。 - 根据条件编译,错误报告必须在调试和发布版本中都可用(在某些情况下,我希望在发布版本中保留错误日志 - 这使得早期生产中的缺陷检测更容易)。
在开发调试宏时,我提出了一些额外的要求,其中大部分是我假设的但未明确说明的
- 调试宏必须报告尽可能多的错误信息:位置、模块、错误代码、错误消息和最后的系统错误。
- 当没有错误发生时,调试宏必须非常轻量级(开销尽可能小)。但如果需要报告错误,报告时间不重要(开销不重要)。
- 调试宏的实现不应依赖 C++ 语言的 RTTI、异常或模板功能(例如,Windows CE 并不完全支持它们)。
- 调试宏必须允许多个线程和进程并发地向错误日志报告。然而,由于关键错误通常很少发生(每天很少发生),同步机制可以简化。
- 应用程序和发生错误的 DLL 的版本号 - 这有助于识别负责的 DLL,即使有多个 DLL 共享一些源代码,也有助于在源代码控制系统中找到正确的源代码版本。
我意识到使用 C++ 语法可以非常轻松地满足这些要求。我花了大约 10 个版本才得到第一套调试宏,又花了一年时间将其改进到目前的水平 :-).
QAFDebug 调试宏
bool Q_ASSERT( bool )
这个宏类似于常规的
ASSERT()
宏。它返回与接收到的值相同。如果bool
为false
(良好条件失败),它会报告。// report if the variable is NULL - it is an unexpected error if( Q_ASSERT( NULL != lpszString ) ) ; // process the string else return ERROR; // you may even skip "else"
bool Q_INVALID( bool )
这个宏在测试参数时非常有用。它返回与接收到的值相同。如果
bool
为true
(不良条件成功),它会报告。// report if the variable is NULL - it is an unexpected error if( Q_INVALID( NULL == lpszString ) ) return ERROR;
bool Q_SUCCEEDED( HRESULT )
这个宏类似于常规的
SUCCEEDED()
宏。它实际上在内部使用了SUCCEEDED()
。如果HRESULT
是错误,它会报告。hr = varTemp.ChangeType( VT_BSTR ); // report if the HRESULT failed - it is an unexpected error if( Q_SUCCEEDED( hr ) ) ; // process the string else return hr; // you may even skip "else"
bool Q_FAILED( HRESULT )
这个宏类似于常规的
FAILED()
宏。它实际上在内部使用了FAILED()
。如果HRESULT
是错误,它会报告。// report if the HRESULT failed - it is an unexpected error if( Q_FAILED(hr) ) return hr;
HRESULT Q_ERROR( HRESULT )
这是一个特殊宏,它返回相同的输入值。它在获取结果并立即返回而无需测试的地方很有用。
// report if the operation failed - it is an unexpected error // while the caller will know about the error, I still // want to get the first sign from the original source of the error return Q_ERROR( bstrResult.CopyTo(pbstrRet) );
bool Q_ERROR_SUCCESS( DWORD )
这是一个用于测试许多 Win32 API 函数(例如注册表函数)返回的
ERROR_SUCCESS
兼容错误码的宏。如果给定的错误码与ERROR_SUCCESS
常量不同,此宏将报告。它还将报告错误消息。DWORD dwRet = RegQueryValueEx( hKey, _T("Value"), NULL, &dwType, (LPBYTE)szBuf, &dwSize ); if( Q_ERROR_SUCCESS( dwRet ) ) // here it will report about any error /*do something*/; // this line is executed only if no error occured
bool Q_CHECK( dwConstant, dwExpression )
这是一个用于测试任何预期值的宏。如果预期的
dwConstant
与dwExpression
返回的结果不同,此宏将报告。它将同时报告预期值和接收到的值。DWORD dwErr = DASetOption( NULL, SCCOPT_FIFLAGS ); if( Q_CHECK( DAERR_OK, m_dwErr ) ) // report about any error /*do something*/; // this line is executed only if no error occured
void Q_MAPI_ERROR( HRESULT, MAPI_Interface )
这是一个用于报告 MAPI 接口返回错误的特殊宏。每个 MAPI 接口都有一个特殊的设施,用于将错误代码转换为错误消息。此类代码需要十多行,所以我将其实现为宏。此宏必须用作语句,而不是表达式。
HRESULT hr = piProfAdmin->CreateProfile( szName, NULL, 0, MAPI_DEFAULT_SERVICES ); if( Q_FAILED( hr ) ) // take care of standard HRESULTs { // decode and report the MAPI error Q_MAPI_ERROR( hr, piProfAdmin ); return hr; }
int Q_SOCKET_ERROR( int )
这是一个用于报告从 Windows Sockets API (WinSock) 的
WSAGetLastError()
函数返回的错误的特殊宏。虽然 WinSock 有许多文档记录良好的错误代码,但它没有返回错误的可读字符串消息的函数(据我所知)。此宏将错误代码转换为字符串消息并返回。如果错误代码为 0,则什么也不做。此宏定义在 QSockErr.h 头文件中。if( SOCKET_ERROR == listen( socket, SOMAXCONN ) ) { int nRet = WSAGetLastError(); if( WSAENETDOWN != nRet ) // this is the only expected error Q_SOCKET_ERROR( nRet ); // report about unexpected error return nRet; // in any case, return the error }
void Q_RETURN( hr )
这是一个快捷宏,用于测试
HRESULT
表达式并在发生任何故障时从函数返回。它代表以下逻辑if( Q_FAILED(hr) ) return hr;
实际代码要复杂一些,以防止多次执行被测试的表达式。此宏必须用作语句,而不是表达式。
// The following code works much better than the MFC-wizard // generated one - it reports the actual location of the problem. Q_RETURN( piWordApp->GetDocument( &piDoc ) ); Q_RETURN( piDoc->GetSelection( &piSel ) ); Q_RETURN( piSel->Copy() );
void Q_MFC_EXCEPTION( CException )
void Q_EXCEPTION( CException ) // 向后兼容的旧宏名称
这是一个非常特殊的宏,对于处理 MFC Wizard 生成的代码中的异常非常有用。它报告异常中包含的错误消息。此宏必须用作语句,而不是表达式。
catch{ CException & e ) { // report about the exception - it is an unexpected error // (In a case if some exceptions are expected, I will // process them differently. Q_MFC_EXCEPTION( e ); }
void Q_STD_EXCEPTION( std::exception )
这是一个非常特殊的宏,对于处理 C++ 标准库异常非常有用。它报告异常中包含的错误消息。此宏必须用作语句,而不是表达式。
catch{ std::exception & e ) { // report about the exception - it is an unexpected error // (In a case if some exceptions are expected, I will // process them differently. Q_STD_EXCEPTION( e ); }
void Q_LOG( LPCTSTR )
此宏将自定义错误消息写入错误日志。它取代了不具描述性的
Q_ASSERT(false)
。此宏必须用作语句,而不是表达式。DLLEXPORT int Func01( LPCTSTR lpszString ) { // report about the unexpected function call Q_LOG( _T("This function is deprecated " "and it should never be called!") ); }
void Q_SET_MODULE( szModuleName )
这是一个用于设置当前 DLL 模块名称的特殊宏。我没有找到一种简单自动的方式来知道代码在哪个 DLL 的上下文中执行,所以我使用一个特殊的静态字符串变量。您只需指定 DLL 模块的文件名,它就会在运行时自动查找其位置和版本号。此宏对于 EXE 模块不是必需的。通常,它必须添加到
DllMain()
函数中。此宏必须用作语句,而不是表达式。BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: { Q_SET_MODULE( _T("MODULE.DLL") ); ...
使用调试宏
现在,让我们回到那个函数并添加调试宏。一旦我们添加了调试宏,函数就会在意外错误发生时小心地报告所有意外错误。而且错误报告工作在代码中所占空间不大,实现起来也很容易。我的意思是,不多不少。
// Listing 3: The instrumented C++ source code #include "QAFDebug.h" ... HRESULT ConvertFile2File( BSTR bstrInFileName, BSTR * pbstrRet ) { // test the input parameters if( Q_INVALID(NULL == bstrRet) || Q_INVALID(SysStringLen(bstrInFileName) <= 0) ) return E_INVALIDARG; // Create the object (I use a descendant of CComDispatchDriver) QComDispatchDriver piConvDisp; HRESULT hr = piConvDisp.CreateObject( L"QTxtConvert.QTxtConv", CLSCTX_LOCAL_SERVER ); // I assume that the object is always installed and available. // If it is not created, this is definitely a bug. if( Q_FAILED(hr) ) return hr; hr = piConvDisp.Invoke1( L"Convert", CComVariant(bstrInFileName), NULL ); // This method should usually never fail. // If something wrong happens, it is most // possible a bug, I want to know about it. // Attention: I filter one expected error first! if( (E_INVALID_FORMAT == hr) || Q_FAILED(hr) ) return hr; CComVariant varTemp; // Get the result hr = piConvDisp.Get( L"Target", &varTemp ); // If this property is not available, it is a bug. if( Q_FAILED(hr) ) return hr; hr = varTemp.ChangeType( VT_BSTR ); // If the property has a wrong type, it is a bug. if( Q_FAILED(hr) ) return hr; CComBSTR bstrResult = varTemp.bstrVal; // Get the optional Author property hr = piConvDisp.Get( L"Author", &varTemp ); // If this property is not available, it is a bug. // If the property has a wrong type, it is a bug. if( Q_SUCCEEDED( hr ) && Q_ASSERT( VT_BSTR == V_VT(&varTemp) ) ) { CComBSTR szAuthor = V_BSTR( &varTemp ); // The Author property may be // an empty string - it's okay! if( szAuthor.Length() > 0 ) { bstrResult += L"&>"; bstrResult += szAuthor; } } // If I cannot copy a string, it is a catastrophe! return Q_ERROR( bstrResult.CopyTo(pbstrRet) ); }
这些宏的语法很清晰。如您所见,表达式只计算一次,因此算法在调试版和发布版中保持不变。如果您禁用调试功能,它只会删除这些 Q_
符号,而不影响程序逻辑(这是一个比喻,但接近事实)。
当出现问题时(程序员试图访问无效属性名,或者内存不足),程序会将所有可用的错误信息写入错误日志文件。
C:\Work\XM\XMClient.cpp(770) : Unspecified error
time: 2003-12-22 12:34:48:044
process: 0x000012D4
thread: 0x00001088
application: c:\connector\xmsrv.exe <1.0.0.15>
module: c:\connector\XMClient.dll <1.0.0.24>
last error: 1008, An attempt was made to reference
a token that does not exist.
expression: FAILED(0x80004005)
您在这里拥有所有必需的信息:托管进程的名称和版本、最后的错误代码、您的模块及其版本、源文件中的位置以及错误的描述(当然,本例中提供的 E_FAIL
并不十分具有描述性:),但也有其他错误代码)。在许多情况下,只需查看 QA 计算机上的日志记录就足以了解您的程序出了什么问题。
在调试版中,相同的日志消息会输出到 VC++ IDE 的调试日志中。这可以快速导航到错误源。
要使用调试宏,您需要将两个文件添加到项目中:QAFDebug.h 和 QAFDebug.cpp。您可能需要将程序链接到 version.lib 库(在 VC++ 6.0 中,默认情况下它未链接到项目)。我还建议调整源代码中的几个常量,以定义日志文件的所需位置和最大大小。请参阅 自定义 部分。
错误日志文件和 VC++ 调试跟踪窗口
我喜欢在运行调试器时,在 VC++ IDE 的调试跟踪窗口中打印错误日志,否则就将其写入文件。这就是我在这里实现的。错误日志被写入文件,并复制到 VC++ IDE 或其他跟踪工具(如 http://www.sysinternals.com/ 的 DebugView)。我有许多不同的 ActiveX 和 DLL,它们由不同的进程托管,因此我需要一个单一的、已知的日志文件位置。我决定将其放在文件夹下
C:\Documents and settings\UserName\Application Data\MyCompany\Log\error.log
请注意,Windows CE 没有大的文件系统。实际上,它只有大约四个标准的根文件夹,仅此而已。因此,在 Windows CE 上,错误日志被写入 我的文档 文件夹的模拟位置。在该文件夹中,它会创建一个子文件夹 \MyCompany\Log\。事实上,我建议在 Windows CE 的发布版本中禁用错误日志 - 手持设备资源有限。
对于 NT 服务托管的 DLL(它们没有当前用户帐户),我使用 All Users 或 Default User 文件夹(具体取决于 Windows 版本)。或者,您可以定义一个名为 QAFERRORLOGPATH
的环境变量。我认为使用环境变量是 NT 服务最理想的方式。
无限增长的错误日志是危险的。因此,我实现了日志文件大小限制(默认为 1MB)。每个时刻有两个日志文件 - 当前文件和前一个文件。通常,一条记录大约占用 300-600 字节,因此我预留了大约 2000 条记录的空间。这应该足够了,并且大小限制确保两个日志文件加起来不超过 1MB。请注意,通常情况下,如果应用程序按预期工作,您将找不到错误日志文件。
所有进程都写入同一个日志文件。当然,对其访问应该进行同步。但由于这是关键错误日志而不是常规跟踪日志,并且关键错误很少发生,所以我使用文件本身进行同步。我会在打开文件时锁定它,如果文件被其他人锁定,我会尝试多次打开文件(最多 200 毫秒)。这可以防止任何类型的死锁。
发布版与调试版构建
Jon Bentley 在他的 [Programming Pearls, page 50] 中引用了 Tony Hoare 的话,他说一个程序员在测试时使用断言,而在生产中关闭它们,就像一个水手在陆地上训练时穿救生衣,而在海上却脱掉它。关于“在发布版本中是否断言”的问题已经在论坛上引起了很多讨论。我认为答案很简单:一旦您知道产品稳定,就可以自由地关闭断言以提高性能和安全性。您也可以在产品不稳定时关闭它们 :)。
在某些情况下,我认为在发布版本中也保留关键错误日志是有用的。在一家初创公司和生产早期阶段,QA/RND 循环非常短,测试未覆盖 100% 的功能,并且新版本几乎立即投入生产(对我来说,作为开发人员,即使是内部版本或试点项目也意味着“生产”),就没有机会发布完全测试过的版本。开发人员收到许多外部的缺陷报告,在大多数情况下很难重现。在这种情况下,关键错误日志变得极其有用。这些宏使程序体积增加几 KB,并对性能产生轻微影响(在未检测到错误时,每个宏最多执行两个汇编指令),但付出的代价是值得的。
这里提供的宏集非常灵活 - 您可以根据调试/发布版本选择启用/禁用它。甚至它的特定部分也可以启用/禁用。根据我的经验,在调试版本中启用最大报告是有用的,而在发布版本中只保留错误日志文件(通常没有人会跟踪发布产品)。这些是默认设置。如果您想覆盖此设置,请看下一节。我也关闭了发布版本中的单元测试支持,因为单元测试有点笨重。
为您的项目自定义调试日志
首先,我建议您修改 QAFDebug.h 中的以下常量,并将其定义为对您的项目唯一的名称
#define QAFDEBUG_COMPANY_NAME "YourCompany"
您的公司名称,它定义了应用程序数据文件夹中的唯一子文件夹,以及用于配置的注册表项。
QAFDEBUG_LOG_ENV_VAR = _T("QAFERRORLOGPATH")
用于设置输出调试日志文件夹的环境变量的名称。
QAFDEBUG_SILENCE_MUTEX = _T("QAFDebugMutex001A")
用于与单元测试支持人员同步的互斥体的名称。
QDEBUG_SHMEMFILE = _T("QAFDbgMemFile01")
用于存储单元测试支持人员共享标志的内存映射文件的名称。
您可能还想在 QAFDebug.h 中更正以下常量
QAFDEBUG_LOG_FILE_MAX_SIZE = (1024 * 1024)
日志文件的最大大小(以字节为单位,而不是像以前版本那样以字符为单位)。
QAFDEBUG_LOG_FILE_NAME = _T("error.log")
当前错误日志文件的名称。
QAFDEBUG_LOG_OLD_FILE_NAME = _T("error.old.log")
上一个错误日志文件的名称。
以下定义允许关闭调试日志的特定功能(或完全关闭任何报告)
QAF_DISABLED
关闭任何错误报告(默认不使用)。
QAF_LOGFILE_DISABLED
关闭写入错误日志文件(默认不使用)。
QAF_OUTPUTDEBUGSTRING_DISABLED
关闭写入调试跟踪窗口(默认在发布版本中定义)。
QAF_UNITTEST_DISABLED
关闭单元测试支持(默认在发布版本中定义)。
单元测试支持
我使用 CppUnit 进行单元测试自动化。在审查了许多 C++ 单元测试框架后,我决定选择最适合我的需求的。编写单元测试是一项繁琐的工作,但它后来为我节省了调试和测试代码更改的时间。
在 CppUnit 中编写测试用例并不难。除了易于学习的测试用例、测试套件和夹具类之外,还有一些“主力”宏。其中最基本的是 CPPUNIT_ASSERT
宏。此宏执行一个表达式,并报告其成功或失败。典型的测试用例可能如下所示
void CUnitTest::testCase01( void ) { CPPUNIT_ASSERT( SUCCEEDED( QAFGetRegKey() ) ); CPPUNIT_ASSERT( 0 == _tcscmp( ST_VAR, _T("100%") ) ); ... }
如果任何一行失败,CppUnit 将报告失败的位置。我的问题是它无法报告具体发生了什么——CppUnit 只知道表达式的最终结果:TRUE
或 FALSE
。这时调试宏就派上用场了。如果被调用的函数使用了 QAFDebug
宏进行插装,它们将仔细地向调试日志文件报告函数体内发生了什么问题。单元测试运行器可以收集错误日志文件并将其附加到单元测试报告日志中。
通常,单元测试不仅测试函数是否正常工作。一个好的单元测试还应该测试带错误参数的函数调用,并确保它返回正确的错误代码。在这种情况下,错误就变成了正常、期望的行为。例如,请看以下单元测试用例
void CUnitTest::testCase01( void ) { CPPUNIT_ASSERT( FAILED( QAFGetRegKey( NULL ) ) ); CPPUNIT_ASSERT( 0 != _tcscmp( ST_VAR, _T("0%") ) ); ... }
在这种情况下,我需要暂时禁用错误日志,测试函数是否失败,然后重新启用错误日志。为了支持这一点,我添加了两个宏
void Q_SILENT( expression );
此宏禁用错误日志,计算表达式,然后重新启用错误日志。
void Q_ENABLE_DEBUG_LOG;
此宏无条件启用错误日志。当日志可能因前一个测试用例中抛出的异常而保持禁用状态时,此宏非常有用。通过包含它,您可以确保错误日志已打开。
void CUnitTest::testCase01( void ) { // ensure that the error log is switched on Q_ENABLE_DEBUG_LOG; // write to the error log if the test case fails CPPUNIT_ASSERT( SUCCEEDED( QAFGetRegKey() ) ); CPPUNIT_ASSERT( 0 == _tcscmp( ST_VAR, _T("100%") ) ); // skip writing to the error log since this function should fail Q_SILENT( CPPUNIT_ASSERT( FAILED( QAFGetRegKey( NULL ) ) ) ); Q_SILENT( CPPUNIT_ASSERT( 0 != _tcscmp( ST_VAR, _T("0%") ) ) ); ... }
单元测试支持会引入一些开销。进程之间的同步需要使用互斥体和内存映射文件。因此,我只在调试版本中保留它,而在发布版本中排除它。通常,我对单元测试也做同样的处理——只有调试版本包含测试。同样,当被测试的应用程序开始向错误日志文件写入数百条消息时,开销就成了一个问题。如果没有发生错误,每个宏只会执行几个额外的汇编指令。
语法着色和自动文本补全
有很多机会可以使编程更简单、更快捷。我在此介绍几个插件(大多数 VC++ 程序员应该已经知道它们),它们不仅在调试宏方面,而且在整个编码过程中都对我有所帮助。您可以在 QAFDebug_doc.zip 中找到相应的文件。
UserType.dat 文件使 Visual Studio 6.0 能够将常见的 ATL 和 MFC 类、类型、宏和函数突出显示为关键字(蓝色)。这非常有帮助,因为有颜色的标准类型易于识别,并且您可以减少语法错误。我已将我的调试宏添加到此文件中,因此它们也会被突出显示。此文件应放在文件夹下
C:\Program Files\Microsoft Visual Studio\Common\MSDev98\Bin
VAssist.tpl 文件包含 Visual Assist 6.0 的键盘快捷键,可加快常用构造的输入速度。Visual Assist 6.0(来自 http://www.wholetomato.com/)是 Visual Studio 6.0 最有用的插件之一,它升级了 VC++ 的 IntelliSense 功能。我强烈推荐此插件。我添加了几个用于我的宏的键盘快捷键,以使我的生活更轻松。您不应该替换此文件,而应将其内容添加到现有文件中。
现在,当我键入 qs
时,它会自动插入 Q_SUCCEEDED()
。其他快捷键的含义是:qa
(Q_ASSERT()
)、qi
(Q_INVALID()
)、qf
(Q_FAILED()
)、qe
(Q_ERROR()
)、qx
(Q_EXCEPTION(e)
)、ql
(Q_LOG( _T("") );
)、qc
(Q_CHECK( , )
)和 qr
(Q_RETURN();
)。此文件通常位于以下文件夹(您也可以从 Visual Assist 选项中编辑它)
C:\Program Files\Visual Assist 6.0\Misc
附加示例
不时地,我需要处理 JNI(Java Native Calls)DLL 模块。通常,模块的接口对错误报告并不太关心——对调用者来说,唯一重要的是调用是否成功。因此,事后故障分析就没有太多内容了。让我们看一个在正常条件下永远不会失败的函数。只有当它遇到意外情况时,它才会返回失败。以下 JNI 包装器调用 C 函数 LRS()
。它不报告从该函数返回的错误代码。相反,它在任何错误情况下只返回一个空字符串。关键错误日志会记录所有内部故障。
JNIEXPORT jstring JNICALL Java_com_company_ta_Detector_getTaFromFile (JNIEnv * env, jclass cls, jstring jFileName) { if( Q_INVALID( NULL == env ) ) return NULL; if( Q_INVALID( NULL == jFileName ) ) return env->NewStringUTF( "" ); char szLang[MAX_PATH] = { 0 }; const char * FileName = NULL; FileName = env->GetStringUTFChars( jFileName, NULL ); if( Q_INVALID( NULL == FileName ) ) return env->NewStringUTF( "" ); int nRet = LRS( FileName, szLang ); if( !Q_CHECK( NO_ERROR, nRet ) ) return env->NewStringUTF( "" ); return env->NewStringUTF( szLang ); }
另一个例子来自自定义套接字连接协议。任何数据传输协议都严重依赖于许多假设(缓冲区大小、常量、不同变量之间的关系)。使用带跟踪的异常实现复杂的错误处理可能很麻烦,并且可能会影响性能。在协议完全调试后,错误处理很少需要。在这种情况下,断言变得非常有用。实际上,在此函数中使用断言帮助我发现并修复了一个非常罕见且难以处理的错误。
// Write the attachment data if there is enough space if( (0 != m_dwAttachSize) && (ulBufSize > ulBytesSentRet) ) { if( Q_INVALID( m_dwRemained > m_dwAttachSize ) ) { Q_CHECK( m_dwRemained, m_dwAttachSize ); return OP_FAILED; } // space remained in the socket buffer unsigned long ulDataSize = ulBufSize - ulBytesSentRet; // if there is less actual data than the buffer space if( ulDataSize > m_dwRemained ) ulDataSize = m_dwRemained; // take the actual data unsigned long ulWritten = 0; // for the file attachment read the data from the file DWORD dwPos = m_dwAttachSize - m_dwRemained; if( 0x0000 == m_wCodePage ) { dwPos = 0; bool bRet = ReadAttachment( dwPos, ulDataSize ); if( Q_INVALID( ! bRet ) ) { Q_CHECK( 0, dwPos ); Q_CHECK( 0, ulDataSize ); return OP_FAILED; } } // cannot read the file by some reason if( Q_INVALID( NULL == m_pAttachment) ) return OP_FAILED; // send the data to the socket if( 0xFFFF == m_wCodePage ) ret = pChannel.SendUTF16( m_pAttachment + dwPos, ulDataSize, ulWritten ); else ret = pChannel.Send( m_pAttachment + dwPos, ulDataSize, ulWritten ); // a couple of things that should never happen Q_INVALID( OP_NEED_MORE_DATA == ret ); Q_INVALID( 0 == ulWritten ); if( OP_FAILED == ret ) return ret; ulBytesSentRet += ulWritten; m_dwProcessed += ulWritten; m_dwRemained -= ulWritten; } Q_CHECK( GetPacketSize(), m_dwProcessed + m_dwRemained );
附录 A:参考文献
- Debugging Applications by John Robbins
一本关于 Win32 应用程序调试的出色书籍。
- Writing Solid Code by Steve Maguire
这本书应该在学校里学习……我是指编程学校!:) 这是我的最爱。以非常流行的方式,它讨论了编写高质量程序并涉及整个开发周期。
- The Pragmatic Programmer - From Journeyman to Master by Andrew Hunt and David Thomas
我的另一本最爱。它与《Writing Solid Code》属于同一类型,但当然有所不同。在我看来,必读。
- Code Complete by Steve McConnell
这本书有点学术化,但也是推荐的。我说它缺少一点乐趣。
- The Practice of Programming by Brian W. Kernighan and Rob Pike
这本书有点过时,但仍然有价值。
- Programming Pearls by Jon Bentley
这本书更多地谈论算法而不是编码风格。但有一些有趣的章节。
- ATL/AUX Library by Andrew Nosenko
这个库中调试宏的思路与我的非常相似。它们将
HRESULT
错误代码转换为有意义的错误消息。 - Extended Debug macros by Jungsul Lee
那里的宏只实现了在发布版本中进行错误处理的思路。没有日志文件,我也不喜欢中断程序执行的想法。
- A Treatise on Using Debug and Trace classes... by Marc Clifton
这篇文章的作者实际上启发了我重写这篇文章并更好地论证我的观点。我也喜欢从他的经验中学习。
- Considerations for implementing trace facilities in release builds... by Vagif Abilov
看看标题。很棒。
- MSDN: Assertions by Microsoft (C)
VC++ 中所有可用断言的简短完整参考。
- Enhancing Assertions by Andrei Alexandrescu and John Torjo
一篇关于使用模板构造
ASSERT
s 的非常有趣的文章。 - CppUnit
我无疑会推荐的 C++ 单元测试框架。
- A set of template classes for working with the registry by Daniel Andersson
曾经,我采用了他一套简单的注册表工作类,并一直使用至今。虽然原始源代码已得到显著修改,但我仍然保留了对原作者的链接。这些类在 QTrace 跟踪日志设施中使用。
附录 B:C++ 标准 ASSERT 的缺点
这是 ANSI assert()
函数显示的标准的断言消息示例。就我个人而言,我不喜欢 VC++ 自带的标准断言。它们存在许多细小但令人讨厌的缺陷。我相信我在这里介绍的框架也充满了缺点 :)(附录 D 只列出了缺失的功能)。尽管如此,我的框架为我提供了更多的好处。
- 缺点 1:信息缺失。
常规的
assert
只知道进程名、源文件名和行号。我认为打印时间戳、进程 ID、线程 ID、最后错误代码和错误消息本身也很有用。我将此信息与格式相似的跟踪日志结合使用。最有用的字段是错误消息和最后错误代码。 - 缺点 2:弹出对话框。
大多数标准的
ASSERT
宏会弹出一个类似屏幕截图的对话框。当您处理一个没有 UI 的项目时,例如 Windows NT 服务或 COM 进程外服务器,用户可能会错过此对话框,甚至可能导致进程挂起(请参阅 [Debugging Applications, page 60])。此外,我从未见过 QA 人员喜欢这些对话框,因为除了选择“忽略”之外,他们什么也做不了。我更喜欢一个单一的、已知的错误日志文件。 - 缺点 3:将
stderr
重定向到文件。C 运行时库允许通过调用
_CrtSetReportMode
函数将断言重定向到文件或OutputDebugString
API(请参阅 [Debugging Applications, page 61])。但这些函数都不处理所有程序的通用文件位置。它们也不会在文件过大时截断文件。(我这里不是完全诚实——可以编写一个辅助类来完成)。最后,如果您开发的是 COM 进程内对象或 DLL,您将无法重定向stderr
,因为它属于整个进程(这是我使用常规ASSERT
s 时遇到的一个问题)。 - 缺点 4:改变系统状态。
所有 Microsoft 提供的断言都有一个致命的缺陷:它们会改变系统状态。这个问题在 [Debugging Applications, page 62] 中有详细讨论,这里我将简要解释。许多 Win32 API 函数使用
SetLastError
和GetLastError
函数。通常需要测试最后的错误代码以对不同错误做出不同反应。VC++ CRT 宏在不保留最后错误代码的同时,自身又调用其他可能通过将其设置为 0 来破坏最后错误代码的 Win32 函数。(坦白说,我必须说,我最近在重读这本书以寻找修订文章的信息时发现了这个论点。) - 缺点 5:潜在的副作用。
几乎所有的编码风格书都提到了断言副作用的危险。(请参阅 [Writing Solid Code, page 38], [Code Complete, page 96] 和 [MSDN: Assertions]。)如果
ASSERT
宏中测试的表达式很复杂,并且包含函数调用,那么该表达式可能会偶尔更改数据。程序的调试版本可能会依赖于这种偶然性,而发布版本将因缺少函数调用而失败。虽然黄金法则是尽量不要在断言中调用函数,但这些情况仍然会发生。我相信通过将断言集成到实际算法中,可以消除这类缺陷。请注意,在这种情况下,当断言被禁用时,编译器可以优化掉不必要的方程,但有少量例外可以手动处理。
附录 C:历史
QAFDebug/QAFTrace
版本 1.6.3.93- 本文已更新为最新更改和修复。
- 支持 VC++ 6.0/7.0/7.1/8.0(已在 VC++ 6.0 SP 6 和 VC++ .NET 2005 beta 上测试)。
- 支持 Linux(已在 Feudora Core 2 发行版上测试)。
- 改进和修复了跟踪日志功能 - 宏名称已更正,添加了跟踪日志级别,添加了仅限 DEBUG 和 DEBUG/RELEASE 的日志。
QAFDebug/QAFTrace
版本 1.6.0.71- 与版本 1.6.0.70 相同 + 修复了论坛中指出的几个小错误 + 添加了 #pragma 以自动链接到 version.lib。
QAFDebug
版本 1.6.0.70- 本文已更新为最新更改和修复。
- 支持 VC++ 6.0/7.0/7.1(已在 VC++ 6.0 SP 5 和 VC++ .NET 2003 上测试)。
- 支持 Embedded VC++ 3.0(感谢 Vadim Entov 将源代码移植到 Windows CE)。
- 输出错误消息格式已更改,以提高可读性和详细程度。
- 添加了新宏:
Q_CHECK
、Q_ERROR_SUCCESS
、Q_MAPI_ERROR
、Q_SOCKET_ERROR
、Q_RETURN
、Q_MFC_EXCEPTION
、Q_STD_EXCEPTION
和Q_SET_MODULE
(感谢 Andrey Krasnopolsky 在库开发中的帮助)。 - 输出错误消息包含源模块文件名和版本号。
- 修复了大型消息的缓冲区溢出错误(现在错误消息被截断为 600 个字符)。
QAFDebug
版本 1.5.0.20- 文章已完全重写,包含更好的示例和论证。
- 修复了
Q_EXCEPTION
宏中的语法错误。 - 修复了 UNICODE 版本中的内存分配错误(我分配了字节而不是
TCHAR
)。 - 按照 [Debugging Applications, page 62] 添加了对最后错误代码的保留。
- 在 RELEASE 版本中,我删除了打印表达式 - 这应该可以优化可执行文件的大小,因为文件名和行号足以定位问题。
附录 D:缺失的功能
- 我想报告调用堆栈,至少在 DEBUG 版本中。问题是大多数示例都特定于 Windows NT。
- 我希望能够实时获取错误报告。比如说,我在网络中定义一个计算机,它收集所有错误日志,对其进行排序,然后转发给负责的开发人员。这应该可以可视化 QA 通常注意不到的一些无声缺陷。
- 如果程序正在从调试器运行,我仍然希望在发生错误时能够进入调试器,但这应该是一种开关(这个功能是我从标准
ASSERT
宏中丢失的)。
附录 E:TRACEX 宏和跟踪日志
在编写调试错误日志设施一段时间后,我发现自己试图将此日志用作跟踪日志。我清楚地认识到,错误日志的设计目标并不适合跟踪日志的要求。一个好的跟踪日志必须具有
- 强大的同步机制,以防止不同线程和进程死锁或发生竞争条件。
- 多个调试级别。
- 丰富的输出消息格式选项:
printf()
风格、C++ 流<<
风格、内存转储。 - 多个不同模块的跟踪日志文件。
- 日志被禁用时性能影响很小。
- 能够从单一已知位置按文件启用/禁用跟踪日志。
- 特定的轻量级输出格式(类似于:时间戳、进程 ID、线程 ID、消息、源文件、行号)。
- 能够自动将源代码位置写入跟踪日志。
- 能够将跟踪日志写入不同的输出流(文件、控制台或 VC++ IDE)。
这些都没有在关键错误日志设施中实现。此外,这些要求与关键错误日志的要求相矛盾。我分析了我当时能找到的每一个跟踪调试日志。没有一个完全适合我,但我学到了很多,也获得了一些新想法。编写一个跟踪日志并不是什么大事,所以我写了它。(大事是将其改进为一个“专业”组件,具有丰富的功能和文档。)目前该单元相当稳定,但仍有许多功能等待实现。无论如何,我喜欢它的灵活性、简洁性和丰富的格式选项。让我演示一下它的用法
#include "QAFTrace.h" // The variable declaration is somewhat need to be hidden with #ifdef #ifdef QD_TRACE_ENABLED // Create the static instance of the log file class, // specify the file name, default log level and the maximum file size in bytes. QTrace::LogFile log( _T("mylogfile.log"), QTrace::TraceDebug, 512 * 1024 ); #endif int main() { int i = 65, j = 5; int arr[5] = { i, i, i, i, i }; QD_TRACEX( log, "Output a simple string" ); QD_TRACEX( log, L"Output a UNICODE string, j = " << j ); QD_TRACEX( log, QTrace::Format( "printf-style %d, %d", i, j ) << " and more!" ); QD_TRACEX( log, "Format hex codes = 0x" << QTrace::Hex( i ) ); QD_TRACEX( log, QTrace::Dump( &arr[0], sizeof(arr), _T("int[5]") ) ); return 0; }
输出将如下所示
01:31 14:05:01:342 [3DA:11E] Output a simple string <== c:\wrk\tst.cpp(10)
01:31 14:05:01:342 [3DA:11E] Output a UNICODE string + j = 5 <== c:\wrk\tst.cpp(11)
01:31 14:05:01:359 [3DA:11E] printf-style 65, 5 and more! <== c:\wrk\tst.cpp(12)
01:31 14:05:01:383 [3DA:11E] Format hex codes = 0x41 <== c:\wrk\tst.cpp(13)
01:31 14:05:01:402 [3DA:11E] Memory dump of "int[5]", address 0x3FE56470,
size 20b (0x14) <== c:\wrk\tst.cpp(14)
3FE56470 00 00 41 00 00 00 41 00 00 00 41 00 00 00 41 00 ..A...A...A...A.
3FE56480 00 00 41 00 ..A.
跟踪日志仅包含重要信息 - 时间戳、进程和线程 ID、消息以及源文件位置(可能从不同函数输出类似的消息)。一个进程中可以有不同的跟踪日志文件,并且许多进程可以写入同一个跟踪日志。所有跟踪日志都创建在关键错误日志所在的同一文件夹中。每个跟踪日志都可以通过注册表在 /HKLM/Software/YourCompany/Log/
下使用名为跟踪日志文件名(例如,“mylogfile.log”)的 DWORD
注册表值来启用/禁用。默认情况下,跟踪日志处于禁用状态,您需要定义一个特定的注册表值来启用它。请注意,在 Linux 上,通过设置环境变量来启用跟踪日志。此环境变量的名称与日志文件名相同,点替换为下划线“mylogfile_log”。
通常,我创建一个静态日志类实例,或者将其添加为另一个类的成员——同一个跟踪错误日志没有问题,因为它们使用命名互斥体进行同步(在 Linux 上尚未实现)。当跟踪日志被禁用时,性能影响非常小——Q_TRACEX
宏被转换为 if( !log.IsEnabled() ) ; else log.output();
结构。
有两种跟踪宏集:QD_XXXX 和 QR_XXXX。QD_XXXX 宏在 RELEASE 版本中被完全排除,以提高性能。QR_XXXX 宏允许创建在 DEBUG 和 RELEASE 版本中都可用的跟踪日志。
同样,跟踪调试日志的目的是帮助调试系统,而关键错误日志用于 QA 和早期生产中的错误诊断。这些目标非常不同。当程序按预期工作时,关键错误日志应保持为空,而调试跟踪日志可能会充斥着跟踪消息(如果它已启用)。