淺析ASSERT&TRACE宏

淺析ASSERT&TRACE宏

如果你沒有用過甚至聽過ASSERT或者TRACE調式宏,那麼在很大程度上,你可以忽略這篇文章。不再扯不相關的東西,我們直入主題好了。

1.TRACE
1.1.TRACE的宏定義


  同樣的,我們先從TRACE的宏定義開始研究,TRACE被定義在AFX.H中。但是我在這個H文件查找時,並沒有發現TRACE被#define成某個函數。雖然你會發現類似的下面兩行代碼:
引用:
#define TRACE              __noop
///////////////////////////////////
#define TRACE ATLTRACE
但是,ATL的宏定義並不是我們要找的,而__noop,如果你翻過MSDN會發現這個Keyword的作用僅僅是忽略被包含的函數及不對參數進行評估(具體請看A附錄)。

  那麼,TRACE到底是如何被使用的呢?機緣巧合之下(這個……),我在TRACE的宏定義附近發現了下面的代碼:
引用:
inline void AFX_CDECL AfxTrace(...) { }  // Look at here!
#define TRACE              __noop
#define TRACE0(sz)
#define TRACE1(sz, p1)
#define TRACE2(sz, p1, p2)
#define TRACE3(sz, p1, p2, p3)
關注那個AfxTrace。如果說我們前面找到的都不是真正的TRACE的話,那麼,這個AfxTrace就非常可能是我們要找的,而且,他還是個inline函數!於是,我以“AfxTrace”爲關鍵字Google,果然找到了一些信息。

  在以前的AFX.H文件中,存在類似下面的代碼:
引用:
#ifdef   _DEBUG   
  
void   AFX_CDECL   AfxTrace(LPCTSTR   lpszFormat,   ...);   
  
#define   TRACE                               ::AfxTrace   
#else   
  #define   
TRACE         1   ?   (void)0   :   ::AfxTrace   
#endif
很明顯,我們可以看到,TRACE被定義成了AfxTrace。但是這裏有個問題,爲什麼在我的VC2003的AFX.H文件中,找不到這段代碼呢?看來需要高人回答哈~

1.2.AfxTrace

  既然TRACE宏只是調用了AfxTrace,那麼我們就來看看AfxTrace函數實現了什麼。不過很可惜,AfxTrace並不是一個文檔記錄函數(Documented-function),這意味着你在MSDN是找不到他的相關信息,那麼我們只能通過他的源代碼來了解他的行爲。AfxTrace的源代碼在DUMPOUT.CPP裏。
引用:
void AFX_CDECL AfxTrace(LPCTSTR lpszFormat, ...)
{
  
va_list args;
  
va_start(args, lpszFormat);

  
int nBuf;
  
TCHAR szBuffer[512];

  
nBuf = _vsntprintf(szBuffer, _countof(szBuffer), lpszFormat, args);

  
// was there an error? was the expanded string too long?
  
ASSERT(nBuf >= 0);

  
afxDump << szBuffer;

  
va_end(args);
}
首先我們來看三個東西:
引用:
va_list args;
va_start(args, lpszFormat);
va_end(args);
這一組宏主要是用來解決函數的不定參數的。回想一下,我們可以在TRACE中輸出任意個參數,靠的就是這三個東西。因爲C沒有重載函數,更何況即使有,當函數參數個數不確定時,重載的侷限性就顯得非常大,於是就有人想通過利用指針參數解決了這個問題。

  由於這個東西比較複雜(MS足夠再寫一篇專門的文章了),所以在此不作詳細闡述,有興趣的可以參考下列URL:

1.MSDN:http://msdn.microsoft.com/en-us/library/kb57fad8.aspx
2.http://www.cppblog.com/qiujian5628/archive/2008/01/21/41562.html

  接着我們可以看到,AfxTrace聲明瞭大小爲512的TCHAR數組作爲緩衝區,然後用_vsntprintf往緩衝區裏寫已經格式化好的數據。

  而_vsntprintf系列函數專門用於以va_list處理可變參數的函數輸出。具體請參考附錄

  最後,AfxTrace又用afxDump對szBuffer進行轉儲(似乎就是輸出到輸出框)。看來,我們還需要對afxDump函數進行跟進。

1.3.afxDump

  afxDump是一個CDumpContext類的預定義對象,用於在Debug模式下,往VC的輸出窗口輸出調試信息。很幸運,M$在MSDN中記錄了他的一些信息。(具體請參考附錄)

  所以,一般的,當存在如下代碼時:
引用:
  LPCTSTR lpszKC = L"KC is a Fucker";
  
afxDump << lpszKC;
輸出框便會輸出“KC is a fucker”。

  我們現在來看看afxDump的定義代碼。源代碼在AFX.H中
引用:
#ifdef _DEBUG
extern AFX_DATA CDumpContext afxDump;  // Look At Here!
extern AFX_DATA BOOL afxTraceEnabled;  // 這個變量和afxTraceFlags同爲調式輸出的開關標誌
                                       // 不過MS在新版本的MFC中被廢除了
#endif
然後我們再來看看CDumpContext輔助類的定義。源代碼同樣也在AFX.H中
引用:
class CDumpContext
{
public:
  
CDumpContext(CFile* pFile = NULL);

// Attributes
  
int GetDepth() const;      // 0 => this object, 1 => children objects
  
void SetDepth(int nNewDepth);

// Operations
  
CDumpContext& operator<<(LPCTSTR lpsz);
#ifdef _UNICODE
  CDumpContext
& operator<<(LPCSTR lpsz);  // automatically widened
#else
  
CDumpContext& operator<<(LPCWSTR lpsz); // automatically thinned
#endif
  
CDumpContext& operator<<(const void* lp);
  
CDumpContext& operator<<(const CObject* pOb);
  
CDumpContext& operator<<(const CObject& ob);
  
CDumpContext& operator<<(BYTE by);
  
CDumpContext& operator<<(WORD w);
  
CDumpContext& DumpAsHex(BYTE b);
  
CDumpContext& DumpAsHex(WORD w);
#ifdef _WIN64
  CDumpContext
& operator<<(LONG l);
  
CDumpContext& operator<<(DWORD dw);
  
CDumpContext& operator<<(int n);
  
CDumpContext& operator<<(UINT u);
  
CDumpContext& DumpAsHex(LONG l);
  
CDumpContext& DumpAsHex(DWORD dw);
  
CDumpContext& DumpAsHex(int n);
  
CDumpContext& DumpAsHex(UINT u);
#else
  
CDumpContext& operator<<(LONG_PTR l);
  
CDumpContext& operator<<(DWORD_PTR dw);
  
CDumpContext& operator<<(INT_PTR n);
  
CDumpContext& operator<<(UINT_PTR u);
  
CDumpContext& DumpAsHex(LONG_PTR l);
  
CDumpContext& DumpAsHex(DWORD_PTR dw);
  
CDumpContext& DumpAsHex(INT_PTR n);
  
CDumpContext& DumpAsHex(UINT_PTR u);
#endif
  
CDumpContext& operator<<(float f);
  
CDumpContext& operator<<(double d);
  
CDumpContext& operator<<(LONGLONG n);
  
CDumpContext& operator<<(ULONGLONG n);
  
CDumpContext& DumpAsHex(LONGLONG n);
  
CDumpContext& DumpAsHex(ULONGLONG n);
  
CDumpContext& operator<<(HWND h);
  
CDumpContext& operator<<(HDC h);
  
CDumpContext& operator<<(HMENU h);
  
CDumpContext& operator<<(HACCEL h);
  
CDumpContext& operator<<(HFONT h);
  
void HexDump(LPCTSTR lpszLine, BYTE* pby, int nBytes, int nWidth);
  
void Flush();
// Implementation
protected:
  
// dump context objects cannot be copied or assigned
  
CDumpContext(const CDumpContext& dcSrc);
  
void operator=(const CDumpContext& dcSrc);
  
void OutputString(LPCTSTR lpsz);
  
int m_nDepth;

public:
  
CFile* m_pFile;
};
CDumpContext只有一個構造函數,而且默認把m_pFile設置成了NULL,這點很關鍵,我們在後面馬上會看到~

  這裏可能會有點疑問,爲什麼會存在一個CFile*類型的Public成員變量?我也不知道,KC個人的猜測是,CDumpContext不僅能夠往輸出框輸出信息,應該還能夠往外寫文件。而下面的m_pFile->Write也能夠支持我的猜測。

  另一個亮點是,CDumpContext中存在多個<<重載運算符,這樣便於afxDump進行不同類型的<<運算。不過這裏有一個插曲,CDumpContext的上述代碼中有幾行比較有意思:
引用:
// Operations
  
CDumpContext& operator<<(LPCTSTR lpsz);
#ifdef _UNICODE
  CDumpContext
& operator<<(LPCSTR lpsz);  // automatically widened
#else
  
CDumpContext& operator<<(LPCWSTR lpsz); // automatically thinned
#endif
之前我一直不明白這段的用意,後來經D大提醒,幡然醒悟。

  這段宏的作用大致是:在UNICODE下,遇到MBCS字符串自動做擴大處理;在MBCS下,遇到UNICODE字符串自動做縮小處理。相應的實現代碼如下:
引用:
#ifdef _UNICODE
// special version for ANSI characters
CDumpContext& CDumpContext::operator<<(LPCSTR lpsz)
{
  
if (lpsz == NULL)
  {
   
OutputString(L"(NULL)");
   
return *this;
  }

  
// limited length
  
TCHAR szBuffer[512];
  
_mbstowcsz(szBuffer, lpsz, _countof(szBuffer));
  
szBuffer[511] = 0;
  
return *this << szBuffer;
}
#else   //_UNICODE
// special version for WIDE characters
CDumpContext& CDumpContext::operator<<(LPCWSTR lpsz)
{
  
if (lpsz == NULL)
  {
   
OutputString("(NULL)");
   
return *this;
  }

  
// limited length
  
char szBuffer[512];
  
_wcstombsz(szBuffer, lpsz, _countof(szBuffer));
  
szBuffer[511] = 0;
  
return *this << szBuffer;
}
#endif  //!_UNICODE

/////////////////////////////////////////////////////////////////////////////
接下來我們重點看CDumpContext對<<的實現。雖然<<的重載很多,但是從本質上,可以分成對String和數值類型的兩類。那麼我們先來看看對於數值類型的處理,額,隨便挑一個~當~當~當~當~
引用:
CDumpContext& CDumpContext::operator<<(WORD w)
{
  
TCHAR szBuffer[32];

  
wsprintf(szBuffer, _T("%u"), (UINT) w);
  
OutputString(szBuffer);

  
return *this;
}
因爲是數值類型,所以算上64Bit的大整數,也長不到哪裏去。所以這裏分配的緩衝區數組的下標只有32.

  然後利用wsprintf把數字格式化,最後用OutputString輸出。wsprintf詳細信息請參考附錄

  至於對String的處理,代碼如下:
引用:
CDumpContext& CDumpContext::operator<<(LPCTSTR lpsz)
{
  
if (lpsz == NULL)
  {
   
OutputString(_T("NULL"));
   
return *this;
  }

  
ASSERT( lpsz != NULL );
  
if( lpsz == NULL )
   
AfxThrowUserException();

  
if (m_pFile == NULL)
  {
   
TCHAR szBuffer[512];
   
LPTSTR lpBuf = szBuffer;
   
while (*lpsz != '/0')
    {
      
if (lpBuf > szBuffer + _countof(szBuffer) - 3)
      {
        *
lpBuf = '/0';
        
OutputString(szBuffer);
        
lpBuf = szBuffer;
      }
      
if (*lpsz == '/n')
        *
lpBuf++ = '/r';
      *
lpBuf++ = *lpsz++;
    }
    *
lpBuf = '/0';
   
OutputString(szBuffer);
   
return *this;
  }

  
m_pFile->Write(lpsz, lstrlen(lpsz)*sizeof(TCHAR));
  
return *this;
}
做<<前,先對參數進行合法性檢查,然後在m_pFile爲NULL的情況下,分配緩衝區(由於是字符串,所以下標爲512),然後逐一的複製字符,最後相同的用OutputString轉出。

  比較上面兩種<<的運算實現,我們可以很明顯的看出,最後的數據都被傳遞到了OutputString裏,所以我們還必須跟進OutputString。

1.4.OutputString

  我們現在跳到OutputString的實現源代碼上:
引用:
void CDumpContext::OutputString(LPCTSTR lpsz)
{
  
// use C-runtime/OutputDebugString when m_pFile is NULL
  
if (m_pFile == NULL)
  {
   
TRACE(traceDumpContext, 0, lpsz);
   
return;
  }
  
ASSERT( lpsz != NULL );
  
if( lpsz == NULL )
   
AfxThrowUserException();
  
// otherwise, write the string to the file
  
m_pFile->Write(lpsz, lstrlen(lpsz)*sizeof(TCHAR));
}
因爲前面說過,m_pFile的值爲NULL(我們沒有給他傳值,構造函數又自動給他NULL掉了),所以OutputString應該會執行下面的代碼:
引用:
// use C-runtime/OutputDebugString when m_pFile is NULL
  
if (m_pFile == NULL)
  {
   
TRACE(traceDumpContext, 0, lpsz);
   
return;
  }
很奇怪,很神奇,很……囧……又回到了TRACE……

  更何況,上面註釋寫着use C-runtime/OutputDebugString的字眼呢,多大個的字啊……

  無奈中,我去翻了下MSDN,又去Google,結果得到了驚人的發現!在MSDN,對於CDumpContext有這麼一段的描述:
引用:
Under the Windows environment, the output from the predefined afxDump object, conceptually similar to the cerr stream, is routed to the debugger via the Windows function OutputDebugString.
換句話說,轉儲的東西的的確確會經過底層的C運行時庫函數或者OutputDebugString這個API。

  此時我想起了之前出現的一個關於TRACE的BUG:在UNICODE下無法輸出中文。當時我通過F9/F10/F11不斷的跟進,但是單語句調試到TRACE(traceDumpContext, 0, lpsz)這裏時,卻提示沒有可顯示的語句。所以,有可能轉儲的東西跑到了某個C底層函數去(如果是OutputDebugString,中文也應輸出)。

  於是我把目光瞄準了traceDumpContext,發現這個是個宏(很奇怪,是ATL系列的),經過多次進進出出的跟進後,發現了一個叫做CTrace的類,而且在裏面還發現如下代碼:
引用:
class CTrace
{
public:
  
typedef int (__cdecl *fnCrtDbgReport_t)(int,const char *,int,const char *,const char *,...);

private:
  
CTrace(
#ifdef _ATL_NO_DEBUG_CRT
    fnCrtDbgReport_t pfnCrtDbgReport
= NULL)
#else
   
fnCrtDbgReport_t pfnCrtDbgReport = _CrtDbgReport)
#endif
我很敏感的關注了_CtrDbgReport這個函數,去MSDN翻了下,得到的結果很驚人!
引用:
Generates a report with a debugging message and sends the report to three possible destinations (debug version only).
而且,Remark上還有這麼一段(具體請參考附錄):
引用:
In Visual C++ 2005, _CrtDbgReportW is the wide-character version of _CrtDbgReport. All its output and string parameters are in wide-character strings; otherwise it is identical to the single-byte character version.

_CrtDbgReport and _CrtDbgReportW create the user message for the debug report by substituting the argument[n] arguments into the format string, using the same rules defined by the printf or wprintf functions. These functions then generate the debug report and determine the destination or destinations, based on the current report modes and file defined for reportType. When the report is sent to a debug message window, the filename, lineNumber, and moduleName are included in the information displayed in the window.
所以,我最後在假設的情況下得出了下列結論:

  OutputString通過宏定義,最終轉到了_CtrDbgReport這個C-Runtime函數上,有他負責在底層的某個地方往輸出框寫調試信息。

  又因爲我的IDE是2003,所以_CtrDbgReport只能輸出Single-byte Char,所以無法輸出中文。

  如此,我們的淺析TRACE的部分應該算是圓滿結束了-。-||

2.ASSERT

  看完了TRACE,我們再來看看ASSERT。相對來說,ASSERT就比TRACE簡單很多,所以我們不再分佈敘述~

  老規矩,還是先從宏定義開始入手。源代碼在AFX.H中
引用:
#define ASSERT(f)      (void) ((f) || !AfxAssertFailedLine(THIS_FILE, __LINE__) || (AfxDebugBreak(), 0))
比較簡單,只有一行。不過不得不說的是,這句寫得非常巧妙。你還記得||的運算順序麼?

  當f爲FALSE的時候,會跳轉到AfxAssertFailedLine,這個函數的源代碼在AFXASERT.CPP中
引用:
BOOL AFXAPI AfxAssertFailedLine(LPCSTR lpszFileName, int nLine)
{
#ifndef _AFX_NO_DEBUG_CRT
  
// we remove WM_QUIT because if it is in the queue then the message box
  // won't display
  
MSG msg;
  
BOOL bQuit = PeekMessage(&msg, NULL, WM_QUIT, WM_QUIT, PM_REMOVE);
  
BOOL bResult = _CrtDbgReport(_CRT_ASSERT, lpszFileName, nLine, NULL, NULL);
  
if (bQuit)
   
PostQuitMessage((int)msg.wParam);
  
return bResult;
#else
  
// Not supported.
#error _AFX_NO_DEBUG_CRT is not supported.
#endif // _AFX_NO_DEBUG_CRT
}
首先,AfxAssertFailedLine會阻塞所有活動的線程,然後顯示我們非常熟悉也非常不願意見到的消息框-_-!。這裏也有一個_CtrDbgReport,個人猜測,那個消息框可能就是通過這個C-Runtime函數產生的。

  這個函數成功後會返回TRUE,經過!運算後就變成了FALSE,於是繼續執行AfxDebugBreak。

  AfxDebugBreak是一個系列宏定義,源代碼在AFXVER_.H中
引用:
#ifndef AfxDebugBreak
#ifdef _AFX_NO_DEBUG_CRT
// by default, debug break is asm int 3, or a call to DebugBreak, or nothing
#if defined(_M_IX86) && !defined(_AFX_PORTABLE)
#define AfxDebugBreak() _asm { int 3 }
#else
#define
AfxDebugBreak() DebugBreak()
#endif
#else
#define
AfxDebugBreak() _CrtDbgBreak()
#endif
#endif

#ifndef
_DEBUG
#ifdef AfxDebugBreak
#undef AfxDebugBreak
#endif
#define
AfxDebugBreak()
#endif  // _DEBUG
根據不同的環境使用不同的方式。不過他的表現行爲還都是類似的,主要是把控制返回給VC的調試器,這樣一來,你就可以交互式的進行調式。

3.尾聲

  好了,終於可以說End Up了~這個算是KC放假後做的第一個Project吧,雖然不具有實際價值-_-!。其實我很早之前就像研究這兩個宏了,只是當時沒有時間。

  之前被TRACE在UNICODE下無法輸出中文的問題困擾了好久,一直說要研究SRC弄清楚原因,現在總算可以給自己交上一份答卷了~HOHO~

  最後感謝支持我的各位,感謝MDSA Group、JAFT、CFAN的各位~

發佈了79 篇原創文章 · 獲贊 6 · 訪問量 31萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章