1. 程式人生 > >CString物件的一種錯誤的使用方式

CString物件的一種錯誤的使用方式

我現在做的系統有的時候會出現這樣的斷言失敗: Debug Error! DAMAGE: after Normal block (#328Array) at 0x182C30F0. 跟蹤一下,發現問題竟出在CString的解構函式中,於是拿出了大半天的時間來研究這個問題,終於發現了原因所在。 問題的起因是我像下面這樣呼叫無參的建構函式宣告一個CString物件: CString strText; 然後把它以這樣的方式傳遞給別的函式:(函式1) pVCG->GetRotDirection(WAVE_P, m_nWaveSide, strText.GetBuffer(0)); 而在這個函式裡對於字串指標進行了類似於如下的操作: sprintf(strDir, "%s", "CW");
這樣做的危險性在於當字串沒有被初始化的時候,CString內部指向緩衝區的指標指向的是一個隨機的地址,在CString的無參建構函式呼叫 瞭如下函式: _AFX_INLINE void CString::Init() { m_pchData = afxEmptyString.m_pchData; } m_pdhData的定義:LPTSTR m_pchData; afxEmptyString的定義是: #define afxEmptyString AfxGetEmptyString() const CString& AFXAPI AfxGetEmptyString() { return *(CString*)&_afxPchNil; }
_afxPchNil的來源如下: AFX_STATIC_DATA int _afxInitData[] = { -1, 0, 0, 0 }; AFX_STATIC_DATA CStringData* _afxDataNil = (CStringData*)&_afxInitData; AFX_COMDAT LPCTSTR _afxPchNil = (LPCTSTR)(((BYTE*)&_afxInitData)+sizeof(CStringData)); 從上面的程式碼可以看出,沒有進行初始化操的CString物件它們的緩衝區指標都是指向一塊相同的記憶體:和一個全域性陣列相關的地址。 而在函式1例呼叫sprintf修改CString物件的緩衝區的結果是修改所有未初始化CString內部緩衝區指標所指,這麼做是非常危險的。但是這還不是出現斷言錯誤的原因。 接下來的錯誤,更難被發現。接著我的程式又呼叫了兩次類似於下面的函式(函式2) pVCG->GetCompressionGrade(WAVE_QRS, m_nWaveSide, 0, 60, 0, 0, strText); 在這個函式的內部有str.Format(IDS_COMPRESSION_LESS);這樣的操作。 這是MFC裡CString::Format的相關程式碼: void AFX_CDECL CString::Format(UINT nFormatID, ...)
{ CString strFormat;//沒有直接修改自己,而是先對新宣告的字串進行操作 VERIFY(strFormat.LoadString(nFormatID) != 0); va_list argList; va_start(argList, nFormatID); FormatV(strFormat, argList); va_end(argList); } 而在void CString::FormatV(LPCTSTR lpszFormat, va_list argList)裡最後作如下操作: GetBuffer(nMaxLen); VERIFY(_vstprintf(m_pchData, lpszFormat, argListSave) <= GetAllocLength());//將修改後的字串拷貝到自己的緩衝區內 ReleaseBuffer(); 關鍵在GetBuffer: LPTSTR CString::GetBuffer(int nMinBufLength) { ASSERT(nMinBufLength >= 0); if (GetData()->nRefs > 1 || nMinBufLength > GetData()->nAllocLength) //如果指定的記憶體空間比已經分配的空間小的話,則重新分配,並釋放掉原來的記憶體 { #ifdef _DEBUG         // give a warning in case locked string becomes unlocked         if (GetData() != _afxDataNil && GetData()->nRefs < 0)                TRACE0("Warning: GetBuffer on locked CString creates unlocked CString!\n"); #endif         // we have to grow the buffer        CStringData* pOldData = GetData();         int nOldLen = GetData()->nDataLength;   // AllocBuffer will tromp it         if (nMinBufLength < nOldLen)                nMinBufLength = nOldLen;        AllocBuffer(nMinBufLength);        memcpy(m_pchData, pOldData->data(), (nOldLen+1)*sizeof(TCHAR));        GetData()->nDataLength = nOldLen;        CString::Release(pOldData); } ASSERT(GetData()->nRefs <= 1); // return a pointer to the character storage for this string ASSERT(m_pchData != NULL); return m_pchData; } 由於字串沒有被初始化,所以GetData()->nAllocLength=0,因此if語句塊被執行,重新在堆上分配記憶體,銷燬原來的記憶體,這才第一次給字 符串分配記憶體。 這時還不會出現問題,接下來還會執行類似函式1的操作。 最後問題之所以發生在CString被析構的時候,原因就在於,在執行函式2的時候,字串有了能容納4個位元組的緩衝區.如果除錯的時候開啟Memory視窗,在Address:文字框裡輸入一個堆記憶體的地址,可以發現VC在除錯版的程式裡為每個在堆裡分配的記憶體塊的後面加了4個位元組的內容,值全為FD,用於檢查記憶體越界。CString析構的時候,呼叫了除錯版的operator delete,它就以此為依據進行了記憶體檢測: if (!CheckBytes(pbData(pHead) + pHead->nDataSize, _bNoMansLandFill, nNoMansLandSize)) _RPT3(_CRT_ERROR, "DAMAGE: after %hs block (#%d) at 0x%08X.\n",                     szBlockUseName[_BLOCK_TYPE(pHead->nBlockUse)],                     pHead->lRequest,                     (BYTE *) pbData(pHead)); 由於後來再次呼叫的函式1時它產生的長度有的時候會大於4 ,就破壞了後面的邊界,所以會出現這樣的問題。 出現這種問題時,在除錯狀態下會在輸出視窗輸出如下類似資訊: memory check error at 0x182C7F22 = 0x57, should be 0xFD 結論: 1.所以str.GetBuffer(0)作為引數傳遞的時候適合於作為只讀的引數; 2.如果非得要做可以修改的引數,那就得給GetBuffer傳遞一個保證足夠安全的引數,也就是足夠大; 2.如果除錯版的程式出現類似 Debug Error! DAMAGE: after Normal block (#328Array) at 0x182C30F0. 的錯誤,應想到記憶體衝突。 問題終於水落石出了。反思一下,這個問題一點也不難,都怪自己基礎沒有打好,考慮問題不周全。